Android自定义View,ViewGroup(一)的一些原理与细节,RecyclerView版侧滑删除

google工程师提供了很多原生控件,原生控件都是继承View或ViewGroup,但是未必能满足工作中业务的需求,有些同学认为,现在技术博客上各种需求的开源控件都有,没有必要去自己定义,但是即使再好的开源项目也有需求改变的情况,如果不掌握这项技术,只要有小小的需求改变,都将一头雾水,所以要进阶android技术必须要掌握原生控件的修改,控件View,ViewGroup的自定义!!!

更多文章请关注:http://blog.csdn.net/u012216274


一:View的生命周期

和activity,fragment一样,view也有自己的生命周期,下面是主要常用的生命周期方法

1:onFinishInflate() 当View中所有的子控件均被映射成xml

2:onAttachedToWindow() 当view被附着到一个窗口时
每一个View都需要依赖于窗口来显示,View和窗口的关系则是放在View.AttachInfo中,与onDetachedFromWindow结合可以做些状态初始化与销毁 比如最常用的ViewPager轮播效果在嵌套到ListView或RecyclerView中滑动超出所在位置被缓存起来时也会调用此方法,此时应该在离开窗体时停止轮播,再次附着到窗体的时候可以再开启轮播 ,你的轮播控件有处理过吗? 还有类似内部添加动画时可以在附着窗体时初始动画,在离开窗体时当停止动画,比如二维码扫描界面在扫描的时候,中间线条动画就是不断的调用postInvalideDelay()延时来产生动画效果,而在View中相关post的相关方法都是View.AttachInfo中维护的Handler来执行的,在离开窗体的时候一定要停止Handler的延时操作,你在定义动画的时候有考虑过这些吗?

3:onWindowVisibilityChanged( int ) 当窗口中包含的可见的view发生变化

4:onMeasure(int widthMeasureSpec, int heightMeasureSpec) 测量该控件的大小 ,如果是ViewGroup还需测量子控件大小,measureChildren或调用子控件的measure来触发子控件元素的onMeasure方法

此方法可能会被调用多次 (父视图可能在它的子视图上调用一次以上的measure(int,int)方法。例如,父视图可以使用unspecified dimensions来将它的每个子视图都测量一次来算出它们到底需要多大尺寸,如果所有这些子视图没被限制的尺寸的和太大或太小,那么它会用精确数值再次调用measure()(也就是说,如果子视图不满意它们获得的区域大小,那么父视图将会干涉并设置第二次测量规则),另外父控件添加移除子元素的时候,或在有元素requestLayout,setVisibility都有可能会影响此方法的调用,所以只方法中尽量只做些测量相关的逻辑。

另外在ViewGroup中需要measureChildren或调用子元素的measure方法来测量子元素,否则在onLayout布局子元素时将获取不到子元素 的测量大小 getMeasuredHeight ,getMeasureWidth都将为0,最后setMeasuredDimension来确定view的大小

(此处链接1)onMeasure参数与MeasureSpec详解

5:onLayout(boolean changed, int l, int t, int r, int b) 当View分配所有的子元素的大小和位置时

在onLayout方法被调用之前getWidth(), getHeight()是获取不到控件的大小,这也是为什么在activity的onCreate方法中获取不到控件的宽高的原因(onCreate方法在setContentView后,view只是执行到onFinishInflate(),还没有执行到onAttachedToWindow,onMeasure,onLayout) 当然onCreate可以通过getViewTreeObserver()添加监听回调来获取
方法中的参数changed:view有新的尺寸或位置,其他四个参数则为该控件相对父控件中的left,top,right,bottom位置

6:onSizeChanged( int , int , int , int ) 当view的大小发生变化时

7:onDraw(Canvas) view渲染内容,就是canvas, Paint, Path的一些api使用(只有可见的 View 才在 window 中绘制)

(此处链接2)canvas详解

8:dispatchDraw 在onDraw之后会调用此方法,分发子元素绘制,主要是针对ViewGroup, View的Animation原理就是上层的ViewGroup不断的调用此方法来绘制动画效果,因此在定义ViewGroup的时候要小心尽量不要在此方法中做些复杂的逻辑,子元素的一个动画就会引起此方法不断的被调用

ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而不执行draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法


9:dispatchTouchEvent 分发触屏事件


10:onTouchEvent(MotionEvent) 触屏事件

[(此处链接3)事件传递机制详解,这块特别重要必须掌握,在后面介绍定义控件滑动时也会详细介绍]
(http://blog.csdn.net/xyz_lmn/article/details/12517911)

补充一点

很多同学碰到onTouchEvent重写后跟onClick有冲突的问题,重写了onTouch会导致onClick无效之类的,网上还有很多种杂七杂八解决的办法,我个人不赞同,个人认为没有冲突,那只是碰到有冲突的人写法不对或没有了解View.onTouchEvent原码,后面讲侧滑删除示例代码的时候会介绍

**补充 从Android5.1 Lollipop之后google在事件传递之上又增加了一种新的滑动事件嵌套机制NestedScrolling,而且支持包中都有兼容类,下篇再介绍

11:onFocusChanged( boolean , int , Rect) 当View获取或失去焦点

12:onWindowFocusChanged( boolean ) 当窗口包含的view获取或失去焦点

13:onVisibilityChanged(View changedView, int visibility) View的三种visibility状态改变

14:Parcelable onSaveInstanceState() 状态保存


15:onRestoreInstanceState(Parcelable state) 状态恢复


在定义控件的时候这两个方法经常被同学们忽视,因为毕竟要在按下Home键或内存不足,横竖切屏的时候才能用到,跟activity有点类似,google提供的原生控件中几乎都有重写这两个方法,比如定义RadioButton时在内存不足又重新恢复界面的时候如果不考虑的话,RadioButton哪个被选中还能恢复到意外回收时的状态吗。还有大家最常用的FrameLayout+fragment的时候,你是否有像google工程师提供的ViewPager+FragmentPagerAdapter那样正确的保存Fragment状态呢?

此处链接4 Fragment与FrameLayout的状态保存

你的自定义控件有考虑这些吗???

16:onDetachedFromWindow() 当view离开附着的窗体时触发,在上面的onAttachedToWindow中有介绍

—————-ViewGroup生命周期新增了一些重要生命周期方法——————

17:onLayout( boolean , int , int , int , int )虚拟化了父类的此方法,分配所有的子元素的位置,上面有讲了一部分,此方法也可能会被多次调用,原理和onMeasure类似, google提供的各中布局类主要就是在此方法中对子元素做不同的布局排版


18:onInterceptTouchEvent(MotionEvent) 事件拦截,可以从上面的链接3中了解

19:onViewAdded(View) 子控件被添加进ViewGroup
20:onViewRemove(View) 子控件被从ViewGroup移除

这里只介绍常用的常用的生命周期方法,要是没有涉及到的
这里写图片描述

二:生命周期方法示例

示例

1:onMeasure方法

a:定义一个宽度填充屏幕,高度按宽的一定比例固定大小的RelativeLayout

    float scale = 1.0f;

    第一种错误写法
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = (int) (width * scale);
        setMeasuredDimension(width, height);
    }
    第二种写法
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = (int) (width * scale);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

严格说第一种写法就是错误的,虽然RelativeLayout大小是达到了需求,但是既然是RelativeLayout如果在里面添加别的子控件,那子控件就无法测量了,而super.onMeasure()中会去根据当然测量的大小再去测量子控件的大小

这里写图片描述

b: 很多热门搜索中能见到这种需求, 类似GridLayout,但如果里面的item文字短的可能一行排列四个,而item文字多的时候可能排列两个或一个,这种需求google提供的常用布局是不好实现(当然GridLayout可以实现的),这个时候就需要定义一个布局类喽

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //这里简化需求,每个item高度都一样textView只是填充内容,直接测量所有child,并且此控件宽度为填充屏幕方式
        //verticalSpace, horizontalSpace为列之间间隙与行之间间隙
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        //这里要考虑控件是否有设置Padding,paddingLeft..等padding属性
        //内容区域为控件宽度减去左右两边的padding值
        int contentWidth = width - getPaddingLeft() - getPaddingRight();
        //高度也一样要考虑padding属性,即使没有子控件,也要有top,bottom的padding值
        int height = getPaddingBottom() + getPaddingTop();
        if (getChildCount() > 0) {
            View firstChild = getChildAt(0);
            //换行之前高度保持不变,只取第一个item的高度
            height += firstChild.getMeasuredHeight();
            //当前行中所有子控件的宽度总和
            int currentWidth = firstChild.getMeasuredWidth() + getPaddingLeft();
            for (int i = 1; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                // 如果宽度总和超过控件大小,换行高度则为已经测量的高度再加上一个item的高度和行之间的间隙
                if (currentWidth + childWidth + verticalSpace > contentWidth) {
                    currentWidth = childWidth + getPaddingLeft();
                    height += child.getMeasuredHeight() + horizontalSpace;
                } else {
                    //如果没有超出该行直接放在右边继续测量
                    currentWidth += childWidth + verticalSpace;
                }
            }
        }
        setMeasuredDimension(width, height);
    }

这里写图片描述

任何控件被添加到ViewGroup中(不论是通过layout布局还是代码addView添加)都能通过getLayoutParams获取到不为空的LayoutParams,可以从ViewGroup源码的addView中看到,有需求的话也可以在onMeasure方法中强制改变LayoutParmas的属性值从而强制改变被添加到ViewGroup中时的测量大小

onLayout方法在后面讲述onLayout的时候再贴出

2:onLayout方法(onMeasure就像是铁路工程测量,onLayout就像是正式铁路施工,只有先前测量好了,后面施工才不会有问题)

onLayout相对简单些,这里直接贴出上个热门搜索的代码

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //可布局的大小为控件大小减右边的填充大小paddingRight,逻辑跟onMeasure差不多
        int width = getMeasuredWidth();
        int left = getPaddingLeft();
        int top = getPaddingTop();
        if (getChildCount() > 0) {
            View firstChild = getChildAt(0);
            int childHeight = firstChild.getMeasuredHeight();
            //这里一定要考虑padding值
            firstChild.layout(left, top, firstChild.getMeasuredWidth() + left, childHeight + top);
            left += firstChild.getMeasuredWidth() + verticalSpace;
            for (int i = 1; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                if (left + childWidth + getPaddingRight() > width) {// 超过控件摆放位置大小,换行
                    left = getPaddingLeft() + childWidth + verticalSpace;
                    top += childHeight + horizontalSpace;
                    child.layout(getPaddingLeft(), top, childWidth + getPaddingLeft(), top + childHeight);
                } else {
                    child.layout(left, top, left + childWidth, top + childHeight);
                    left += childWidth + verticalSpace;
                }
            }
        }
    }

3:onDraw方法,这里只简单介绍二维码扫描边框与背景的绘制

[(此处链接5)仿微信拍照的中间进度控件,可以单击,长按,长按进度显示,对焦动画等控件的定义]
(http://blog.csdn.net/u012216274/article/details/68059637)

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        int width = getWidth();
        int height = getHeight();

        //设置画阴影的矩阵
        RectF rectf = new RectF();
        float shadeWidth = width * verticalShadeScale;;
        float shadeHeight = shadeWidth / verticalScale;
        float halfWidthPadding = (width - shadeWidth) / 2;
        float halfHeightPadding = (height - shadeHeight) / 2;
        rectf.set(halfWidthPadding, halfHeightPadding, halfWidthPadding + shadeWidth, halfHeightPadding + shadeHeight);

        canvas.clipRect(rectf, Region.Op.DIFFERENCE);//剪切框除交集外
        canvas.drawColor(shadeColor);//画剪切框外的阴影
        canvas.restore();

        drawCorner(canvas, rectf);
    }

    /**
     * 画边角的八个小矩形
     */
    private void drawCorner(Canvas canvas, RectF frame) {
        canvas.drawRect(frame.left, frame.top, frame.left + strokeWidth, frame.top + strokeLength, paint);
        canvas.drawRect(frame.left, frame.top, frame.left + strokeLength, frame.top + strokeWidth, paint);
        canvas.drawRect(frame.right - strokeLength, frame.top, frame.right, frame.top + strokeWidth, paint);
        canvas.drawRect(frame.right - strokeWidth, frame.top, frame.right, frame.top + strokeLength, paint);
        canvas.drawRect(frame.left, frame.bottom - strokeLength, frame.left + strokeWidth, frame.bottom, paint);
        canvas.drawRect(frame.left, frame.bottom - strokeWidth, frame.left + strokeLength, frame.bottom, paint);
        canvas.drawRect(frame.right - strokeLength, frame.bottom - strokeWidth, frame.right, frame.bottom, paint);
        canvas.drawRect(frame.right - strokeWidth, frame.bottom - strokeLength, frame.right, frame.bottom, paint);
    }

**

4:onSaveInstanceState(),onRestoreInstanceState(Parcelable state)

**
这里以自定义ProgressBar的状态保存与恢复为示例

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        ProgressSaveState ps = new ProgressSaveState(superState);
        ps.progress = this.progress;
        ps.currentProgressColor = this.currentProgressColor;
        ps.state = this.state;
        return ps;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof ProgressSaveState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        ProgressSaveState ps = (ProgressSaveState)state;
        super.onRestoreInstanceState(ps.getSuperState());
        this.progress = ps.progress;
        this.currentProgressColor = ps.currentProgressColor;
        this.state = ps.state;
        if(this.state == STATE_PAUSE) {
            progressPaint.setColor(pauseColor);
            pauseProgress(this.progress);
        } else {
            progressPaint.setColor(progressColor);
            setProgress(this.progress);
        }
    }

    /** 保存状态  */
    static class ProgressSaveState extends BaseSavedState {
        int progress;   
        int currentProgressColor;
        int state;
        public ProgressSaveState(Parcelable state) {
            super(state);
        }

        private ProgressSaveState(Parcel in) {
            super(in);
            this.progress = in.readInt();
            this.currentProgressColor = in.readInt();
            this.state = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(this.progress);
            dest.writeInt(this.currentProgressColor);
            dest.writeInt(this.state);
        }

        public static final Parcelable.Creator<ProgressSaveState> CREATOR
            = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<ProgressSaveState>() {
                @Override
                public ProgressSaveState createFromParcel(Parcel in, ClassLoader loader) {
                    return new ProgressSaveState(in);
                }
                @Override
                public ProgressSaveState[] newArray(int size) {
                    return new ProgressSaveState[size];
                }
            });         
    }

补充一点:此处链接6 控件自定义属性

三:事件传递

事件传递这块应该说是最复杂也最让人头痛的地方啦,因为只要在滑动控件里就能见到这些事件方法的重写而且逻辑比较复杂,而滑动控件是无处不在,在4.0之前,google提供的滑动控件像ViewPager,ListView,ScrollView如果有嵌套的话还得自己去处理滑动冲突了,不过还好,4.0以后google工程师都有针对这些滑动冲突有修改,但是原生的滑动控件还是不能满足所有的需求!!!

举个例子下拉刷新控件PullToRefreshLayout相信大家都熟悉,想问一下你用的这个开源控件(或别的开源下拉刷新控件)有支持多点触摸吗(多个手指来操作下拉刷新,看看会不会有问题就知道了),如果没有你能改造吗,或是加个类似京东下拉刷新的那种效果,或是兼容android design风格的CoordinatorLayout动画,再或者在下拉刷新控件嵌套多层滑动控件,要兼容4.0以下版本的滑动冲突,能快速解决吗?

这里写图片描述

熟悉了事件的传递,再复杂的嵌套当然都能解决喽,还是先从简单的开始吧,这里举例写个简单一点的RecyclerView版的侧滑删除控件(15年初的时候临时写的,那时候找不到开源RecyclerView版的侧滑删除只能自己写了一套,可以随意添加menu,拿来介绍如何定义滑动控件,现在有很多开源的大神写好的,有错误的地方多指教)

这里写图片描述

这里只用到两个类SwipeMenuLayout与SwipeMenuRecyclerView,先看SwipeMenuLayout

package you.xiaochen.swipe;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.OverScroller;

/**
 * Created by you on 2015/4/1.
 */

public class SwipeMenuLayout extends ViewGroup {
    /**
     * 默认最大滑动时间,滑动的距离不同,滑动的时间也就不同
     */
    public static final int MAX_SCROLLER_DURATION = 300;
    /**
     * 滑动器
     */
    private OverScroller mScroller;
    /**
     * 速度监测器
     */
    private VelocityTracker mVelocityTracker;
    /**
     * 机器最小滑动单位
     */
    private int mTouchSlop;
    /**
     * 记录上一次滑动事件坐标
     */
    private float mLastMotionX, mLastMotionY;
    /**
     * 按下时的坐标位置
     */
    private float mDownX, mDownY;
    /**
     * 最大滑动速度单位与最小滑动速度单位
     */
    private int mMaxVelocity, mMinVelocity;
    /**
     * 是否手指正在拖动, 是否展开状态
     */
    private boolean isBeingDrag, isMenuOpened;
    /**
     * 是否可以侧滑的开关
     */
    private boolean mSwipeEnable = true;
    /**
     * 滑动展开的宽度
     */
    private int mSwipeWidth;


    public SwipeMenuLayout(Context context) {
        super(context);
        init(context);
    }

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

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

    /**
     * 初始化滑动单位相关值
     * @param context
     */
    private void init(Context context) {
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mScroller = new OverScroller(context);
        mTouchSlop = configuration.getScaledTouchSlop();
        mMaxVelocity = configuration.getScaledMaximumFlingVelocity();
        mMinVelocity = configuration.getScaledMinimumFlingVelocity();
    }

    /**
     * 测量item的实际宽度,这里做一个限制,该控件只可有两个子元素,第一个为实际item,第二个为菜单跟删除的布局
     * 就像ScrollView也限制了只有一个子元素,你可以在子元素中添加别的子元素
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {//当没有指明控件高度时
            //这里就不考虑宽度没有指明,既然是水平滑动,宽度当然是填充整个父控件大小
            int width = getMeasuredWidth();
            if (getChildCount() <= 0) {
                setMeasuredDimension(width, 0);
            } else {//用第一个控件的高度标准来测量右边控件的高度,即菜单与删除项跟item高度一至喽
                View child = getChildAt(0);
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                int measureHeight = child.getMeasuredHeight();
                //第一个子控件元素即为item的宽度,这里就不需要考虑padding的大小了
                for (int i = 1; i < getChildCount(); i++) {
                    View otherChild = getChildAt(i);
                    LayoutParams params = otherChild.getLayoutParams();
                    if (params != null) {
                        params.height = measureHeight;
                    }
                    measureChild(otherChild, widthMeasureSpec, heightMeasureSpec);
                }
                //宽度还是第一个子元素的大小,因为右边的菜单项默认是隐藏不见,只是要在onLayout的时候要控制
                setMeasuredDimension(child.getMeasuredWidth(), measureHeight);
            }
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            measureChildren(widthMeasureSpec, heightMeasureSpec);
        }
    }

    /**
     * onMeasure中有测量了菜单项的宽度,这一步就要布局到界面中,虽然默认看不到,但是滑动的时候就会显示
     */

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View firstChild = getChildAt(0);
            int left = getPaddingLeft();
            int top = getPaddingTop();
            int firstRight = firstChild.getMeasuredWidth() + left;
            firstChild.layout(left, top, firstRight, top + firstChild.getMeasuredHeight());
            if (getChildCount() > 1) {
                View secondChild = getChildAt(1);
                mSwipeWidth = secondChild.getMeasuredWidth();
                //直接摆放到item的右边
                secondChild.layout(firstRight, top, firstRight +
                        secondChild.getMeasuredWidth(), top + secondChild.getMeasuredHeight());
            }
        }
    }

    /**
     * 事件拦截,这里的子元素只有item跟菜单删除项,没有滑动效果,为什么还要重写拦截方法呢,这里只是
     * 用来拦截当菜单删除项展开的时候,手指点击到item的位置时,应当首先考虑闭合滑动菜单,而不是点击效果,所以重写此方法
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercepted = super.onInterceptTouchEvent(ev);
        if (!isSwipeEnable()) return isIntercepted;//不允许侧滑的item不做考虑
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isIntercepted = false;
                mDownX = mLastMotionX = ev.getX();
                mDownY = mLastMotionY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float disX = Math.abs(ev.getX() - mDownX);
                float disY = Math.abs(ev.getY() - mDownY);
                //这里介绍下并不是move事件一发生就产生滑动,而是滑动的差值大于最小滑动单位才算真正的滑动
                //由于是在recyclerView中有纵向的滑动,所以还要判断x轴的滑动差值大于Y轴的滑动差值才算产生水平滑动
                isIntercepted = disX > mTouchSlop && disX > disY;
                break;
            case MotionEvent.ACTION_UP:
                isIntercepted = false;
                if (isMenuOpened() && ev.getX() < getWidth() - mSwipeWidth) {
                    //点击在item内容上时考虑拦截事件,并闭合菜单项
                    smoothCloseMenu();
                    isIntercepted = true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                //当事件被上一层的控件拦截时,这时候事件会被取消,应该结束动画
                isIntercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
        }
        return isIntercepted;
    }

    /**
     * onTouch就处理滑动效果喽
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isSwipeEnable()) return super.onTouchEvent(event);
        obtainVelocityTracker(event);
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionX = x;
                mLastMotionY = y;
                if (isMenuOpened) {
                    setPressed(false);
                    //如果展开点击item是没有按击效果的,并且直接返回true,不能返回super.onTouch,否则如果item设置了onClickListener事件,还是会出现按击效果的
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mSwipeEnable) break;
                float disX = x - mLastMotionX;
                float disY = y - mLastMotionY;
                /**
                 * isBeingDrag代表自身是否正在滑动,相信google提供滑动控件中都能见到这种变量的身影
                 * 如果此次move的坐标跟上一次的差值没有达到最小滑动单位时,不能将mLastMotionX,mLastMotionY附上新的值
                 */
                if (!isBeingDrag) {
                    float absX = Math.abs(disX);
                    if (absX > mTouchSlop && absX > Math.abs(disY)) {
                        isBeingDrag = true;
                    }
                }
                if (isBeingDrag) {
                    int scrollX = (int) disX;
                    if (isChildNeedScroll(scrollX)) {//这一步至关重要,也是大多滑动控件中都必须要判断的逻辑
                        scrollBy(-scrollX, 0);//这步就是跟着手指一起滑动
                    }
                    mLastMotionX = x;
                    mLastMotionY = y;
                }
                if (isBeingDrag) {
                //这里跟case MotionEvent.ACTION_DOWN:解释一样
                    setPressed(false);
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                isBeingDrag = false;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                scrollByVelocityX(mVelocityTracker.getXVelocity());
                releaseVelocityTracker();
                if (Math.abs(mDownX - x) > mTouchSlop || Math.abs(mDownY - y) > mTouchSlop || isMenuOpened()) {
                    event.setAction(MotionEvent.ACTION_CANCEL);
                    super.onTouchEvent(event);
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                isBeingDrag = false;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                scrollByVelocityX(mVelocityTracker.getXVelocity());
                releaseVelocityTracker();
                break;
        }
        //这里介绍一下onTouch跟onClick没有冲突的原因,如果定义的控件有设置onClickListener,isClickable会返回true,
        // super.onTouchEvent也会返回true,因此事件也不会断掉,相反没有设置onClick的时候,isClickable返回false,
        // 此时调用super.onTouchEvent会返回false,如果返回false,
        // 表明该层控件不消费事件,事件就断掉了,而那些认为onTouchEvent跟onClick有冲突的同学
        // 应该就是在重写onTouchEvent的时候直接返回true,这种写法是错误的,
        // 可以去看super.onTouchEvent的源码就知道了
        if (isClickable()) {
            return super.onTouchEvent(event);
        } else {
            return true;
        }
    }

/**

    这一步也相当重要了,在手指离开屏幕时,要执行滑动动画,就得重写此方法,其实就是根据Scroller的滑动值
不断的刷新界面上的滑动位置,如果Scroller不会使用那就得先了解下它的用法
*/
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    /**
     * 防止手指快速拖动时的越界,比如控件要向右滑动,手指滑动过快当前内容超出了滑动的区域时,
 * 如果不加此判断界面上就会出现滑动越界的空白区域,这里的逻辑应该是在滑动控件中都要考虑的
     */
    private boolean isChildNeedScroll(int diffX) {
        int scrollX = getScrollX();
        if (diffX > 0) {//向右滑动 时
            if (scrollX <= 0)
                return false;
            else if (diffX - scrollX > 0) {
                scrollTo(0, 0);
                isMenuOpened = false;
                return false;
            }
        } else {//向左滑动的时候
            if (scrollX >= mSwipeWidth)
                return false;
            else if (scrollX - diffX > mSwipeWidth) {
                scrollTo(mSwipeWidth, 0);
                isMenuOpened = true;
                return false;
            }
        }
        return true;
    }

    /**
     * 手指离开时判断所处位置并根据速度监测器所得到的手指离开地的滑动速度滑回原处或展开
     */
    private void scrollByVelocityX(float velocityX) {
        int scrollX = getScrollX();
        if (Math.abs(velocityX) > mMinVelocity && scrollX >= mSwipeWidth / 3) { //滑动加速度达到,滑动位置到达
            if (velocityX > 0) {//向右的加速度
                if (scrollX > 0) {//控件处于左偏移才可以右滑
                    mScroller.startScroll(scrollX, 0, -scrollX, 0, computeScrollDuration(scrollX, velocityX));
                    isMenuOpened = false;
                    invalidate();
                }
            } else {  //滑动到展开时的位置
                if (scrollX < mSwipeWidth) {
                    int dx = mSwipeWidth - scrollX;
                    mScroller.startScroll(scrollX, 0, dx, 0, computeScrollDuration(dx, velocityX));
                    isMenuOpened = true;
                    invalidate();
                }
            }
        } else {
            scrollToEdage(scrollX);
        }
    }

    /**
     * 滑动到展开或闭合时的边缘
     */
    private void scrollToEdage(int scrollX) {
        if (scrollX == 0) {
            isMenuOpened = false;
            return;
        }
        if (scrollX == mSwipeWidth) {
            isMenuOpened = true;
            return;
        }
        if (scrollX >= mSwipeWidth / 2) {
            int dx = mSwipeWidth - scrollX;
            mScroller.startScroll(scrollX, 0, dx, 0, computeScrollDuration(dx, 0));
            isMenuOpened = true;
        } else {
            mScroller.startScroll(scrollX, 0, -scrollX, 0, computeScrollDuration(scrollX, 0));
            isMenuOpened = false;
        }
        invalidate();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        closeMenu();
    }

    /**
     * 初始速度监测器
     *
     * @param event
     */
    private void obtainVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * 释放速度监测器
     */
    private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
        根据滑动位移和滑动速度计算滑动所需要的时间,这里参照了ViewPager源码,
    滑动距离跟滑动速度不同都应该取不同的值,不然滑动动画效果会很难看,而且原生大部分滑动控件计算滑动时间都是这个逻辑
    */
    private int computeScrollDuration(int dx, float velocityX) {
        final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
        velocityX = Math.abs(velocityX);
        int duration;
        if (velocityX > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocityX));
        } else {
            final float pageDelta = (float) Math.abs(dx) / width;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, MAX_SCROLLER_DURATION);
        return duration;
    }

    private float distanceInfluenceForSnapDuration(float f) {
        f -= 0.5f; // center the values about 0.
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    public boolean isMenuOpened() {
        return isMenuOpened || (getScrollX() > 0 && getScrollX() == mSwipeWidth);
    }

    /**
     * 缓和的关闭侧滑菜
     */
    public void smoothCloseMenu() {
        int scrollX = getScrollX();
        mScroller.startScroll(scrollX, 0, -scrollX, 0, computeScrollDuration(scrollX, 0));
        invalidate();
        isMenuOpened = false;
    }

    /**
     * 直接关闭侧滑菜单,不做动画效果
     */
    public void closeMenu() {
        scrollTo(0, 0);
        isMenuOpened = false;
    }

    public void openMenu() {
        scrollTo(mSwipeWidth, 0);
        isMenuOpened = true;
    }

    public void smoothOpenMenu() {
        int scrollX = getScrollX();
        int dx = mSwipeWidth - scrollX;
        mScroller.startScroll(scrollX, 0, dx, 0, computeScrollDuration(dx, 0));
        isMenuOpened = true;
    }

    public boolean isSwipeEnable() {
        return mSwipeEnable;
    }

    public void setSwipeEnable(boolean mSwipeEnable) {
        this.mSwipeEnable = mSwipeEnable;
    }

}

再来看看SwipeMenuRecyclerView的源码,因为滑动功能已经有了,这里的代码就只是处理些事件的拦截了,只贴出核心代码了

@Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        if (e.getPointerCount() > 1) {
            return true;//只控制滑动一个
        }
        boolean isIntercepted = super.onInterceptTouchEvent(e);
        float x = e.getX();
        float y = e.getY();
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = x;
                mDownY = y;
                int touchingPosition = getChildAdapterPosition(findChildViewUnder(x, y));
                isIntercepted = touchingPosition != mSwipedPosition && smoothCloseMenu();
                if (isIntercepted) {//有展开的menu,拦截事件,不点击
                    mSwipedLayout = null;
                    mSwipedPosition = INVALID_POSITION;
                } else {
                    ViewHolder vh = findViewHolderForAdapterPosition(touchingPosition);
                    if (vh != null) {
                        View itemView = getSwipeMenuView(vh.itemView);
                        if (itemView != null && itemView instanceof SwipeMenuLayout) {
                            mSwipedLayout = (SwipeMenuLayout) itemView;
                            mSwipedPosition = touchingPosition;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                isIntercepted = handleUnDown(x, y, isIntercepted);
                break;
        }
        return isIntercepted;
    }

    /**
     * 找到SwipeMenuLayout
     * @param itemView
     * @return
     */
    private View getSwipeMenuView(View itemView) {
        if (itemView instanceof SwipeMenuLayout) {
            return itemView;
        }
        List<View> unvisited = new ArrayList<>();
        unvisited.add(itemView);
        while (!unvisited.isEmpty()) {
            View child = unvisited.remove(0);
            if (!(child instanceof ViewGroup)) { // view
                continue;
            }
            if (child instanceof SwipeMenuLayout) {
                return child;
            }
            ViewGroup group = (ViewGroup) child;
            final int childCount = group.getChildCount();
            for (int i = 0; i < childCount; i++) {
                unvisited.add(group.getChildAt(i));
            }
        }
        return itemView;
    }

    private boolean handleUnDown(float x, float y, boolean defaultValue) {
        float disX = mDownX - x;
        float disY = mDownY - y;
        if (Math.abs(disX) > mTouchSlop) {//swipe
            defaultValue = false;
        }
        if (Math.abs(disY) < mTouchSlop && Math.abs(disX) < mTouchSlop) {// click
            defaultValue = false;
        }
        return defaultValue;
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (e.getAction() == MotionEvent.ACTION_MOVE) {
            smoothCloseMenu();
        }
        return super.onTouchEvent(e);
    }

    /**
     * 闭合menu
     */
    public boolean smoothCloseMenu() {
        if (mSwipedLayout != null && mSwipedLayout.isMenuOpened()) {
            mSwipedLayout.smoothCloseMenu();
            return true;
        }
        return false;
    }

看预览效果

这里写图片描述


小结:ViewGroup滑动控件的定义相对View的定义是难一点点,但是只要思维清晰还是很简单,另外android有提供些写好的滑动辅助类GestureDetector直接帮你封装好了onTouchEvent事件,滑动原理还是一样,里面还有handler处理单击与长按的逻辑原理,可以去学学这个类的使用,当然RecyclerView也提供了ItemTouchHelper,其实里面也就是封装了GestureDetector类,这里介绍的是滑动的原理,原理懂了用起来就简单多了是吧!!!



原来定义控件是不是so easy

google原码是个好东西!!!
google原码是个好东西!!!
google原码是个好东西!!!


如果你想进一步提升就多看些开源的控件原理是如何实现的,或是直接看google提供的那些滑动控件的原码,只是用用谁都会~~!!!,多指触摸跟多层滑动嵌套冲突的问题下篇文章再介绍喽!**

最后附上示例原码:http://download.csdn.net/detail/u012216274/9802752

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值