链接 Android Scroll 分析

这是我重读《Android 群英传》的时候做的读书笔记,这里主要讲了 Android 坐标系和视图坐标系,以及实现滑动的多种实现方法。

Android 坐标系和视图坐标系

区别

  • Android 坐标系:左上角作为原点,由 getLocationScreen(int location[]) 获取点的位置,或在触控事件中使用 getRawX()getRawY()获得Android 坐标系中的坐标。

  • 视图坐标系:子视图在父视图中的位置关系,同样,父视图的左上角为坐标原点,通过 getX()getY() 获得视图坐标系的坐标。

获取坐标值

View 提供的获取坐标方法

getTop()getLeft()getRight()getBottiom() 获取到的是 View 自身的顶边、左边、右边、底边到其父布局顶边、左边、右边、底边的距离。

MotionEvent 提供的方法

  • getX(), getY() 获取到点击事件距离控件左边、顶边的距离,即视图坐标
  • getRawX(), getRawY() 获取点击事件距离整个屏幕左边、顶边的距离,即绝对坐标

实现滑动的方法

滑动的思想:触摸 View 时,系统记下当前触摸点坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量修改 View 的坐标。如此重复,从而实现滑动的过程。

1. layout 方法

@Overridepublic boolean onTouchEvent(MotionEvent event){    //绝对坐标,当然也可以通过 getX() 视图坐标获取偏移量, 两种方式得到的偏移量都是相同的    //但是注意,使用绝对坐标系,一定要在每次执行完 ACTION_MOVE 的逻辑后,重设初始坐标,才能准确地获取偏移量    int rawX = (int)event.getRawX();    int rawY = (int)event.getRawY();    switch(event.getAction()){        case MotionEvent.ACTION_DOWN:            //记录触摸点坐标            lastX = rawX;            lastY = rawY;            break;        case MotionEvent.ACTION_MOVE:            //计算偏移量            int offsetX = rawX - lastX;            int offsetY = rawY - lastY;            //在当前 left, top, right, bottom 的基础上加上偏移量            layout(getLeft() + offsetX,                getTop() + offsetY,                getRight() + offsetX,                getBottom() + offsetY);            //重设初始坐标            lastX = rawX;            lastY = rawY;            break;    }    return true;}

2. offsetLeftAndRight()offsetTopAndBottom() 方法

与 layout 方法 效果相同,只是多了一层封装而已

//同时对 left 和 right 进行偏移offsetLeftAndRight(offsetX);//同时对 top 和 bottom 进行偏移offsetTopAndBottom(offsetY);

3. LayoutParams

LayoutParams 保存了一个 View 的布局参数,所以通过改变 LayoutParams 来动态修改一个布局的位置参数,从而达到改变 View 位置的效果。

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();layoutParams.leftMargin = getLeft() + offsetX;layoutParams.topMargin = getTop() + offsetY;setLayoutParams(layoutParams);

所以,其实我们改变的是这个 View 的 Margin 属性

4. scrollTo 和 scrollBy

  • scrollTo(x, y) 移动到一个具体的坐标点

  • scrollBy(dx, dy) 移动的增量为 dx, dy

scrollTo 和 scrollBy 移动的是 View 的内容。即:对 TextView 使用的话,则是移动它的文本;对 ViewGroup 使用的话,则移动的是所有的子 View。所以,一般不对 View 使用这两个方法,而是对 ViewGroup 使用。

int offsetX = x - lastX;int offsetY = y - lastY;//注:随手指移动的话,偏移量要为负((View)getParent()).scrollBy(-offsetX, -offsetY);

5. Scroller

Scroller 类可以实现平滑移动的效果,而不再是瞬间完成的移动。

Scroller 原理:和 scrollTo, scrollBy 类似,只是,在 ACTION_MOVE 中不断获取手指移动的微小偏移量,将一段距离划分为 N 个非常小的偏移量。在每个偏移量里面通过 scrollBy 方法进行瞬间移动,实现平滑移动。

例:让子 View 跟随手指滑动,但在手指离开屏幕时,让子 View 平滑移动到初始位置,即屏幕左上角。

使用 Scroller 类需要如下三个步骤:

a. 初始化 Scroller
mScroller = new Scroller(context);
b. 重写 computeScroll() 方法,实现模拟滚动
//模板代码//系统在绘制 View 的时候会在 draw() 方法中调用该方法//该方法实际上就是使用 scrollTo 方法, 不断的瞬间移动一个小距离实现整体的平滑移动效果@Overridepublic void computeScroll(){    super.computeScroll();    //判断 Scroller 是否执行完毕    if(mScroller.computeScrollOffset()){        ((View)getParent()).scrollTo(            mScroller.getCurrX(),//获得当前的滑动坐标            mScroller.getCurrY());        //通过重绘不断调用 computeScroll        invalidate();    }}

需要注意:因为只能在 computeScroll() 方法中获取模拟过程的 scrollX 和 scrollY 坐标,但 computeScroll() 不会自动调用,只能通过 invalidate() -> draw() -> computeScroll() 来间接调用 computeScroll(),所以需要在上述代码中调用invalidate(),实现循环获取 scrollX 和 scrollY 的目的。模拟过程结束后,scroller.computeScrollOfset() 方法会返回 false, 从而中断循环,完成整个平滑移动的过程。

c. startScroll 开启模拟过程
case MotionEvent.ACTION_UP:    //手指离开时,执行滑动过程,让子 View 平滑移动到初始位置,即屏幕左上角    View viewGroup = ((View)getParent());    mScroller.startScroll(        viewGroup.getScrollX(),//起始坐标        viewGroup.getScrollY(),        -viewGroup.getScrollX(),//偏移量        -viewGroup.getScrollY());    //通知重绘!    invalidate();    break;

6. 属性动画

略(我也等笔记做到第7章的动画机制的时候再写吧,哈哈)

7. ViewDragHelper

例:实现 QQ 滑动侧边栏的布局

public class DragViewGroup extends FrameLayout {    private ViewDragHelper mViewDragHelper;    private View mMenuView, mMainView;    private int mWidth;    public DragViewGroup(Context context) {        super(context);        initView();    }    public DragViewGroup(Context context, AttributeSet attrs) {        super(context, attrs);        initView();    }    public DragViewGroup(Context context,                         AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    //加载布局文件完成后调用    @Override    protected void onFinishInflate() {        super.onFinishInflate();        //按顺序将子 View 分别定义成 MenuView 和 MainView        mMenuView = getChildAt(0);        mMainView = getChildAt(1);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        // 获取 View 的宽度        // 如果需要根据 View 的宽度来处理滑动后的效果,可以使用这个值来判断        mWidth = mMenuView.getMeasuredWidth();    }    //步骤二:拦截    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        //将事件传递给 ViewDragHelper 处理        return mViewDragHelper.shouldInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        //将触摸事件传递给ViewDragHelper,此操作必不可少        mViewDragHelper.processTouchEvent(event);        return true;    }    //步骤一:初始化    private void initView() {        //使用静态工厂方法初始化        //参数1 通常是一个 ViewGroup,即parentView        //参数2 是一个 Callback 回调,是整个 ViewDragHelper 的逻辑核心        mViewDragHelper = ViewDragHelper.create(this, callback);    }    //步骤四:处理回调 Callback    private ViewDragHelper.Callback callback =            new ViewDragHelper.Callback() {                // 何时开始检测触摸事件                // 通过该方法,指定在创建 ViewDragHelper 时,参数 parentView 中的哪一个子 View 可以被移动                @Override                public boolean tryCaptureView(View child, int pointerId) {                    //如果当前触摸的child是mMainView时开始检测                    //即 只有 MainView 可以被拖动                    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);                }                // 处理垂直滑动                // top: 垂直方向上 child 移动的距离                // 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;                }                // 拖动结束后调用                // 即手指离开屏幕后实现的操作                // 该方法内部是通过 Scroller 类来实现的                @Override                public void onViewReleased(View releasedChild, float xvel, float yvel) {                    super.onViewReleased(releasedChild, xvel, yvel);                    //手指抬起后缓慢移动到指定位置                    //让 MainView 移动后左边距小于500像素时,就使用 smoothSlideViewTo() 将 MainView 还原到初始状态,即(0,0)的点                    if (mMainView.getLeft() < 500) {                        //关闭菜单                        //相当于Scroller的startScroll方法                        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);                    } else {                        //打开菜单                        //大于500时,移动到(300, 0)坐标,即显示 MenuView                        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);                    }                }            };    //步骤三:处理 computeScroll()    //因为 ViewDragHelper 内部同样是通过 Scroller 来实现平滑移动的,所以重写该方法    //可作为模板代码    @Override    public void computeScroll() {        if (mViewDragHelper.continueSettling(true)) {            ViewCompat.postInvalidateOnAnimation(this);        }    }}

ViewDragHelper.Callback 中其他的一些强大的事件

  • onViewCaptured() 触摸到 View 后回调
  • onViewDragStateChanged() 拖拽状态改变时回调
  • onViewPositionChanged() 位置改变时回调,如滑动时更改 scale 进行缩放等效果

更多相关文章

  1. 另一个更简单的Android应用程序全屏的方法
  2. Android SDK4.0 离线安装方法
  3. Android 全屏显示的两种方法
  4. Android中设置文本颜色的三种方法
  5. Android P(api28) 不支持 http 协议解决方法
  6. 三步搞定:Vue.js调用Android原生方法
  7. android 防止键盘弹出的简单方法
  8. Android高手进阶教程(十七)之---Android中Intent传递对象的两种

随机推荐

  1. android lint的使用
  2. Android(安卓)EditText默认不弹出软件键
  3. 关于android 数据库SQLite的使用日记
  4. CTS测试介绍
  5. ARFoundation系列讲解-11安装GoodlePlayS
  6. EditText与退格键的冲突问题
  7. 高德Titus
  8. 【Android】LiveData 用法及源码解析
  9. 关于使用Eclipse打包签名后APP出现不能正
  10. Android(安卓)发送有序广播