仿QQ抽屉导航栏

一、需求

对QQ抽屉导航栏效果进行复现。

二、实现方式

  • DrawerLayout
  • ViewDragHelper
  • 自定义ViewGroup

三、调研

  • DrawerLayout

    DrawerLayout包含两个子布局,内容布局以及菜单栏布局。实现 addDrawerListener接口以及在回调方法里进行逻辑处理。

  • ViewDragHelper
    View拖拽帮助类,创建实例并重写callback接口回调方法处理逻辑。

  • 自定义ViewGroup
    重写事件有关方法,针对事件进行布局处理。

  • 确定实现方式

    • DrawerLayout实现简单方便,但是由于封装的原因,很难了解到原理。
    • ViewDragHelper的tryCaptureView()方法只能捕获需要移动的View,而我们需要对菜单栏布局进行位移,需要在onViewCaptured()方法中调用captureChildView()传入需要进行操作的布局。
public void captureChildView(@NonNull View childView, int activePointerId) {
        if (childView.getParent() != mParentView) {
            throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
                    + "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
        }
        mCapturedView = childView;
        mActivePointerId = activePointerId;
        mCallback.onViewCaptured(childView, activePointerId);
        setDragState(STATE_DRAGGING);
    }

在captureChildView()中mCapturedView表示捕获的View,我们需要在这里把菜单栏布局View传 入,但是此方法还会调用onViewCaptured()方法,形成死循环。可以在onViewCaptured()方法中加一个变量判断是否是第一次调用避免死循环。但是在ViewDragHelper事件方法回调中还是会调用onViewCaptured()方法将包含事件的View传入,所以即使进行了View调换但是最后还是会捕获包含事件的View,可实现较难。

  • 自定义ViewGroup 只需将dispatchTouchEvent()方法进行重写,针对事件进行逻辑处理即可。

四、分析

1.当在主页面进行滑动时,菜单栏跟着滑动,即菜单栏滑动距离依赖于滑动距离。
2.当手指松开时,滑动距离大于等于屏幕宽度1/2,菜单栏布局自动铺满屏幕,否则显示内容布局。
3.当菜单栏滑动时,主页面做缩放及透明度动画。
当事件结束时,菜单栏始终拥有两种状态:OUT(屏幕外)和FULL(填充屏幕)
右滑时,事件结束时事件位置始终大于等于事件开始时位置(当等于时,菜单栏已经全部在屏幕外,所以小于时看不到效果直接不做判断)。左滑一样。

五、代码实现

因为是自定义ViewGroup,所以实现FrameLayout布局。

class FirstFrameLayout(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(context, attributeSet){}

在onFinishInflate()中获取子布局对象

override fun onFinishInflate() {
        super.onFinishInflate()
        menuLayout = getChildAt(1) as RelativeLayout
        contentLayout = getChildAt(0) as FrameLayout
    }

重写onLayout()布局,确定子布局位置

override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) {
        secondLayout.layout(-right, top, left, bottom)
        firstLayout.layout(left,top,right,bottom)
    }

设置菜单栏状态

enum class State {
    OUT,
    FULL,
}

重写dispatchTouchEvent()方法进行事件拦截

    var first = 0f
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                first = ev.x //记录第一次DOWN事件,X轴坐标位置
            }
            MotionEvent.ACTION_MOVE -> {
                val dif = ev.x - first //每次MOVE事件移动距离
                if (flag == State.OUT) { //菜单栏从左往右滑动
                    if (dif > 0) { 
                        menuLayout.left = -right
                        menuLayout.right = 0
                        menuLayout.offsetLeftAndRight(dif.toInt()) //菜单栏布局进行位移
                        scaleSecond(dif) //内容布局动画
                    }
                } else if (flag == State.FULL) { //菜单栏从右往左滑动
                    if (dif < 0) {
                        menuLayout.left = 0
                        menuLayout.right = right
                        menuLayout.offsetLeftAndRight(dif.toInt())
                        scaleSecond1(dif)
                    }
                }
            }
            MotionEvent.ACTION_UP -> {
                val dif = ev.x - first//UP事件移动距离
                if (flag == State.OUT) { //从外往里滑动停止
                    flag = if (dif >= right / 2) { //距离大于1/2 菜单栏全屏 状态置为FULL
                        menuLayout.layout(left, top, right, bottom)
                        State.FULL
                    } else {//距离小于1/2 菜单栏左挨屏幕 状态不变
                        menuLayout.layout(-right, top, left, bottom)
                        release() 
                        State.OUT
                    }
                } else if (flag == State.FULL) {
                    flag = if (dif < 0 && abs(dif) >= right / 2) {
                        menuLayout.layout(-right, top, left, bottom)
                        State.OUT
                    } else {
                        menuLayout.layout(left, top, right, bottom)
                        release1()
                        State.FULL
                    }
                }
            }
        }

        return true //必须返回true,否则只会拦截DOWN事件后,对后续其他UP、MOVE事件不做处理。
    }
    //对contentLayout动画清空
    private fun release() {
        contentLayout.scaleX = 1f
        contentLayout.scaleX = 1f
        contentLayout.alpha = 1f
    }
    //contentLayout缩放及透明度动画
    private fun scaleSecond(dif: Float) {
        val scale = 1 - ((dif / right) * 0.05f)
        val alpha = 1 - ((dif / right) * 0.6f) 
        contentLayout.scaleX = scale
        contentLayout.scaleY = scale
        contentLayout.alpha = alpha
    }

六、小结

实现前应该针对抽屉效果进行分类,针对不同状态做不同处理,可提高效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值