仿QQ侧滑面板(一)

本文详细介绍了如何在Android中实现仿QQ的侧滑面板。从自定义DragLayout开始,通过ViewDragHelper实现拖拽功能,并在onFinishInflate()中查找和设置子控件,接着限制主面板的拖动范围,确保其在一定区域内移动。文章涵盖了从初始化到触摸事件处理的全过程。
摘要由CSDN通过智能技术生成

1.1 实现最简单的拖拽

1.2 限定拖拽范围

效果:

这里写图片描述

1.1 实现最简单的拖拽

1.1.1 自定义一个 DragLayout

在创建 DragLayout 时,继承 FrameLayout,这里需要注意两个问题
1、 为什么不继承 ViewGroup,因为继承 ViewGroup 需要重写 onMeasure()和实现 onLayout()方法, 自己实现子 view 的测量和摆放, 在这里我们不需要自己去做测量和摆放, 而 FrameLayout 已经对这两个方法进行了具体实现,所以继承 FrameLayout 更加简单省事
2、 为什么不继承 RelativeLayout,因为这里我们只需要层级关系, 不需要相对关系, 继承 RelativeLayout界面效果是一样的,但 RelativeLayout 对 FrameLayout 多了相对关系的计算,效率会低一些,所以选择继承 FrameLayout

package com.fighting.qqview;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;

/**
 * 描述:
 * 作者 mjd
 * 日期:2016/1/26 19:10
 */
public class DragLayout extends FrameLayout {
    public DragLayout(Context context) {
        super(context);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

1.1.2 串联构造方法

DragLayout 实例化时需要做一些初始化操作, 如果我们定义一个 init()方法, 则我们需要在三个构造方法中都调用 init()方法,这样非常麻烦,我们可以通过串连三个构造方法的方式实现只调用一次 init()方法这样无论是代码创建还是布局在 xml 中都能调用到我们的初始化代码

package com.fighting.qqview;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;

/**
 * 描述:
 * 作者 mjd
 * 日期:2016/1/26 19:10
 */
public class DragLayout extends FrameLayout {
    public DragLayout(Context context) {
        //代码创建时调用
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        //布局在 xml 中,实例化时调用
        this(context, attrs, 0);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

1.1.3 ViewDragHelper 简介

我们要实现拖拽的效果,则需要自己去解析 Touch 事件的 ACTION_DOWN,ACTION_MOVEACTION_UP,相当的麻烦。所以 Google 在 2013 年的 IO 大会上发布了 ViewDragHelper 这个类,用来解决滑动拖拽问题,用这个类可以非常简单的实现 view 的拖拽

1.1.4 创建 ViewDragHelper

我们只需要在第三个构造方法中实现 ViewDragHelper 的实例即可

public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    // 在这里初始化
    // 1.forParent 父类容器 2.sensitivity 敏感度,越大越敏感, 1.0f 是默认值 3.Callback 回调事件
    //1. 通静态方法创建拖拽辅助类
    viewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
}

ViewDragHelper 三个参数的创建的方法源码中的 mTouchSlop 表示触摸的最小敏感范围,越小越敏感即在界面拖动的瞬间变化量大于 mTouchSlop 时才可以成功触发拖拽事件

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
    final ViewDragHelper helper = create(forParent, cb);
    helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
    return helper;
}

1.1.5 触摸事件转交

ViewDragHelper 创建成功了,但它和 DragLayout 并没有任何关系,我们需要让它们建立关系


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    //return super.onInterceptTouchEvent(ev);
    //由 ViewDragHelper 判断是否拦截
    return viewDragHelper.shouldInterceptTouchEvent(ev);
}

重写 onInterceptTouchEvent 方法, 将触摸事件交给 ViewDragHelper 判断是否拦截, 这样它们就建立了关系,事件拦截后,还需要对拦截到的事件进行处理,注意返回值必须是 true


@Override
public boolean onTouchEvent(MotionEvent event) {
    try {
        viewDragHelper.processTouchEvent(event);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}

1.1.6 处理回调事件

ViewDragHelper 在处理触摸事件时会通过传入的 callback 给我们反馈, 通过对回调方法的处理即可实现简单的拖拽


//3. 处理回调事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

    //返回值决定了 child 是否可以被拖拽
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        //child 被用户拖拽的孩子 pointerId 多点触摸的手指 id
        return true;
    }

    //修正子 view 水平方向上的位置,此时还没有真正的移动,返回值决定 view 将移动到的位置
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //left 建议移动到的位置
        return left;
    }

};

1.1.7 DragLayout 布局到 xml 中

<?xml version="1.0" encoding="utf-8"?>
<com.fighting.qqview.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#66ff0000">

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00ff00">

    </LinearLayout>

</com.fighting.qqview.DragLayout>

给左面板和主面板设置不同的背景颜色便于拖拽时观察效果,运行工程,即可实现简单的拖拽

1.2 限定拖拽范围

现在左面板和主面板可以任意拖动,本节要实现左面板不动,拖动时,主面板在一定范围内拖动

1.2.1 OnFinishInflate()介绍

onFinishInflate()在控件 inflate 完成时会被调用,可以在这个方法中查找子控件
1.可以通过 findViewById()的方式查找子控件
2.可以通过子 view 索引的方式查找子控件
这里采用第二种方式


@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    //增强代码的健壮性
    if (getChildCount() < 2) {
        //必须有两个子 view
        throw new IllegalStateException("Your viewGroup must have two children.");
    }
    if (!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)) {
        //子 view 必须是 viewGroup 的子类
        throw new IllegalStateException("The child must an instance of viewGroup.");
    }
    mLeftContent = getChildAt(0);
    mMainContent = getChildAt(1);
}

1.2.2 获取控件宽高

在 onMeasure()方法中可以获取到控件的宽高, 也可以在 onSizeChanged()方法中去获取宽高, onMeasure方法调用后会检测宽高值有没有变化,有变化才调用 onSizeChanged()方法,无变化则不调用,所以onSizeChanged()调用的次数比 onMeasure()少,在这里我们在 onSizeChanged()方法中去获取宽高,同时计算出拖拽范围为宽度的 60%


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    viewWidth = getMeasuredWidth();
    viewHeight = getMeasuredHeight();
    range = (int) (viewWidth * 0.6f);
    Log.e(TAG, "viewWidth = " + viewWidth + ", viewHeight = " + viewHeight + ", range = " + range);
}

1.2.3 限定主面板的拖动范围

对 callback 中的其它几个方法进行重写

//3. 处理回调事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

    //返回值决定了 child 是否可以被拖拽
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        //child 被用户拖拽的孩子 pointerId 多点触摸的手指 id
        return true;
    }

    @Override
    public int getViewHorizontalDragRange(View child) {
        return super.getViewHorizontalDragRange(child);
    }

    //修正子 view 水平方向上的位置,此时还没有真正的移动,返回值决定 view 将移动到的位置
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //left 建议移动到的位置
        return left;
    }

    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
    }

    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
    }
};

回调方法中的 getViewHorizontalDragRange(View child)方法返回拖拽的范围,但不会真正限定这个范围,只要返回一个大于零的值即可。
在 ViewDragHelper 源码中, computeSettleDuration()会调用这个返回值来计算动画执行的时长,checkTouchSlop()方法会调用这个返回值检查左面板,主面板是否可以被滑动,所以需要返回一个大于 0的值才能实现拖动。
如果返回值为 0, 左面板, 主面板中不能有子 view 或子 view 没有对 touch 事件做处理, 最后触摸还是会交给 ViewDragHelper 处理,所以也能实现拖动


//返回拖拽的范围,返回一个大于零的值,计算动画执行的时长,水平方向是否可以被滑开
@Override
public int getViewHorizontalDragRange(View child) {
    //computeSettleDuration 计算动画执行的时长
    //checkTouchSlop 检查是否可以被滑动(没有孩子处理触摸事件, 最后返回给 DragLayout 处理)
    return range;
}

限定主面板的拖拽范围,当建议的值 left 小于 0 时,让 left 等于 0,大于 range时等于 range,然后再将 left 返回


//修正子 view 水平方向上的位置,此时还没有真正的移动,返回值决定 view 将移动到的位置
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    //child 被用户拖拽的孩子 left 建议移动到的位置 dx 新的位置与旧的位置的差值
    int oldLeft = mainContent.getLeft();
    Log.e(TAG, "clamp: left:" + left + " oldLeft:" + oldLeft + " dx:" + dx);
    if (child == mainContent) {
        left = fixLeft(left);
    }
    return left;
}

/**
 * 修正左边的位置,限定拖拽范围在 0 到 range 间变化
 */
private int fixLeft(int left) {
    if (left < 0) {
        left = 0;
    } else if (left > range) {
        left = range;
    }
    return left;
}

当控件位置变化时会调用 onViewPositionChanged()方法,可以在此方法中做伴随动画,状态更新,事件回调, left 表示最新的水平位置, dx 表示刚刚发生的水平变化量。

此时左面板还可以任意拖动, 为了实现拖动左面板时界面表现为拖动主面板, 可以对 changedView 进行判断,如果 changedView 是左面板,则通过 layout()把左面板放回到原来的位置,然后把变化量 dx 累加给主面板,再通过 layout()方法来移动主面板


// 当控件位置变化时调用,可以做伴随动画,状态更新,事件回调
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    super.onViewPositionChanged(changedView, left, top, dx, dy);
    // left 最新的水平位置 dx 刚刚发生的水平变化量
    Log.e(TAG, "onViewPositionChanged: left:" + left + " dx:" + dx);
    if (changedView == leftContent) {
        // 如果滑动的是左面板
        // 1. 放回到原来的位置
        leftContent.layout(0, 0, viewWidth, viewHeight);
        // 2. 把变化量传递给主面板, 主面板旧的值+变化量
        int newLeft = mainContent.getLeft() + dx;
        // 需要修正左边值
        newLeft = fixLeft(newLeft);
        mainContent.layout(newLeft, 0, newLeft + viewWidth, viewHeight);
    }
    // offsetLeftAndRight 在低版本中没有重绘界面,手动调用重绘
    invalidate();
}

注意:由于 onViewPositionChanged()方法调用前调用了 offsetLeftAndRight()方法,此方法在低版本中没有重绘界面, 并且在高版本中也有一个 bug, 最后一帧没有被绘制, 所以需要手动调用一次 invalidate(),否则在低版本中无法实现拖拽效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值