《Android群英传》读书笔记(4)第五章:Android Scroll分析

1.Android坐标系

在Android中,将屏幕左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,向下是Y轴正方向。
系统提供了getLocatinoOnScreen(int location[]);这样的方法来获取Android坐标系中点的位置。

2.视图坐标系

视图坐标系描述子视图在父视图中的位置关系,视图坐标系的原点是父视图的左上角。通过getX()getY()获得的是视图坐标系中的坐标。

3.触控事件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;
View提供的获取坐标的方法

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

MotionEvent提供的方法

getX():获取点击事件距离控件左边的距离,即视图坐标
getY():获取点击事件距离控件顶边的距离,即视图坐标
getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标
getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标
可以参考下图:

4.实现滑动的7种方法

1.使用layout方法

用View中的layout(int left,int top,int right,int bottom)方法在每次触摸移动后加上相应的偏移量就能实现滑动:

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX, getBottom() + offsetY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

或者使用getRawX()getRawY()方法来获取坐标,不过要记得在ACTION_MOVE逻辑的最后为lastXlastY重新赋值。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        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;
                layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX, getBottom() + offsetY);
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
2.offsetLeftAndRight()offsetTopAndBottom()

这个方法相当于系统提供的左右、上下移动的API的封装,计算出偏移量后,只需要使用如下的代码完成View的重新布局:

//对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
3.使用LayoutParams

LayoutParams保存了一个View的布局参数,因此可以在程序中通过改变LayoutParams来动态地修改一个布局的位置参数。从而达到改变View位置的效果。使用getLayoutParams()来获取一个View的LayoutParams然后通过setLayoutParams()来改变View的LayoutParams,代码如下所示:

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

scrollTo(x,y)表示移动到一个具体的具体的坐标点(x,y),scrollBy(dx,dy)表示移动的偏移量为dx、dy;
使用scrollByscrollTo时需要通过父布局来进行移动,因为这两个方法移动的是View的内容或者ViewGroup的子view。因此通过父View调用该方法移动的才是子View,还有一点是此时移动的方向正好和offset的方向相反,需要将其取反。代码如下:

((View)getParent()).scrollBy(-offsetX,-offsetY);
5.Scroller

scrollBy()scrollTo()方法实现的移动都是瞬间完成的,而Scroller类可以实现平滑移动的效果。使用Scroller都要重写computeScroll()方法,系统在绘制View时会在draw()方法中调用它,这个方法实际上就是使用scrollTo()的方法,再结合Scroller对象帮助获取当前的滚动值,来实现滚动效果。下面是computeScroll()的代码:

@Override
public void computeScroll(){
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        ((View)getParent()).scrollTo(mScroller.getCurrX(),
            mScroller.getCurrY());
        invalidate();
    }
}

computeScrollOffset()方法判断是否完成了整个滑动过程,过程结束后该方法会返回false,getCurrX()和getCurrY()来获得当前的滑动坐标。通过invalidate()方法重绘来循环调用computeScroll()方法。
使用ScrollerstartScroll()方法来开启滑动过程,Scroller类提供了两个重载的方法:

startScroll(int startX,int startY,int dx,int dy,int duration);
startScroll(int startX,int startY,int dx,int dy);

下面的代码实现了View跟随手指滑动,当手指离开屏幕时,View自动回到原位置的效果:

int lastX;
int lastY;
View parent = ((View)getParent());
@Override
public boolean onTouchEvent(MotionEvent event){
    int x = (int)event.getX();
    int y = (int)event.getY();
    switch(event.getAction){
        case MotionEvent.ACTION_DOWN:
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            int offsetX = x - lastX;
            int offsetY = y - lastY;
            .scrollBy(-offsetX,-offsetY);
            break;
        case MotionEvent.ACTION_UP:
            mScroller.startScroll(
                parent.getScrollX(),
                parent.getScrollY(),
                -parent.getScrollX(),
                -parent.getScrollY());
            invalidate();
            break;
    }
}
6.属性动画

使用属性动画也能完成对View的移动

7.ViewDragHelper

通过ViewDragHelper可以实现各种不同的滑动、拖放需求。通过实现类似QQ侧滑栏的布局来了解一下ViewDragHelper
首先初始化ViewDragHelper:

mViewDragHelper = ViewDragHelper.create(this,mCallback);

第一个参数是要监听的View,通常是一个ViewGroup,即ParentView,第二个参数是一个Callback回调,这个回调是整个ViewDragHelper的逻辑核心。
然后还要重写事件拦截方法,将事件传递给ViewDragHelper来处理:

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

@Override
public boolean onTouchEvent(MotoinEvent event){
    mViewDragHelper.processTouchEvent(event);
    return true;
}

处理computeScroll()

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

处理callback回调

private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback(){
    @Override
    public boolean tryCaptureView(View child,int pointerId){

        return mMainView == child;
    }
}

通过这个方法我们可以指定创建ViewDragHelper时,参数parentView中哪一个子view可以被移动,在这个实例中自定义了一个ViewGroup,里面有两个子view——mMenuView和mMainView,当指定了上述代码时,只有mMainView是可以被拖动的。

然后处理具体的滑动方法——clampViewPositionVertical()clampViewPositionHorizontal(),分别对应垂直和水平滑动。必须重写这两个方法实现滑动。它们的默认返回值是0,即不发生滑动,只重写其中一个,就会实现对应方向上的滑动。代码如下所示:

@Override
public int clampViewPositionVertical(View child,int top,int dy){
    return top;
}

@Override
public int clampViewPositionHorizontal(View child,int left,int dx){
    return left;
}

参数top和left分别代表在对应方向上的child移动的距离,dy、dx则表示相对于前一次的增量。通常情况下只需要返回top和left即可,但当需要更加精确地计算padding等属性时,就需要针对left和top进行一些处理,并返回合适的值。
下面是实现水平滑动的代码:

private ViewDragHelpe.Callback mCallback = new ViewDragHelper.Callback(){
    @Override
    public boolean tryCaptureView(View child,int pointerId){
        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;
    }
}

下面实现手指离开屏幕后View返回原来位置的效果:
在ViewDragHelper.Callback 中重写onViewReleased方法

@Override
public void onViewReleased(View releasedChild,float xvel,float yvel){
    super,onViewReleased(releasedChild,xvel,yvel);
    if(mMainView.getLeft() < mWidth / 3){
        mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
    }else{
        mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
    }
}

上述代码让MainView移动后左边距小于其三分之一时回到原位置,否则就滑动到300的位置。
下面是自定义DragViewGroup的完整代码:

package com.chaoyang805.chapter5scrolldemo.view;

import android.content.Context;
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 chaoyang805 on 2015/12/16. */
public class DragViewGroup extends FrameLayout {

    private ViewDragHelper mHelper;
    private View mMenuView;
    private View mMainView;
    private int mWidth;
    public DragViewGroup(Context context) {
        this(context, null);
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init() {
        mHelper = ViewDragHelper.create(this,mCallback);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mHelper.processTouchEvent(event);
        return true;
    }

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

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

    private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mMainView == child;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if (left < 0 && dx < 0) {
                return 0;
            }
            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() < mWidth / 3) {
                mHelper.smoothSlideViewTo(mMainView, 0, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            } else {
                mHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };
}

ViewDragHelper.Callback中其他事件介绍:

//这个事件在用户触摸到View后回调
onViewCaptured(View capturedChild,int activePointerId);
//这个事件在拖拽状态改变时的回调,比如idle、dragging等
onViewDragStateChanged(int state);
//这个事件在位置改变时回调,常用于滑动时更改scale进行缩放等效果
onViewPositionChanged(View changedView,int left,int top,int dx,int dy);

你可能感兴趣的:(android,读书笔记)