Android Scroll分析
1.滑动原理
滑动一个View,本质上就是移动一个View,通过不断改变View的坐标来实现。
一般监听用户触摸事件,根据传入坐标,动态且不断的改变View的坐标,实现滑动。
2.Android坐标系
Android坐标系描述的是手机屏幕的位置
(0,0)
.-----------------------------► X轴
|
|
|
|
|
|
|
|
| Android手机屏幕
|
|
|
|
|
|
▼
Y轴
- 以屏幕左上角为原点(0,0)向右为X轴正方向,向下为Y轴正方向。
- 系统提供getLocationOnScreen(int[] location)方法获取View的左上角点在Android坐标系中的坐标
- 在触摸事件中getRawX(),getRawY()获取的也是Android坐标系中的坐标
3.视图坐标系
视图坐标系描述的是子View在父View中的位置关系
(0,0)
.-----------------------------► X轴
|
|
| Android手机屏幕
| (0,0)
| .-----------► X轴
| |
| | 父View
| |
| | --------
| | | View |
| | --------
| ▼
| Y轴
|
|
|
▼
Y轴
- 原点是父View的左上角
- 在触摸事件中getX(),getY()获取的就是视图坐标系中的坐标
4.触摸事件–MotionEvent
- 事件常量
ACTION_DOWN = 0 单点触摸按下动作
ACTION_UP = 1 单点触摸离开动作
ACTION_MOVE = 2 触摸点移动动作
ACTION_CANCEL = 3 触摸动作取消
ACTION_OUTSIDE = 4 触摸动作超出边界
ACTION_POINTER_DOWN = 5 多点触摸按下
ACTION_POINTER_UP = 6 多点触摸离开
- 通常会在onTouchEvent(MotionEvent event)函数中调用event.getAction获取事件类型
- 通过event.getX()、event.getY()来获取触摸点在视图坐标的位置
- 通过event.getRawX()、event.getRawY()来获取触摸点Android坐标系的位置
5.实现滑动的方法
假设我们现在有这样一个布局
ViewGroup -- View
我们要实现View的滑动,有以下几个方法
- 在View中通过layout方法重新设置坐标
- View在进行绘制之前会调用onLayout方法来设置在ViewGroup中的位置
- 同理通过layout方法修改View的left、top、right、bottom属性来控制View在ViewGroup中位置
private int lastX, lastY;
@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;
}
return true;
}
- 在View中通过offsetLeftAndRight、offsetTopAndBottom方法进行移动
- 这2个方法相当于系统提供对左右、上下移动的API封装
private int lastX, lastY;
@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;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
- 在View中通过改变LayoutParams进行移动
- LayoutParams保存了一个View的布局参数
- 通过动态修改margin值来进行移动
- 当然,这个方法只有View在对margin有处理的ViewGroup下有效
private int lastX, lastY;
@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;
if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
Log.i(TAG, "onTouchEvent: ACTION_MOVE isMarginLayoutParams = true");
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
}
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
- 在ViewGroup中通过scrollTo、scrollBy方法进行滑动
- scrollTo(x,y)表示移动到点坐标(x,y)
- scrollBy(offsetX,offsetY)表示当前坐标偏移offsetX,offsetY
- 移动的是ViewGroup中的所有子View
- 注意!移动的是Android坐标系,并不是View的坐标
private int lastX, lastY;
@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;
View parent = (View) getParent();
//与之前不同,offsetX和offsetY取负值
//因为之前的移动都是改变View在Android坐标系中的坐标(相当于View在坐标系中移动了)
//而scrollTo、scrollBy改变的Android坐标系原点的坐标(相当于坐标系移动了,所以要取负值)
parent.scrollBy(-offsetX, -offsetY);
lastX = rawX;
lastY = rawY;
break;
return true;
}
- 扩展:Scroller类
- 实现效果:View滑动后,自动平滑的回到初始位置
- 上面方法1–>方法4的滑动都是瞬间完成的
- Scroller类模拟滑动效果,使得滑动不那么突兀
/**
* Step1: 初始化Scroller类
*
* @param context
*/
private void initScroller(Context context) {
mScroller = new Scroller(context);
}
private int lastX, lastY;
@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;
View parent = (View) getParent();
//与之前不同,offsetX和offsetY取负值
//因为之前的移动都是改变View在Android坐标系中的坐标(相当于View在坐标系中移动了)
//而scrollTo、scrollBy改变的Android坐标系原点的坐标(相当于坐标系移动了,所以要取负值)
parent.scrollBy(-offsetX, -offsetY);
lastX = rawX;
lastY = rawY;
break;
}
case MotionEvent.ACTION_UP: {
View parent = (View) getParent();
//Step3: 开始模拟滑动过程
mScroller.startScroll(parent.getScrollX(),
parent.getScrollY(),
-parent.getScrollX(),
-parent.getScrollY());
invalidate();
break;
}
}
return true;
}
/**
* Step2: 重写computeScroll方法,实现模拟滑动
*/
@Override
public void computeScroll() {
super.computeScroll();
//computeScrollOffset判断滑动动画是否执行完毕,true 代表动画进行中,false 代表动画结束了
if (mScroller.computeScrollOffset()) {
View parent = (View) getParent();
parent.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 通过重绘函数,不断调用computeScroll实现滑动效果
invalidate();
}
}
- 属性动画
- 待续….
- ViewDragHelper
- support v4包中的一个辅助类,用于在父视图中拖动视图
- ViewDragHelper要写在ViewGroup里面,因为要重写onTouchEvent、onInterceptTouchEvent方法
public class DragViewGroup extends FrameLayout {
public static final String TAG = DragViewGroup.class.getSimpleName();
private ViewDragHelper mViewDragHelper;
public DragViewGroup(@NonNull Context context) {
super(context);
initViewDragHelper();
}
public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initViewDragHelper();
}
public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initViewDragHelper();
}
private void initViewDragHelper() {
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
/**
* ACTION_DOWN事件发生在 child 时候回调
* 返回值决定了是否需要捕获这个 child
* 只有返回值等于true (即捕获了child) 才能回调下面其他函数
*/
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
Log.i(TAG, "tryCaptureView: ");
return true;
}
int initX, initY;
/**
* 捕获到 child 时候回调,这里记录下捕获到时候的坐标
*/
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
Log.i(TAG, "onViewCaptured: ");
initX = capturedChild.getLeft();
initY = capturedChild.getTop();
}
/**
* ACTION_UP事件发生时候回调
* 释放捕获的child
*/
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
Log.i(TAG, "onViewReleased: ");
//设置view回到初始位置,启动动画
//动画执行过程需要重写computeScroll方法,原理即Scroller类
mViewDragHelper.settleCapturedViewAt(initX, initY);
invalidate();
}
/**
* 修整 child 水平方向上的坐标,left 指 child 要移动到的坐标,dx 相对上次的偏移量
* 注意!只有被捕获到的child才会触发
*/
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
Log.i(TAG, "clampViewPositionHorizontal: ");
return left;
}
/**
* 修整 child 垂直方向上的坐标,top 指 child 要移动到的坐标,dy 相对上次的偏移量
* 注意!只有被捕获到的child才会触发
*/
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
Log.i(TAG, "clampViewPositionVertical: ");
return top;
}
/**
* 边缘被触摸时候回调
*/
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
Log.i(TAG, "onEdgeTouched: ");
}
/**
* 边缘拖拽开始时候回调
*/
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
Log.i(TAG, "onEdgeDragStarted: ");
//手动捕获边缘拖拽事件要触发滑动的view
mViewDragHelper.captureChildView(getChildAt(0), pointerId);
}
});
//设置被捕获的child view的共同边缘会触发回调边缘拖拽函数
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_TOP | ViewDragHelper.EDGE_RIGHT | ViewDragHelper.EDGE_BOTTOM);
}
/**
* 重写onInterceptTouchEvent方法
* 委托给ViewDragHelper进行事件拦截操作,只有拦截到了才能进行拖拽功能
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = mViewDragHelper.shouldInterceptTouchEvent(ev);
Log.i(TAG, "onInterceptTouchEvent: result=" + result);
return result;
}
/**
* 重写onTouchEvent方法
* 让ViewDragHelper处理触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "onTouchEvent: ACTION_UP");
break;
}
mViewDragHelper.processTouchEvent(event);
return true;
}
/**
* 重写computeScroll方法
*/
@Override
public void computeScroll() {
super.computeScroll();
Log.i(TAG, "computeScroll: " + mViewDragHelper.continueSettling(true));
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
}