第五章 Android Scroll 分析

Android 群英传笔记
第一章Android体系与系统架构
第二章 Android开发工具及技巧
第三章 Android控件架构与事件拦截机制
第四章 ListView 使用技巧
第五章 Android Scroll 分析
第六章 Android 绘图机制与屏幕适配
第七章 Android 动画机制与使用技巧
第八章 Activity与Activity调用栈分析
第九章 Android 系统信息与安全机制
第十章 Android性能优化
本文出自:
http://www.jianshu.com/u/a1251e598483

滑动效果是如何产生的

滑动一个View的本质其实就是移动一个View,改变其当前所在的位置,它的原理和动画效果十分的相似,就是通过不断的改变View的坐标来实现这一效果,动态且不断的改变View的坐标,从而实现View跟随用户触摸滑动而滑动
但是在讲解滑动效果之前,需要先了解一下Android中窗口坐标体系和屏幕的触控事件——MotionEvent

Android坐标系

在物理学上,要描述一个物体的运动,就必须选定一个参考系,所谓滑动,正是相对于参考系的运动,在Android,系统将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向,如图

《第五章 Android Scroll 分析》 Android 坐标系

系统提供了getLocationOnScreen(intlocation[])来获取Android坐标中的位置,即该视图左上角Android的坐标,另外,在触摸事件中使用getRawX(),getRawY()方法来获取坐标同样是Android坐标系中的坐标。

视图坐标系

Android中除了上面所说的这种坐标系之外们还有一个视图坐标系,他描述了子视图在父视图的位置关系,这两个坐标系并不复杂也不矛盾,他们的作用是相辅相成的,与Android坐标系类似,视图坐标系同样的以原点向右为X正方向,以原点向下为Y方法,只不过在视图坐标系中,原点不再是Android坐标系中的屏幕左上角,而是以父视图左上角为坐标原点,看图

《第五章 Android Scroll 分析》 视图坐标系

在触控事件中通过getX,getY来获取的坐标就是视图坐标中的坐标

触控事件——MotionEvent

触控事件MotionEvent在用户的交互中,占着举足轻重的位置,学好触控事件是掌握后续内容的基础,首先,我们来看看MotionEvent中封装的一些常量,他定义了触摸事件的不同类型。

    //单点触摸按下的动作
    public static final int ACTION_DOWN = 0;
    //单点触摸离开的动作
    public static final int ACTION_UP = 1;
    //单点触摸移动的动作
    public static final int ACTION_MOVE = 2;
    //单点触摸取消
    public static final int ACTION_CANCEL = 3;
    //单点触摸超出边界
    public static final int ACTION_OUTSIDE = 4;
    //多点触摸按下的动作
    public static final int ACTION_POINTER_DOWN = 5;
    //多点触摸离开的动作
    public static final int ACTION_POINTER_UP = 6;

通常情况下,我们会在onTouchEvent(MotionEvent event)方法中通过event.getAction()来获取触摸事件的类型,并使用switch来判断,这个代码模块是固定的 上一篇文章已经写过了

@Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取当前输入点的X,Y坐标(视图坐标)
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //处理输入的按下动作
                break;
            case MotionEvent.ACTION_MOVE:
                //处理输入的移动动作
                break;
            case MotionEvent.ACTION_UP:
                //处理输入的离开动作
                break;
        }
        return true;
    }

在不涉及多点触控的前提下,通常可以使用以下代码来完成触摸事件的监听,不过这里只是一个代码模块,后面我们会讲具体的逻辑的

在Android中,系统提供了非常多的方法来获取坐标值,相对距离等,方法丰富固然好,但也给初学者带来了很多的困扰,不知道在什么情况下使用下图总结人一下一些常用的API

《第五章 Android Scroll 分析》 获取坐标值的各种方法

这些方法可以分成两个类别

View提供的获取坐标方法
getTop():获取到的是View自身的顶部到其父布局顶部的距离
getLeft():获取到的是View自身的左边到其父布局左边的距离
getRight():获取到的是View自身的右边到其父布局右边的距离
getBottom():获取到的是View自身的底部到其父布局底部的距离

MotionEvent提供的方法
getX():获取点击事件距离控件左边的距离,即视图坐标
getY():获取点击事件距离控件顶部的距离,即视图坐标
getRawX:获取点击事件整个屏幕左边的距离,即绝对坐标
getRawY:获取点击事件整个屏幕顶部的距离,即绝对坐标

实现滑动的七种方法

当了解了Android坐标系和触控事件之后,我们再来看一看如何使用系统提供的API来实现动态的修改一个View的坐标,即滑动效果,而不管采用哪种方式,其实现的思路基本上是一样的,当触摸View的时候,系统几下View的坐标,从而获得到相对于之前坐标的偏移量,并通过偏移量来修改View的坐标,这样不断的重复就实现了滑动的过程。

下面我们通过实例来看看Android中如何实现滑动的效果没定义一个View,简单的实现一个布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="com.example.younger.youngertest.MainActivity">

    <com.example.younger.youngertest.MyView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_orange_dark"
        >
    </com.example.younger.youngertest.MyView>
    
</LinearLayout>

默认的显示样子

《第五章 Android Scroll 分析》 示例布局

layout方法

我们都知道,在View的绘制上,会调用onLayout()方法来设置显示的位置,同样可以修改View的left,top,right,bottom四个属性来控制View的坐标,与前面提供的模板代码一样,每次调用onTouchEvent()的时候,我们来获取点的坐标,这里的逻辑很清楚,真的,我们来看看

    //触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        int lastX = 0;
        int lastY = 0;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录触摸点的坐标
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int officeX = rawX - lastX;
                int officeY = rawY - lastY;
                //在当前的left,top,right,bottom基础上加上偏移量
                layout(getLeft()+officeX,getTop()+officeY,getRight()+officeX,getBottom()+officeY);
                //重新设置初始值
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_UP:
                //处理输入的离开动作
                break;
        }
        return true;
    }
offsetLeftAndRight()与offsetTopAndBottom()

这个方法相当于系统提供的一个对左右,上下移动的封装,当计算出偏移量的时候,只需要使用如下的代码就可以完成View的重新布局,效果和使用Layout()方法是一样的

//同时对左右偏移
offsetLeftAndRight(officeX);
//同时对上下偏移
offsetTopAndBottom(officeY);

LayoutParams

LayoutParams保留了一个View的布局参数,因此可以在程序中,通过改变LayoutParams来动态改变一个布局的位置参数,从而改变View位置的效果,我们可以很方便的在程序中使用getLayoutParams()来获取一个View的LayoutParams,当然,在计算偏移量的方法和Layout方法中计算offset是一样的,当获取到偏移量之后,可以通过setLayoutParams来改变LayoutParams,代码如下

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+officeX;
                layoutParams.topMargin = getTop()+officeY;
                setLayoutParams(layoutParams);

不过这里需要注意的是,通过getLayoutParams()获取layoutParams时,需要根据View所在的跟布局的类型来设置不同的类型,,比如View放在LinearLayout里那就是 LinearLayout.LayoutParams,比如在RelativeLayout里就是 RelativeLayout.LayoutParams,不然系统是无法获取到layoutParams的.

在通过一个layoutParams来改变一个View的位置时,通常改变的是这个view的Margin属性,所以除了使用布局的layoutParams属性外,还需要 ViewGroup.MarginLayoutParams来实现这样的功能

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+officeX;
                layoutParams.topMargin = getTop()+officeY;
                setLayoutParams(layoutParams);

我们可以发现,用ViewGroup更好,都不用去管父布局是什么

scrollTo与scrollBy

在一个View当中,系统提供了scrollTo与scrollBy这两种方式来实现移动一个View的位置,这两种方法的区别也很好理解,to和by, scrollTo(x,y);表示移动到一个具体的 点,scrollBy(dx,dy);表示移动的增量

int officeX = rawX – lastX;
int officeY = rawY – lastY;
scrollBy(officeX,officeY);

但是,当我们拖动View的时候,你会发现View并没有移动,难道我们写错了?方法没有写错,View也的确移动了,但是他移动的并不是我们想要的东西,他只是移动了view的content,即让View的内容移动了,如果用ViewGroup使用to和by的话,那所有的子View都将移动,要是在View中使用的话,,那么移动的就是View的内容了,我们举个例子,比如TextView,content就是他的文本,ImageView,drawable就是对象

相信通过上面的分析,你也应该知道为什么不能再View里面使用这个两个方法来拖动这个view了,那么我们就该View所在的ViewGroup中使用scrollBy方法来移动这个view

((View)getParent()).scrollBy(officeX,officeY);

但是,当再次拖动View的时候,你会发现View虽然移动了,但却在乱动,并不是我们想要的跟随触摸点的移动而移动,这里需要先了解一下视图移动的一些知识,大家在理解这个问题的时候,不妨想象一下手机是一个中空的盖板,盖板下面是一个巨大的画布,也就是我们想要显示的视图,当把这个盖板盖在画布的某处时,透过中间空着的矩形,我们看见了手机屏幕上显示的视图,而画布上其他的视图,则被盖板盖住了无法看见,我们的视图和这个例子事项相似,我们没有看见视图,但并不代表它不存在,有可能只是在屏幕外面而已,当调用scrollBy的方法时,可以想象外面的盖板在移动,这么说比较抽象,我们来看一下具体的例子

《第五章 Android Scroll 分析》 理解ScrollBy

上图,中间的矩形相当于屏幕,即可是区域,后面的content相当于画布,代表视图,大家可以看到,只有视图的中间部分目前是可视的,其他部分都不可见,可见区域中设置一个button,他的坐标是(20.10),下面我们使用scrollBy方法来进行移动后如图

《第五章 Android Scroll 分析》 移动之后的可视区域

我们会发现,虽然设置scrollBy(20.10),偏移量均为XY的正方向,但是屏幕的可视区域,Button却向反方向移动了,这就是参考系选择的不同,而产生的不同效果。

通过上面的分析,可以发现,我们将scrollBy的参数dx,dy设置成正数,那么content将向坐标轴负方向移动,反之,则正方向

  int officeX = rawX - lastX;
  int officeY = rawY - lastY;
  scrollBy(-officeX,-officeY);

可以发现,效果和前面的几种方法相同了,类似的,我们使用scrollTo也是可以实现的

Scroller

既然提到scrollTo与scrollBy,那就不得不提一下Scroller类了,Scroller和scrollTo与scrollBy十分的相似,有着千丝万缕的关系,那么他们有什么具体的区别尼?要解答这个问题,首先我们来看一个小栗子,假设要完成这样的一个效果:点击button,让一个viewgroup的子View移动100像素,问题看似很简单,只要使用scrollBy的方法就可以,的确,用这个方法确实可以,可是那都是一瞬间完成的事情,很突兀,而Scroller就可以实现平滑的效果,而不再是一瞬间的事情.

说道Scroller的原理,其实他与前面使用scrollTo与scrollBy的方法原理是一样的,下面我们通过一个小栗子来演示一下

  • 初始化scroller

首先,通过他的构造方法来创建一个scroller对象

//初始化mScroller
mScroller = new Scroller(context);

  • 重写computeScroll,实现模拟滑动

下面我们需要重写computeScroll这个方法,他是使用Scroller的核心,系统在绘制View的同时,会在onDraw()方法中调用这个方法,这个方法实际上就是使用了ScrollTo()方法再结合Scroller对象,帮助获取到当前的滚动值,我们可以通过不断的瞬息移动一个小的距离来实现整体上的平滑移动效果,通常情况下,computeScroll的代码可以利用标准的写法:

   /**
     * 模拟滑动
     */
    @Override
    public void computeScroll() {
        super.computeScroll();

        //判断Scroller是否执行完毕
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        }
        //通过重绘来不断调用computeScroll
        invalidate();
    }

Scroller类提供了computeScrollOffset()来判断是否完成了整个页面的滑动,,同时也提供了getCurrX(),getCurrY()来获取当前滑动坐标,上面唯一要注意的就是invalidate()了,因为只能在computeScroll中获得模拟过程中的scrollX,scrollY,坐标,但computeScroll方法是不会自动调用的,只能通过invalidate——》OnDraw——》computeScroll来简介调用,所以需要这个invalidate,而当模拟过程结束的时候,computeScrollOffset返回的是false,从而结束循环

  • startScroll开启模拟过程

最后,万事俱备只欠东风了,我们需要使用平滑移动事件,使用Scroller类的startScroll()方法来开启平滑过程,startScroll有两个重载的方法

public void startScroll(int startX,int startY,int dx,int dy,int duration)
public void startScroll(int startX,int startY,int dx,int dy)

可以看到他们的区别分别是具有指定的持续时长,而另一个没有,这个非常好理解,与在动画中的设置时间是一样的,而其他四个坐标,就是起始和偏移量,通过上述的步骤,就可以完成一个平移的效果了,下面我们回到实例,在构造分钟初始化Scroller对象,然后重写computeScroll方法,最后需要监听手指离开屏幕的事件,并在该事件之后调用startScroll()完成平移,所以我们在ACTION_UP中

      case MotionEvent.ACTION_UP:
                //处理输入的离开动作
                View view = ((View)getParent());
                mScroller.startScroll(view.getScrollX(),view.getScrollY(),view.getScrollX(),view.getScrollY());
                invalidate();
                break;
属性动画

在本书中第七章将会详细介绍,这里就不重复了

ViewDragHelper

Google在其support库中为我们提供了一个DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现策划效果,这两个布局,大大的方便了我们自己创建自己的滑动布局,然而,这两个强大的布局背后,却隐藏着一个鲜为人知,却功能强大的类——ViewDragHelper,通过ViewDragHelper,基本可以实现各种不同的侧滑,拖放需求,因此这个方法也是各种滑动解决方案的终极绝招

ViewDragHelper虽然很强大,但是使用也是本章节最复杂 的,我们需要了解ViewDragHelper的基本使用方法的基础上,通过不断的练习去掌握它,我们这里就实现一个 QQ滑动侧边栏的布局,我么来看看具体怎么实现的

  • 初始化ViewDragHelper

ViewDragHelper通常定义在一个ViewGroup中,通过其静态方法初始化

mViewDragHelper = ViewDragHelper.create(this,callback);

他的第一个参数是要监听的View,第二个参数是一个Callback回调

  • 拦截事件

要重写拦截事件,将事件传递给ViewDragHelper进行处理

    //事件拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    //触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递给ViewDragHelper

        mViewDragHelper.processTouchEvent(event);

        return true;
    }
  • 处理computeScroll()

使用ViewDragHelper也是需要重写computeScroll的,因为ViewDragHelper内部也是通过Scroller来实现平移的,我们可以这样使用

@Override
  public void computeScroll() {
      if(mViewDragHelper.continueSettling(true)){
          ViewCompat.postInvalidateOnAnimation(this);
      }
  }
  • 处理回调Callback
    //侧滑回调
    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        //何时开始触摸
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //如果当前触摸的child是mMainView开始检测
            return mMainView == child;
        }

        //处理水平滑动
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        //处理垂直滑动
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        //拖动结束后调用
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //手指抬起后缓慢的移动到指定位置
            if(mMainView.getLeft() <500){
                //关闭菜单
                mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else{
                //打开菜单
                mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };

通过一步步的分析,现在要实现QQ的侧滑,是不是非常简单了尼,下面自定义一个viewGroup来完成整个编码的实例

 //XML加载组建后回调
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }


    //组件大小改变时回调
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }

最后 整个view 的代码

package com.example.younger.youngertest;

import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

/**
 * Created by Younger on 2018/3/6.
 */
public class DragView extends FrameLayout {
    private ViewDragHelper mViewDragHelper;
    private View mMenuView, mMainView;
    private int mWidth;
    public DragView(@NonNull Context context) {
        super(context);
        initView();
    }

    public DragView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public DragView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public DragView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递给ViewDragHelper,此操作必不可少
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private void initView() {

        mViewDragHelper = ViewDragHelper.create(this, callback);
    }

    private ViewDragHelper.Callback callback =
            new ViewDragHelper.Callback() {

                // 何时开始检测触摸事件
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //如果当前触摸的child是mMainView时开始检测
                    return mMainView == child;
                }

                // 触摸到View后回调
                @Override
                public void onViewCaptured(View capturedChild,
                                           int activePointerId) {
                    super.onViewCaptured(capturedChild, activePointerId);
                }

                // 当拖拽状态改变,比如idle,dragging
                @Override
                public void onViewDragStateChanged(int state) {
                    super.onViewDragStateChanged(state);
                }

                // 当位置改变的时候调用,常用与滑动时更改scale等
                @Override
                public void onViewPositionChanged(View changedView,
                                                  int left, int top, int dx, int dy) {
                    super.onViewPositionChanged(changedView, left, top, dx, dy);
                }

                // 处理垂直滑动
                @Override
                public int clampViewPositionVertical(View child, int top, int dy) {
                    return 0;
                }

                // 处理水平滑动
                @Override
                public int clampViewPositionHorizontal(View child, int left, int dx) {
                    return left;
                }

                // 拖动结束后调用
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //手指抬起后缓慢移动到指定位置
                    if (mMainView.getLeft() < 500) {
                        //关闭菜单
                        //相当于Scroller的startScroll方法
                        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                        ViewCompat.postInvalidateOnAnimation(DragView.this);
                    } else {
                        //打开菜单
                        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
                        ViewCompat.postInvalidateOnAnimation(DragView.this);
                    }
                }
            };
}

XML 的写法

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

    <com.example.younger.youngertest.DragView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/view"
       >
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_blue_light">

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Menu" />
        </FrameLayout>

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_orange_dark">

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Main" />
        </FrameLayout>
    </com.example.younger.youngertest.DragView>
</RelativeLayout>
  • 这里只是非常简单的模拟了一下
       //用户触摸到view回调
       @Override
       public void onViewCaptured(View capturedChild, int activePointerId) {
           super.onViewCaptured(capturedChild, activePointerId);
       }
  • onViewDragStateChanged
       //拖拽状态改变时,比如idle,dragging
        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
        }
  • onViewPositionChanged
        //位置发生改变,常用于滑动scale效果
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }

好了 ,Scroll 分析到此结束.

    原文作者:YoungerDev
    原文地址: https://www.jianshu.com/p/e25861a4340e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞