Android自定义view实现侧滑栏详解

目录
  • 前言
  • 需求
  • 效果图
  • 编写代码
  • 主要问题

前言

上一篇文章学了下自定义View的onDraw函数及自定义属性,做出来的滚动选择控件还算不错,就是逻辑复杂了一些。这篇文章打算利用自定义view的知识,直接手撕一个安卓侧滑栏,涉及到自定义LayoutParams、带padding和margin的measure和layout、利用requestLayout实现动画效果等,有一定难度,但能重新学到很多知识!

需求

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

1、两部分,主内容和左边侧滑栏,侧滑栏不完全占满主内容

2、在主内容页面向右滑动展现侧滑栏,同时主内容以更慢的速度向右滑动

3、侧滑栏完全显示时不再左滑

4、类似侧滑栏,通过自定义属性来指定侧滑栏页面,其他view为主内容

5、侧滑栏就一个view,容器内其他view作为主内容,view摆放类似垂直方向LinearLayout

效果图

编写代码

代码有点长,而且有些没用的代码没用注释,不过我希望的是能通过这些没用的代码来说明思路的不正确性。就像移动时的动画,本来我以为主内容和侧滑栏一起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)

上面是我们主内容和侧滑栏偏移的计算代码,逻辑是我们设定一个让侧滑栏展开的最大滑动距离,滑动的时候侧滑栏按滑动距离占最大滑动距离的比例去展开侧滑栏,也就是说滑动距离等于最大滑动距离时就展开了,中间按比例移动;对于主内容,我们就让它移动的最大距离为侧滑栏所占屏幕宽度的剩余值,也就是说滑动距离等于最大滑动距离时主内容就移动了侧滑栏占屏幕宽度的剩余值,中间同样时按比例移动。稍微理解下,很简单,如果侧滑栏占屏幕宽度的比例大于一半,那侧滑栏速度就比主内容大,反之主内容速度大,实际上这样也很合理!

到此这篇关于Android自定义view实现侧滑栏详解的文章就介绍到这了,更多相关Android侧滑栏内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android自定义View实现跟随手指移动

    对View的移动,实现的方法有好几种,原理是通过改变View的位置来移动View,下面来实现这样的效果 动画的方法 通过改变View的tranlationX和tranlationY的值来实现移动,首先来写一个自定义View类,重写onTouchEvent方法,实现构造方法 public class MyView extends View {     public MyView(Context context) {         super(context);     }     public

  • Android自定义view实现滚动选择控件详解

    目录 前言 需求 编写代码 主要问题 前言 上篇文章通过一个有header和footer的滚动控件(Viewgroup)学了下MeasureSpec.onMeasure以及onLayout,接下来就用一个滚动选择的控件(View)来学一下onDraw的使用,并且了解下在XML自定义控件参数. 需求 这里就是一个滚动选择文字的控件,还是挺常见的,之前用别人的,现在选择手撕一个,核心思想如下: 1.有三层不同大小及透明度的选项,选中项放在中间 2.接受一个列表的数据,静态时显示三个值,滚动时显示四个

  • Android小工具自定义view课表

    本文实例为大家分享了Android自定义view课表的具体代码,供大家参考,具体内容如下 这里是模拟课表,数据写死了的,不过也可以通过抓包获取教务系统课表 1.xml文件 <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="

  • Android自定义View实现水波纹扩散效果

    目录 1.创建RippleView.class, 继承与View 1.1特殊属性解释 1.2新建attrs.xml文件(res/values) 1.3初始化画笔 2.开始绘制onDraw() 效果:水波纹扩散 场景:雷达.按钮点击效果.搜索等 实现:先上效果图,之前记得支付宝有一个咻一咻,当时就是水波纹效果,实现起来一共两步,第一画内圆,第二画多个外圆,不同时创建有间隔创建然后缓慢增大外圆半径,到达最远距离时移除掉,扩散时把透明度从255-1不断赋值即可.复杂在第二步,开工. 开工 1.创建Ri

  • Android自定义View实现天气预报折线图

    本文实例为大家分享了Android自定义View画天气预报折线图的具体代码,供大家参考,具体内容如下 效果图如下: 刚开始尝试用第三方画曲线的框架来画效果图,后来发现曲线间的阴影当有负数的度数的时候画不出来,而且不需要点击放大.点点可点的效果,用框架显得很臃肿,所以最后用自定义View来画的折线图.自定义画折线图的大致思路:这个图是有多个四边形组成的(4个点连接起来就是一个四边形),两边延伸:添加四个多余的点,将左右的边距设置成负数即可. 代码如下: public class WeatherCh

  • Android自定义View绘制贝塞尔曲线实现流程

    目录 前言 二阶贝塞尔曲线 三阶贝塞尔曲线 前言 对于Android开发,实现贝塞尔曲线还是比较方便的,有对应的API供你调用.由于一阶贝塞尔曲线就是一条直线,实际没啥多大用处,因此,下面主要讲解二阶和三阶. 二阶贝塞尔曲线 在Android中,使用quadTo来实现二阶贝塞尔 path.reset() path.moveTo(startX, startY) path.quadTo(currentX, currentY, endX, endY) canvas.drawPath(path, cur

  • Android自定义View实现时钟功能

    最近在练习自定义view, 想起之前面试的时候笔试有道题是写出自定义一个时钟的关键代码. 今天就来实现一下. 步骤依然是先分析, 再上代码. 实现效果 View分析 时钟主要分为五个部分: 1.中心点: 圆心位置2.圆盘: 以中心点为圆心,drawCircle画个圆3.刻度: paint有个aip, setPathEffect可以根据path画特效, 那么刻度就可以根据圆的path画一个矩形path的特效, 并且这个api只会画特效, 不会画出圆. /** * shape: 特效的path, 这

  • Android自定义view实现侧滑栏详解

    目录 前言 需求 效果图 编写代码 主要问题 前言 上一篇文章学了下自定义View的onDraw函数及自定义属性,做出来的滚动选择控件还算不错,就是逻辑复杂了一些.这篇文章打算利用自定义view的知识,直接手撕一个安卓侧滑栏,涉及到自定义LayoutParams.带padding和margin的measure和layout.利用requestLayout实现动画效果等,有一定难度,但能重新学到很多知识! 需求 这里类似旧版QQ(我特别喜欢之前的侧滑栏),有两层页面,滑动不是最左侧才触发的,而是从

  • Android自定义View实现体重表盘详解流程

    目录 效果视频 分析 起始角度 圆弧 指针 代码 初始化属性 画布 绘制内圆弧 绘制外圆弧 绘制中间指针 绘制中间文字 绘制左右两边文字 动画 全部代码 下载链接 效果视频 分析 起始角度 如下图所示,起点角度为150,终点角度为240 圆弧 白色圆弧为整个圆弧范围,蓝色圆弧为根据数据变动而覆盖白色圆弧,蓝色圆弧比白色圆弧大一点,突出显示 InnerArcPaint.setStrokeWidth( Width * (float)0.1 ); OuterArcPaint.setStrokeWidt

  • Android自定义View app更新动画详解

    为了做一个有温度的IT男,我决定在以后的文章中给大家分享一些看到的,听到的一些东西,如果你不喜欢请留言让我知道,如果你喜欢请点个赞.你也可留言写下自己想分享的东西,温暖你我他.这次分享的是一首歌,毛不易的<消愁>,分享这首歌主要是这首歌的歌词,借用薛之谦的评价:"我是研究歌词的人,我研究了十几年,但是你写到我想给你跪!",下面贴部分歌词供大家欣赏 一杯敬朝阳,一杯敬月光 唤醒我的向往,温柔了寒窗 于是可以不回头的逆风飞翔 不怕心头有雨,眼底有霜 一杯敬故乡,一杯敬远方 守着

  • Android 自定义返回按钮的实例详解

    Android 自定义返回按钮的实例详解 程序中我们有时候想让放回按钮按照自己的需求调整页面而不是单纯的按照系统返回上一级,这个问题很简单,重写 onKeyDown 方法即可. 下面方法,包含了 webview 中的返回上一页和普通 activity 的单击设置和双击退出程序. @Override public boolean onKeyDown(int keyCode, KeyEvent event) { //如果我们用的是webview页面,想返回网页的上一页设置这里就可以了 if (key

  • Android 使用View Binding的方法详解

    前言 Android Studio稳定版发布了3.6版本,带来了一些新变化:首先外观,启动页变了,logo改了,更显现代化:增加Multi Preview功能,能同时预览多个尺寸屏幕的显示效果:模拟器支持多屏:也终于支持全新的视图绑定组件View Binding:等. 之前我们与视图交互的方式有findViewById.kotlin中引入Android Kotlin Extensions后直接通过id进行访问.前者模板化严重,重复代码多:后者最为方便.现在有了新的选择–View Binding,

  • Android自定义日历控件实例详解

    为什么要自定义控件 有时,原生控件不能满足我们对于外观和功能的需求,这时候可以自定义控件来定制外观或功能:有时,原生控件可以通过复杂的编码实现想要的功能,这时候可以自定义控件来提高代码的可复用性. 如何自定义控件 下面我通过我在github上开源的Android-CalendarView项目为例,来介绍一下自定义控件的方法.该项目中自定义的控件类名是CalendarView.这个自定义控件覆盖了一些自定义控件时常需要重写的一些方法. 构造函数 为了支持本控件既能使用xml布局文件声明,也可在ja

  • Android自定义View中attrs.xml的实例详解

    Android自定义View中attrs.xml的实例详解 我们在自定义View的时候通常需要先完成attrs.xml文件 在values中定义一个attrs.xml 然后添加相关属性 这一篇先详细介绍一下attrs.xml的属性. <?xml version="1.0" encoding="utf-8"?> <resources> //自定义属性名,定义公共属性 <attr name="titleText" for

  • Android自定义View的实现方法实例详解

    一.自绘控件 下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次.新建一个CounterView继承自View,代码如下所示: 可以看到,首先我们在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用.而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法.通过 Andr

  • Android自定义View详解

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章.先总结下自定义View的步骤: 1.自定义View的属性 2.在View的构造方法中获得我们自定义的属性 [ 3.重写onMesure ] 4.重写onDraw 我把3用[]标出了,所以说3不一

随机推荐