​友球要写一个跟纸片一样的能够翻转过来跟纸片一样的自定义View,所以我就去研究了一下View的Animation和滑动、拖拽。这篇就记录一下学习《Android群英传》里的滑动部分。

Key Points

滑动的实现:不断地改变View的坐标

Android坐标系: 左上角为原点,水平向右为X轴正方向,垂直向下为Y轴正方向

视图坐标系: 描述子视图在父视图中的位置关系,子视图的坐标以父视图左上角为原点

触控事件—MotionEvent

MotionEvent中封装的常用事件常量:

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

实现触摸实现的代码套路:在onTouchEvent(MotionEvent event)中通过event.getAction()获取触控事件类型,并且用switch-case语句来筛选我们所需要的动作。

触摸事件的代码套路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override 
public boolean onTouchEvent(MotionEvent event) {
  // 获取触摸事件坐标
   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.UP:
                // 处理离开
                break;
        }
        return true;
}

Android提供的获取一系列坐标的方法示意图:

PositionMethods

以上方法分成两类:

  • View提供的获取坐标方法
    1. getTop() : 获取View自身顶边到其父布局顶边的距离
    2. getLeft(): 获取View自身左边到其父布局左边距离
    3. getBottom(): 获取View自身的底边到父布局顶边的距离
    4. getRight(): 获取View自身的右边到父布局左边的距离
  • MotionEvent提供的方法
    1. getX(): 获取点击事件距离控件左边的距离,即视图坐标
    2. getY(): 获取点击事件距离控件顶边的距离,即视图坐标
    3. getRawX(): 获取点击事件距离整个屏幕左边的距离,即绝对坐标
    4. getX(): 获取点击事件距离整个屏幕顶边的距离,即绝对坐标

只要牢记坐标获取都是以左/顶边为参考的即可,配合图片理解就好了。

实现滑动的若干种方法

​接下来就用这些系统提供的API来实现一个动态的修改一个View的坐标,也就是实现滑动效果。不管哪种方式,实现的思路是一样的:

触摸View时,系统记录下此时坐标;手指一动时,记录下移动后的触摸点坐标,获取到偏移量,并通过偏移量来修改View坐标;不断重复,就实现了滑动

​首先,自定义一个DragView继承自View,重写Constructor方法,并且有两个私有成员准备用来记录上一次的坐标。

1
2
3
4
5
6
7
8
public class DragView extends View {
    private int lastX = 0;
    private int lastY = 0;
  
    public DragView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
}

​写一个简单的布局,把我们的DragView设成100*100,并且让他填上背景色方便观察。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="top.tobiaslee.viewtest.ScrollActivity">
    <top.tobiaslee.viewtest.DragView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@color/colorAccent"/>
</LinearLayout>

​接下来,照搬套路。我们重写onTouchEvent(MotionEvent event)这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override 
public boolean onTouchEvent(MotionEvent event) {
  // 获取触摸事件坐标
   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.UP:
                // 处理离开
                break;
        }
        return true;
}

法1. layout方法

View进行绘制的时候,会调用onLayout()方法来设置显示的位置,我们可以在ACTION_MOV事件中计算偏移量,然后在layout中增加偏移量就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case MotionEvent.ACTION_DOWN:
   // 记录触摸点坐标
   lastX = x;
    lastY = y; 
   break;
case MotionEvent.ACTION_MOVE:
   // 计算偏移量
 int offsetX = x - lastX;
 int offsetY = y - lastY;
 // 调用layout() 在四个方向上增加偏移量
    layout(getLeft() + offsetX, 
           getTop() + offsetY, 
           getRight() + offsetX, 
           getBottom() + offsetY);
  break;

这里是通过视图坐标来计算偏移量的,我们同样可以通过绝对坐标来计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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;
}

​需要注意的是,因为通过绝对坐标计算偏移量,我们要及时更新上一个触摸点的坐标,所以在ACTION_MOVE方法中也要记得更新lastXlastY,否则就会出现偏移量不准确的情况。

法2. offsetLeftAndRight()与offsetTopAndBottom()

这两个方法就是系统提供的一个对左右、上下移动的API封装,计算出偏移量后,直接调用即可。

1
2
3
4
// 左右移动
offsetLeftAndRight(offsetX);
// 上下移动
offsetTopAndBottom(offsetY);

法3. LayoutParams

LayoutParams保存了View的布局参数,我们可以通过改变LayoutParams的参数来动态改变View的位置参数,从而实现View的移动。我们可以通过getLayoutParams()获取到View的LayoutParams,然后通过setLayoutParams()来改变其参数。

1
2
3
4
5
6
7
8
9
10
11
// 通过layoutParams来实现移动
// 通过父布局
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) 
                        getLayoutParams();
// 通过MarginLayout来实现
ViewGroup.MarginLayoutParams layoutParams =(ViewGroup.MarginLayoutParams)
                          getLayoutParams();

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

这里有两种方法获取LayoutParams。

一种根据父布局获取对应的LayoutParams,这里我们用的是LinearLayout,所以对应的也就要使用这种类型的布局参数,如果是父布局RelativeLayout,那就是RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();。但要注意,如果没有父布局,就无法获取LayoutParams

还有一种是通过ViewGroup.MarginLayoutParams来实现,因为我们实现View的移动,通常是改变这个View的Margin属性。这种实现就不用考虑父布局,更加方便。

法4. scrollTo和scrollBy

在一个View中,系统提供了scrollTo()scrollBy()来改变一个View位置。

从字面上就可以看出scrollBy()移动一个增量(dx, dy),scrollTo()移动到指定的坐标位置(x, y)

但是要注意一点,这两个方法移动的是View的content,也就是让View的内容移动,比如一个TextView,content就是它的文本;ImageView,就是它的drawable对象。

所以如果我们要移动这个DrawView,就要移动它的父View

1
((View)getParent()).scrollBy(offsetX, offsetY)

但是这样的实现,我们会发现拖动时候,View的移动是反向的!

Why? 因为我们移动的是ViewGroup,而计算的偏移量是通过子View来计算的。

假设我们希望View移动(20,10),那么上面语句执行结果就是让父布局向右下方移动了(20, 10),对于View来说就相当于移动了(-20, -10),他们的移动是相反的。

所以这个和在显微镜下移动载玻片相类似,需要向相反的方向移动。

也就是:

1
((View)getParent()).scrollBy(-offsetX, -offsetY)

法5. Scroller

我们之前通过scrollBy()方法,通过不断微分移动的距离,切割成N个小片段,然后瞬间移动一个小片段,实现了滑动,但未免有些不够流畅;而Scroller是用来实现平滑移动效果的一个类。

Scroller的使用,我们需要在添加一个私有成员mScroller,并且在初始化的时候创建一个scroller对象

1
2
3
4
public DragView(Context context, @Nullable AttributeSet attrs) {
       super(context, attrs);
       mScroller = new Scroller(context);
}

然后重写onComputeScroll()方法,实现模拟滑动,这是Scroller类的核心

1
2
3
4
5
6
7
8
9
10
11
12
    
@Override
public void computeScroll() {
 super.computeScroll();
   // 判断Scroller是否执行完毕
  if(mScroller.computeScrollOffset()) {
       ((View)getParent()).scrollTo(mScroller.getCurrX(),
                                    mScroller.getCurrY());
        // 通过重绘 不断调用computeScroll
        invalidate();
 }
}

Scroller类提供了computeScrollOffset()来判断滑动是否完成,也提供了getCurrX()getCurrY()来获取当前的滑动坐标。

需要注意的是,computeScroll()方法是不会自动的调用的,但我们只能在这里获取currX和currY,所以只能通过invalidate()->draw()->computeScroll()来间接调用,从而不断获得坐标

接下来就是要启动Scroller了

1
2
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启动有两个方法,区别就在于一个指定了时间长短,一个没有。(startX, startY)起始坐标,(dx, dy)就是坐标偏移量。

然后我们在ACTION_UP中启动,让View滑动回原来的位置:

1
2
3
4
5
6
7
8
9
10
case MotionEvent.ACTION_UP:
 View viewGroup = (View)getParent();
   // 滑动回原位置
    mScroller.startScroll(viewGroup.getScrollX(),
                          viewGroup.getScrollY(),
                          -viewGroup.getScrollX(),
                          -viewGroup.getScrollY());
 // 通知重绘
  invalidate();
 break;

法6.7 属性动画和ViewDragHelper

限于篇幅,就不放在这里讲了。

效果

最后我们看看实现的效果:

scroll

Categories:

Updated: