《Android群英传》读书笔记5.Android Scroll分析

相对于Android 2.X版本中的长按、点击操作,从Android 4.X开始,滑动操作出现在了Android中

1. 滑动效果是如何产生的

滑动一个View,本质上就是移动一个View,通过不断地改变View的坐标来实现这一效果

1.1. Android坐标系

屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向。
系统提供了getLocationOnScreen(int location[])方法来获取Android坐标系中点的位置。另外,在触控事件中使用getRawX()getRawY()方法所获得的坐标同样是Android坐标系中的坐标。

1.2. 视图坐标系

描述子视图在父视图中的位置关系,以父视图左上角为坐标原点,以原点向右为X轴正方向,以原点向下为Y轴正方向
在触控事件中,通过getX()getY()所获得的坐标就是视图坐标系中的坐标

1.3. 触控事件——MotionEvent

MontionEvent中封装的一些常用的事件常量

// 单点触摸按下动作
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()方法来获取触控事件的类型
Android系统提供了非常多的方法来获取坐标值、相对距离等,如图。这些方法可以分成两类:
* View提供的获取坐标方法:getTop() getLeft() getRight() getBottom()
* MotionEvent提供的方法:getX() getY() getRawX() getRawY()
获取坐标值的各种方法

2. 实现滑动的七种方法

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

2.1. layout方法

MotionEvent.ACTION_DOWN时,记录触摸点坐标;当MotionEvent.ACTION_MOVE时:

// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

// 计算偏移量
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;

使用绝对坐标系,在每次执行完ACTION_MOVE的逻辑后,一定要重新设置初始坐标,因为从触摸按下直至触摸离开,触摸点的相对位置不会改变,而绝对位置会一直改变

2.2. offsetLeftAndRight()offsetTopAndBottom()
// 同时对left和right进行偏移
offsetLeftAndRight(offsetX);
// 同时对top和bottom进行偏移
offsetTopAndBottom(offsetY);
2.3. LayoutParams
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// 或 LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); // 这里LinearLayout是父布局的类型
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
2.4. scrollToscrollBy
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);

scrollToscrollBy方法移动的是View的content,即让View的内容移动,如果在ViewGroup中使用scrollToscrollBy方法,移动的将是所有子View
scrollBy方法移动的是屏幕(可视区域),而不是View,如图为scrollBy(20, 10)的效果
理解scrollBy
因此,如果将scrollBy中的参数dx和dy设置为正数,那么content将向坐标轴负方向移动,如果将scrollBy中的参数dx和dy设置为负数,那么content将向坐标轴正方向移动
在使用绝对坐标时,可以通过使用scrollTo方法来实现这一效果

2.5. Scroller

不管使用scrollTo还是scrollBy方法,子View的平移都是瞬间发生的。Google建议使用自然的过渡动画来实现移动效果。通过Scroller类可以实现平滑移动的效果。
虽然scrollBy方法是让子View瞬间从某点移动到另一个点,但是由于在ACTION_MOVE事件中不断获取手指移动的微小的偏移量,从而在整体上获得一个平滑移动的效果。Scroller类的实现原理与其类似。
使用Scroller类需要如下三个步骤:
* 初始化Scroller
* 重写computeScroll()方法,实现模拟滑动

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

computeScroll()方法不会自动调用,只能通过invalidate()draw()computeScroll()来间接调用.
* startScroll开启模拟过程(此例子为手指移动时scrollBy,手指离开后startScroll

View viewGroup = ((View) getParent());
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate(); //invalidate()→draw()→computeScroll()
2.6. 属性动画

详见第7章

2.7. ViewDragHelper

例:实现类似QQ滑动侧边栏的布局,初始时显示内容界面,当用户手指滑动超过一段距离时,内容界面侧滑显示菜单界面。
* 初始化ViewDragHelper
ViewDragHelper通常定义在一个ViewGroup的内部,并通过其静态工厂方法进行初始化

mViewDragHelper = ViewDragHelper.create(this, callback);
  • 拦截事件
@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 clampViewPositionVertical(View child, int top, int dy) {
        return 0;
    }

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

继续优化:在ViewDragHelper.Callback中,通过重写onViewReleased()方法,可以实现当手指离开屏幕后实现的操作。这个方法内部是通过Scroller类来实现的,这也是前面重写computeScroll()方法的原因

// 拖动结束后调用
@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(DragViewGroup.this);
    } else {
        //打开菜单
        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
    }
}

关闭菜单时,将MainView还原到初始状态,即坐标为(0, 0)的点,打开菜单时,则将MainView移动到(300, 0)坐标。

在自定义ViewGroup的onFinishInflate()方法中,定义MenuView和MainView

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mMenuView = getChildAt(0);
    mMainView = getChildAt(1);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值