前语

上一篇文章学了下自界说View的onDraw函数及自界说特点,做出来的翻滚挑选控件还算不错,便是逻辑复杂了一些。这篇文章打算利用自界说view的常识,直接手撕一个安卓侧滑栏,涉及到自界说LayoutParams、带padding和margin的measure和layout、利用requestLayout完成动画作用等,有必定难度,但能从头学到很多常识!

需求

这儿相似旧版QQ(我特别喜爱之前的侧滑栏),有两层页面,滑动不是最左侧才触发的,而是从中间页面滑动就触发,滑动的时分主页面和侧滑栏页面会以不同速度滑动,中心思路如下:

  • 1、两部分,主内容和左面侧滑栏,侧滑栏不彻底占满主内容
  • 2、在主内容页面向右滑动展示侧滑栏,同时主内容以更慢的速度向右滑动
  • 3、侧滑栏彻底显现时不再左滑
  • 4、相似侧滑栏,经过自界说特点来指定侧滑栏页面,其他view为主内容
  • 5、侧滑栏就一个view,容器内其他view作为主内容,view摆放相似笔直方向LinearLayout

作用图

自定义view实战(5):手撕安卓侧滑栏

编写代码

代码有点长,并且有些没用的代码没用注释,不过我期望的是能经过这些没用的代码来阐明思路的不正确性。就像移动时的动画,本来我以为主内容和侧滑栏一同scrollTo就处理了,成果并不是。下面时代码:

import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.core.animation.addListener
import androidx.core.view.forEach
import com.silencefly96.module_common.R
import kotlin.math.abs
/**
 * 相似旧版QQ,两层页面,切换的运用有互相移动动画
 * 中心思路
 * 1、两部分,主内容和左面侧滑栏,侧滑栏不彻底占满主内容
 * 2、在主内容页面向右滑动展示侧滑栏,同时主内容以更慢的速度向右滑动
 * 3、侧滑栏彻底显现时不再左滑
 * 4、相似侧滑栏,经过自界说特点来指定侧滑栏页面,其他view为主内容
 * 5、侧滑栏就一个view,容器内其他view作为主内容,view摆放相似笔直方向LinearLayout
 */
@Suppress("unused")
class TwoLayerSlideLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): ViewGroup(context, attributeSet, defStyleAttr){
    @Suppress("unused")
    companion object{
        //侧滑共有四个方向,一个不设置的特点,暂时只完成GRAVITY_TYPE_LEFT
        const val GRAVITY_TYPE_NULL = -1
        const val GRAVITY_TYPE_LEFT = 0
        const val GRAVITY_TYPE_TOP = 1
        const val GRAVITY_TYPE_RIGHT = 2
        const val GRAVITY_TYPE_BOTTOM = 3
        //滑动状况
        const val SLIDE_STATE_TYPE_CLOSED = 0
        const val SLIDE_STATE_TYPE_MOVING = 1
        const val SLIDE_STATE_TYPE_OPENED = 2
    }
    //侧滑栏控件
    private var mSlideView: View? = null
    //滑动状况
    private var mState = SLIDE_STATE_TYPE_CLOSED
    //最大滑动长度
    private var maxScrollLength: Float
    //最大动画运用时间
    private var maxAnimatorPeriod: Int
    //上次事情的横坐标
    private var mLastX = 0f
    //累计的滑动间隔
    private var mScrollLength: Float = 0f
    //侧滑栏所占份额
    private var mSidePercent: Float = 0.75f
    //切换到方针状况的特点动画
    private var mAnimator: ValueAnimator? = null
    init {
        //读取XML参数
        val attrArr = context.obtainStyledAttributes(attributeSet, R.styleable.TwoLayerSlideLayout)
        //取得XML里边设置的最大滑动长度,没有的话需求在onMeasure后依据控件宽度设置
        maxScrollLength = attrArr.getDimension(R.styleable.TwoLayerSlideLayout_maxScrollLength,
            0f)
        //最大动画时间
        maxAnimatorPeriod = attrArr.getInteger(R.styleable.TwoLayerSlideLayout_maxAnimatorPeriod,
            300)
        //侧滑栏所占份额
        mSidePercent = attrArr.getFraction(R.styleable.TwoLayerSlideLayout_mSidePercent,
            1,1,0.75f)
        attrArr.recycle()
    }
    //丈量会进行屡次
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //用默认办法,核算出所有的childView的宽和高,带padding不带margin
        //measureChildren(widthMeasureSpec, heightMeasureSpec)
        //getDefaultSize会依据默认值、模式、spec的值给到成果,建议点进去看看
        val width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)
        val height = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec)
        //相似笔直方向LinearLayout,核算一下笔直方向高度运用状况
        //var widthUsed = 0
        var heightUsed = paddingTop
        var childWidthMeasureSpec: Int
        var childHeightMeasureSpec: Int
        forEach { child->
            //获取设定的gravity,用于判定是否是侧滑栏view,只需终究一个
            val childLayoutParams = child.layoutParams as LayoutParams
            val gravity = childLayoutParams.gravity
            if (gravity != GRAVITY_TYPE_NULL) {
                //暂不支撑除左滑以外的状况
                if (gravity != GRAVITY_TYPE_LEFT)
                    throw IllegalArgumentException("function not support")
                //取到侧滑栏,多个时取终究一个
                mSlideView = child
                //侧滑栏大小另外丈量,高度铺满父容器,宽度设置为父容器的四分之三
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    (width * mSidePercent).toInt(), MeasureSpec.EXACTLY)
                //高度不限定
                childHeightMeasureSpec =
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
                //侧滑栏不带padding和margin
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
            }else {
                //宽按需求恳求,所以应该用AT_MOST,并向下层view传递
                childWidthMeasureSpec =
                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST)
                childHeightMeasureSpec =
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)
                //heightUsed会在getChildMeasureSpec中用到,MATCH_PARENT时占满剩下size
                //WRAP_CONTENT时,会带着MeasureSpec.AT_MOST及剩下size向下层传递
                //带padding和margin的丈量,推荐看看measureChildWithMargins
                //里边用到的getChildMeasureSpec函数,加深对MeasureSpec了解
                measureChildWithMargins(child, widthMeasureSpec, 0,
                    heightMeasureSpec, heightUsed)
                //核算的时分要加上child的margin值
                //widthUsed += child.measuredWidth
                heightUsed += child.measuredHeight +
                        childLayoutParams.topMargin + childLayoutParams.bottomMargin
            }
        }
        //终究加上本控件的paddingBottom,终究核算得到终究高度
        heightUsed += paddingBottom
        //设置最大滑动长度为宽度的三分之一
        if (maxScrollLength == 0f) {
            maxScrollLength = width / 3f
        }
        //设置丈量参数,这儿不能用heightUsed,因为尽管主内容可能未用完height,可是侧滑栏用完了height
        setMeasuredDimension(width, height)
    }
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        //滑动时的偏移值,核算dx的时分时前面减后边,这儿偏移值应该是后边减前面,所以取负
        val mainOffset = -mScrollLength / maxScrollLength * measuredWidth * (1 - mSidePercent)
        val slideOffset = -mScrollLength / maxScrollLength * (measuredWidth * mSidePercent)
        //不要忘记了paddingTop和paddingLeft,不然内容会被padding的布景掩盖
        var curHeight = paddingTop
        //布局
        var layoutParams: LayoutParams
        var gravity: Int
        var cTop: Int
        var cRight: Int
        var cLeft: Int
        var cBottom: Int
        forEach { child ->
            //获取设定的gravity,用于判定是否是侧滑栏view,只需终究一个
            layoutParams = child.layoutParams as LayoutParams
            gravity = layoutParams.gravity
            //布局主内容中view
            if (gravity == GRAVITY_TYPE_NULL) {
                //其他view带上累加高度布局
                cTop = layoutParams.topMargin + curHeight
                cLeft = paddingLeft + layoutParams.leftMargin + mainOffset.toInt()
                cRight = cLeft + child.measuredWidth
                cBottom = cTop + child.measuredHeight
                //布局
                child.layout(cLeft, cTop, cRight, cBottom)
                //累加高度
                curHeight = cBottom + layoutParams.bottomMargin
            }
        }
        //终究制作侧滑栏,使其在最顶层???这儿直接layout是没用的,绘想想看,制作是onDraw的职责,这儿有两个种办法
        //一是在XML中将侧滑栏放到终究去,二是将mSlideView放到children的终究去,onDraw内应该是for循环制作的
        mSlideView?.let {
            //下面办法是专门在onLayout办法中运用的,不会触发requestLayout
            removeViewInLayout(mSlideView)
            addViewInLayout(mSlideView!!, childCount, mSlideView!!.layoutParams)
            //这儿还有一个问题,当当时view设置padding的时分,侧滑栏会被裁切,设置不裁切padding内容
            this.layoutParams.apply {
                //不裁切孙view在父view超出的部分,让孙view在爷爷view中正常显现,这儿不需求
                //clipChildren = false
                clipToPadding = false
            }
            //在页面左面
            cTop = 0
            cRight = slideOffset.toInt()
            cLeft = cRight - mSlideView!!.measuredWidth
            cBottom = cTop + mSlideView!!.measuredHeight
            //布局
            mSlideView!!.layout(cLeft, cTop, cRight, cBottom)
        }
    }
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
    }
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        ev?.let {
            when(ev.action) {
                MotionEvent.ACTION_DOWN -> preMove(ev)
                MotionEvent.ACTION_MOVE -> return true
            }
        }
        return super.onInterceptTouchEvent(ev)
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        ev?.let {
            when(ev.action) {
                //假如子控件未拦截ACTION_DOWN事情或许点击在view没有子控件的地方,onTouchEvent要处理
                MotionEvent.ACTION_DOWN -> {
                    //preMove(ev)
                    return true
                }
                MotionEvent.ACTION_MOVE -> moveView(ev)
                MotionEvent.ACTION_UP -> stopMove()
            }
        }
        return super.onTouchEvent(ev)
    }
    private fun preMove(e: MotionEvent) {
        mLastX = e.x
        if (mState == SLIDE_STATE_TYPE_MOVING) {
            //要撤销完毕监听,避免过错修正状况,把当时方位交给接下来的滑动处理
            mAnimator?.removeAllListeners()
            mAnimator?.cancel()
        }else {
            //封闭和打开时,点击滑动应该切换状况
            mState = SLIDE_STATE_TYPE_MOVING
        }
    }
    private fun moveView(e: MotionEvent) {
        //没有侧滑栏不移动,避免屡次恳求布局
        if (mSlideView == null) return
        //留意前面减去后边,便是页面应该scroll的值
        val dx = mLastX - e.x
        mLastX = e.x
        //Log.e("TAG", "moveView: mScrollLength=$mScrollLength")
        //设定滑动规模,留意mScrollLength和scrollX是不相同的,咱们要完成不同的滑动作用
        //留意滑动的是窗口,view是窗口下的内容,手指向右滑动,页面(即主内容)向左移动,窗口向右移动
        if ((mScrollLength + dx) >= -maxScrollLength && (mScrollLength + dx) <= 0) {
            //规模内,叠加差值
            mScrollLength += dx
            //手指向右滑动,主内容向左缓慢滑动,侧滑栏向右滑动
            //要表现更慢的速度,主内容就移动侧滑栏所占份额的剩下值
            //val mainDx =  dx / maxScrollLength * measuredWidth * (1 - mSidePercent)
            //scrollBy(mainDx.toInt(), 0)
            //侧滑栏速度更大,这儿依据最大滑动间隔和侧滑栏的宽度做个映射
            //val sideDx = dx / maxScrollLength * (measuredWidth * mSidePercent)
            //侧滑栏的移动不能运用scrollTo和scrollBy,因为只是移动的是其中的内容,并不会移动整个view
            //能够了解成scrollTo和scrollBy只是在该方针的原有方位移动,即使移动了也不会在其规模之外显现(draw)
            //特点动画能够完成在父容器里边对子控件的移动,可是也是经过修正特点值从头布局完成的
            //sideView!!.scrollTo(sideView!!.scrollX + sideDx.toInt(), 0)
            //这儿累加mScrollLength后直接恳求从头布局,在onLayout里边去处理移动
            requestLayout()
        }
    }
    private fun stopMove() {
        //停止后,运用动画移动到方针方位
        val terminalScrollX: Float = if (abs(mScrollLength) >= maxScrollLength / 2f) {
            //触发移动至彻底打开,mScrollLength是个负数
            -maxScrollLength
        }else {
            //假如移动没过半应该康复状况,则康复到本来状况
            0f
        }
        //这儿运用ValueAnimator处理剩下的间隔,模拟滑动到需求的方位
        mAnimator = ValueAnimator.ofFloat(mScrollLength, terminalScrollX)
        mAnimator!!.addUpdateListener { animation ->
            mScrollLength = animation.animatedValue as Float
            //恳求从头布局
            requestLayout()
        }
        //动画完毕时要更新状况
        mAnimator!!.addListener (onEnd = {
            mState = if(mScrollLength == 0f) SLIDE_STATE_TYPE_CLOSED else SLIDE_STATE_TYPE_OPENED
        })
        //滑动动画总时间应该和间隔有关
        val percent = 1 - abs(mScrollLength / maxScrollLength)
        mAnimator!!.duration = (maxAnimatorPeriod * abs(percent)).toLong()
        //mAnimator.duration = maxAnimatorPeriod.toLong()
        mAnimator!!.start()
    }
    //自界说的LayoutParams,子控件运用的是父控件的LayoutParams,所以父控件能够增加自己的特点,在子控件XML中运用
    @Suppress("MemberVisibilityCanBePrivate")
    class LayoutParams : MarginLayoutParams {
        //侧滑栏方向,不设置便是null
        var gravity: Int = GRAVITY_TYPE_NULL
        //三个结构
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
            //读取XML参数,设置相关特点,这儿有个很烦的warning,样式有必要是外部类加layout结尾
            val attrArr =
                context.obtainStyledAttributes(attrs, R.styleable.TwoLayerSlideLayout_Layout)
            gravity = attrArr.getInteger(
                R.styleable.TwoLayerSlideLayout_Layout_slide_gravity, GRAVITY_TYPE_NULL)
            //收回
            attrArr.recycle()
        }
        constructor(width: Int, height: Int) : super(width, height)
        constructor(source: ViewGroup.LayoutParams) : super(source)
    }
    //重写下面四个函数,在布局文件被填充为方针的时分调用的
    override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams {
        return LayoutParams(context, attrs)
    }
    override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams {
        return LayoutParams(p)
    }
    override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
        return LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT)
    }
    override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean {
        return p is LayoutParams
    }
}

下面是合作运用的XML特点代码:

res->value->two_layer_slide_layout_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name ="TwoLayerSlideLayout">
        <attr name="maxScrollLength" format="dimension"/>
        <attr name="maxAnimatorPeriod" format="integer"/>
        <attr name="mSidePercent" format="fraction"/>
    </declare-styleable>
    <declare-styleable name ="TwoLayerSlideLayout.Layout">
        <attr name ="slide_gravity">
            <enum name ="left" value="0" />
            <enum name ="top" value="1" />
            <enum name ="right" value="2" />
            <enum name ="bottom" value="3" />
        </attr >
    </declare-styleable>
</resources>

运用时在XML里边的例子,kotlin代码几乎不必写了,留意命名空间是app,res-auto引入了咱们的特点:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.silencefly96.module_common.view.TwoLayerSlideLayout
        android:id="@+id/hhView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/teal_700"
        android:padding="50dp"
        app:mSidePercent="75%"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <LinearLayout
            app:slide_gravity="left"
            android:background="@color/teal_200"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:text="@string/test_string"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </LinearLayout>
        <TextView
            android:background="@color/purple_200"
            android:layout_marginTop="10dp"
            android:text="@string/app_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:background="@color/purple_200"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:text="@string/app_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:background="@color/purple_200"
            android:layout_marginTop="50dp"
            android:text="@string/test_string"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </com.silencefly96.module_common.view.TwoLayerSlideLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

主要问题

说起这个控件,问题可就很多了,当然学到的东西也特别多,下面好好讲讲。

自界说XML中Fraction的运用

上一篇文章实践也用到了这个类型的特点,即百分比,可是我没测试下。在这个控件里边自己设置了一下,发现这个并不是像我幻想填小数或许100内的整数,而是填完百分比后还要自己加一个百分号“%”!至于getFraction里边的base和pbase能够自己搜一下,我这就不打开讲了,究竟主要内容是自界说view。

View供给的getDefaultSize

前面都是自己写一个getSizeFromMeasureSpec函数来依据MeasureSpec模式取得size,没想到View中现已供给了一个如出一辙的功用,尴尬了。

自界说LayoutParams

这个是这篇文章的重头戏了,没学习之前,我是万万没想到一个View的LayoutParams特点居然是父viewgroup的LayoutParams类型,并且自界说Viewgroup的同时还得自界说本身的LayoutParams,不然LayoutParams就一个height和一个width参数。话不多说,下面大致讲讲,详细的仍是找材料再补充下!

了解

关于一个View的LayoutParams特点居然是父viewgroup的LayoutParams类型的描绘,其实也很好了解,想想常常用到的ConstraintLayout,我能不便是在它的子view中设置束缚特点么。所以咱们要完成一个Layout,那子view不便是运用Layout的LayoutParams么。更何况哪面试常常问的问题来说,一个view的宽高受什么影响,不便是父viewgroup的MeasureSpec和子view的LayoutParams决定的么,子view要对父view进行束缚,那不就得知道父view需求控制什么特点么!

好了上面是我的了解,下面开端阐明怎样运用。

LayoutParams需求

首先咱们这儿要完成一个相似官方侧滑栏的功用,信任大家都用过DrawerLayout,在DrawerLayout里边咱们经过指定一个子view的layout_gravity就能让它成为侧滑栏,没错,咱们这也想完成这样的作用。一开端我就直接写嘛,app:layout_gravity不便是官方的么,可是我在XML中输入这样一个特点,在onMeasure里边读取不就能够判定了。成果代码中的LayoutParams只有height和width两个参数,这麻烦了,找了下材料,本来要自己界说Viewgroup的LayoutParams!

自界说LayoutParams

这儿就大致讲下思路,代码里边注释写的很清楚,分三步吧。第一步是要在代码中创立一个自界说的LayoutParams,这儿我就直接写成内部类了,完成其中几个结构函数,并在结构里边读取到要用的参数;第二步便是自界说参数了,需求创立一个xml文件来界说参数,这儿用到了枚举类型的特点,并且代码里边也要界说好各种type,LayoutParams类中界说一个变量来储存这个特点;第三步便是重写在布局文件被填充为方针的时分调用的几个函数,就大功告成了。

运用的时分要自己强制转化一下,就能从子view的LayoutParams中拿到自界说的特点了。

带padding和margin的丈量

侧滑栏应该占满屏幕,不应该带padding和margin,另外丈量就行,很简单。主内容部分咱们要完成相似LinearLayout的作用,就得带上带padding和margin进行丈量。

这儿用到了measureChildWithMargins这个函数,他会接收child、MeasureSpec及宽高的运用状况对child进行带padding和margin的丈量,能够点进去看看这个函数,里边又会调用getChildMeasureSpec去取得child的MeasureSpec,依据MeasureSpec的三种类型及LayoutParams.layout_width/height的三种形式(确切值、wrap_content、match_parent),会产生九种不同的组合。

不过能够了解的是,控件假如设置了值那便是设置的值(三种状况);假如控件是match_parent,那EXACTLY和AT_MOST的值都会被该view用完(两种状况),假如是UNSPECIFIED就要特别处理了(一种状况);假如控件是wrap_content,在EXACTLY和AT_MOST里边,都会用给的值和AT_MOST生成一个新的MeasureSpec,并向下层传递下去,即wrap_content不知道要多大,可是知道最大有多大,下层的view按需索求(两种状况),在UNSPECIFIED里也是特别处理下(一种状况)。

这儿还有个heightUsed要留意下,累加的高度应该是父容器的padding,加上子控件的margin及高度共同构成的。我这儿只核算了高度,宽度上也是同理。在这儿的setMeasuredDimension函数中,用的是整个控件最大的高度,而不是heightUsed,因为侧滑栏占满了控件的高度。可是假如咱们只是是完成一个LinarLayout的话,就应该用这个heightUsed了。

带padding和margin的布局

这儿和上面丈量相似,要带上父容器的padding和子控件的margin以及子控件的宽高进行摆放。这儿暂时不涉及动画的话,便是要把各个child的left、top、right、bottom四个值核算清楚,同时留意curHeight的累加就行了。

侧滑栏被主内容里边控件掩盖显现问题

这儿有个很古怪的问题,便是侧滑栏会被主内容里边控件掩盖显现,侧滑栏能够掩盖主内容的布景,可是主内容里边的控件会在侧滑栏上面制作。这儿我把侧滑栏的view从XML第一个移到终究一个就没事了,可是这不符合咱们的逻辑,我又在onLayout里边终究去layout侧滑栏,成果仍是不可。后边想想制作应该是在draw里边吧,可能是直接for循环制作的,我用iterator移除再添加到终究去不就行了,后边发现children的iterator并未供给删去的功用,终究仍是发现了removeViewInLayout和addViewInLayout两个函数,是专门在onLayout里边运用的,按前面的逻辑试一下,公然就好了。

设置padding被裁切的问题

这儿假如在咱们的TwoLayerSlideLayout上设置padding,那就会呈现很神奇的作用,侧滑栏也有padding了,可是仔细看,侧滑栏的内容方位是没错的,便是有padding的方位,侧滑栏的内容会被主内容的布景掩盖。查了下材料,又学了几个东西,主要便是viewgroup的layoutParams里边有个clipToPadding特点,默认为true,会将padding部分的子view进行裁切,咱们在侧滑栏layout前把它设置为false就行了。

滑动不收效问题

假如看了我前面的文章,在带header和footer的翻滚控件中,中间翻滚的控件是TextView,也是无法移动,在那里我是经过设置clickable为true让TextView也会耗费ACTION_DOWN事情,然后保证viewgroup能收到move事情。在写当时控件的时分,不仅是里边的TextView不会耗费ACTION_DOWN事情了,并且因为咱们view还有很多是没有子view的空隙,点击在这些空隙里边同样不会耗费ACTION_DOWN事情,导致事情序列被丢掉,ACTION_MOVE事情也没了。

后边想想,好像还挺好处理的,之前没思考光去考虑TextView了,假如子控件没耗费耗ACTION_DOWN事情,事情会交到它的父控件的onTouchEvent处理,面试过的都知道,办法补救在这儿吗?无论是子控件未耗费,仍是点击在空隙上,终究都会把ACTION_DOWN事情交到当时控件的onTouchEvent办法内,咱们在这儿return true就能够了。

侧滑栏的移动

前面几篇文章都做过移动的处理了,这个view我开端也是照搬代码,运用scrollBy去移动,侧滑栏在主内容移动的基础上持续经过scrollBy移动,成果想法很好,还核算了一系列值,终究发现只有主内容会移动。实践想了想,我调用侧滑栏的scrollBy去移动,移动的也只是侧滑栏的内容啊,也便是说移动是在侧滑栏内部进行的,又持续看了下滑动作用,公然侧滑栏尽管没有被scrollBy滑动掩盖主内容,可是侧滑栏里边的内容的确是以我规划的速度进行的。

写道这儿我又想到了上面的clipToPadding特点,viewgroup的layoutParams还有一个clipChildren特点,便是不裁切不裁切孙view在父view超出的部分,可是就算侧滑栏里边的控件移动到了主内容上面,作用也仍是不对的,因为侧滑栏的布景并没有移动,也便是说这是不可行的。

这儿我想到了特点动画,特点动画是能够让整个view移动的,可是在每一个move事情里边去创立一个特点动画,每次移动一小部分吗?好像不太好,并且已然特点动画是依据特点去修正方位的,咱们直接去修正布局不就行了。这儿依据滑动值,核算出主内容和侧滑栏的偏移,然后运用requestLayout从头布局就能够了,布局的时分加上偏移,代码很简单。

滑动停止切换到方针方位

这儿和前面几个view相同,用ValueAnimator来模拟持续滑动,可是上一篇文章中翻滚挑选控件会因为动画没完毕有持续滑动导致呈现滑出界的问题,这儿处理下。主要便是增加了一个状况的判定,分三个状况,假如动画没有完毕,就点击进行滑动,在ACTION_DOWN事情时就把动画停了,并移除完毕监听回调,这时分并不会修正mScrollLength,能够持续交给新的滑动接管整个滑动过程,这样用起来就流畅多了!

滑动速度问题
val mainOffset = -mScrollLength / maxScrollLength * measuredWidth * (1 - mSidePercent)
val slideOffset = -mScrollLength / maxScrollLength * (measuredWidth * mSidePercent)

上面是咱们主内容和侧滑栏偏移的核算代码,逻辑是咱们设定一个让侧滑栏打开的最大滑动间隔,滑动的时分侧滑栏按滑动间隔占最大滑动间隔的份额去打开侧滑栏,也便是说滑动间隔等于最大滑动间隔时就打开了,中间按份额移动;对于主内容,咱们就让它移动的最大间隔为侧滑栏所占屏幕宽度的剩下值,也便是说滑动间隔等于最大滑动间隔时主内容就移动了侧滑栏占屏幕宽度的剩下值,中间同样时按份额移动。稍微了解下,很简单,假如侧滑栏占屏幕宽度的份额大于一半,那侧滑栏速度就比主内容大,反之主内容速度大,实践上这样也很合理!