Android Tech And Thoughts.

View常见滑动方式与比较

Word count: 2.6kReading time: 10 min
2019/12/09 Share

常见滑动方式

掌握滑动的方法是实现自定义 View 的基础,常见的实现滑动的方式有三种

  • ScrollTo/SrollBy
  • 动画
  • 改变View的LayoutParams使得View重新布局从而实现滑动

使用ScrollTo/ScrollBy

1
public void scrollTo(int x,int y)

x: 滑动到的距离,这个距离指的是相对于初始位置(并非上一次的位置,而是View内容最初的位置,最初是0)

y: 同上

View内部会有两个变量来记录当前内容的位置 : mScrollX 和 mScrollY ,向左或者向上为正,反之为负

1
public void scrollBy(int x,int y)

x: 相对于 oldX 的偏移量,向左为正,反之为负

y:同上

实际上 scrollBy 也是通过 scrollTo 来实现的,将 偏移量x与oldX相加即可得到绝对值

Scroller

为什么把 scroller 拿出来一起说呢,因为其实吧,scroller 也是通过 scrollTo 来实现的,不过它具有弹性的效果。其实学过微积分的都很容易理解,无非就是将一个滑动过程分解为时间间隔较小的scrollTo。由此,我们也能很容易地联想到,使用最基本地属性动画(单纯地将 duration 分解,再在update里面调用scrollTo即可)也可以实现这样的效果。

如何使用Scroller?

1
public class Scroller{
2
    ...
3
    private int mStartX;
4
    private int mStartY;
5
    private int mFInalX;
6
   	private int mFinalY;
7
    ...
8
                                          
9
    public void startScroll(int startX, int startY, int dx, int dy) {
10
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
11
    }
12
                    
13
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
14
        mMode = SCROLL_MODE;
15
        mFinished = false;
16
        mDuration = duration;
17
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
18
        mStartX = startX;
19
        mStartY = startY;
20
        mFinalX = startX + dx;
21
        mFinalY = startY + dy;
22
        mDeltaX = dx;
23
        mDeltaY = dy;
24
        mDurationReciprocal = 1.0f / (float) mDuration;
25
    }
26
    
27
    // 计算下一次滑动到的 x 和 y                
28
    public boolean computeScrollOffset() {
29
    
30
    }
31
}

要记住一点,Scroller 是不会产生滑动的逻辑的,它的作用是记录这个滑动的起始状态,以及提供一个计算当前滑动分段的函数 computeScrollOffset 函数。

由此,我们可以知道 ,要想实现这个弹性滑动,需要一个触发滑动的动作,以及不断产生下一次滑动的动作。这就引出了一个很重要的函数:View 的 computeScroll() 方法,在 View中这是一个空实现,在每次 draw 的时候,draw 方法都会调用 computeScroll 方法(我们可以在这里触发下一次绘制,而下一次绘制又会调用这个方法,如此循环往复直到滑动结束)。上面的机制满足了我们实现这个滑动过程的两个因素。

1
public MView extends View{
2
    
3
    Scroller mScroller = new Scroller();                                     
4
    
5
    //我们自定义的 smoothScrollTo 方法 
6
    public void smoothScrollTo(int destX,int destY){
7
    	int scrollX = getScrollX();
8
    	int deltaX = destX - scrollX;
9
    	// startScroll 方法负责记录滑动的起始状态信息,以及duration,以便计算每一个细分滑动的状态
10
    	mScroller.startScroll(scrollX,0,deltaX,0,1000);
11
    	// 触发第一次滑动
12
    	invalidate();
13
    }
14
             
15
    // 重写 computeScroll 方法,计算下一次滑动的位置信息,并调用 invalidate触发下一次滑动                
16
    @Override
17
    public void computeScroll(){
18
      // 判断滑动是否结束,没有的话,计算出下一次的位置信息
19
    	if(mScroller.computeScrollOffSet()){
20
    			scrollTo(mScroller.getCurX(),mScroller.getCurY());
21
    			// 触发下一次重绘
22
    			postInvalidate();
23
      }
24
    }
25
   
26
}

除了上面提到的 Scroller 和 属性动画之外,我们也可以通过 postDelayed 或者 sleep 等延时策略,其基本思想都是一致的,即弹性滑动分解。

动画:

1
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(1000).start();

tranbslationX: 指的是View的偏移量,而不是View内容的偏移量

View还有 x,y 参数,其中 x 表示 View 左上角的横坐标 (不一定扽古语left,left表示初始坐标), x = left + tranlastionX;要注意区别 x 和 left ,以及 tranlastionX 与 scrollX

另外,有一个特殊的 属性动画,我们可以用它来做一些连续性的动作:

1
ValueAnimator animator = ValueAnimator,ofInt(0,1).setDuration(1000);
2
animator.addUpdateListener(new AnimatorUpdateListener(){
3
    @Override
4
    public void onAnimationUpdate(ValueAnimator animator){
5
    	float fraction = animator.getAnimatedFraction();
6
    	mButton1.scrollTo(startX+(int)(deltaX*fraction),0);
7
    }                
8
});
9
animator.start();

ViewDragHelper

2013 Google I/O 大会上介绍了两个新的 Layout : SlidingPanelLayout 和 DrawerLayout ,现在这两个类都被广泛地运用,其实研究他们的源码你会发现这两个类都运用了 ViewDragHelper 来处理拖动。ViewDragHelper是 framework中不为人知却非常有用的一个工具

ViewDragHelper 解决了Android 手势处理过于复杂的问题。在DrawLayout出来之前,侧滑菜单都是由第三方开源代码来实现的,其中最著名的是 MenuDrawer ,MenuDrawer 重写了 onTouchEvent 方法来实现侧滑效果,代码量很大,实现逻辑也需要很大耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进

ViewDrapHelper基本用法

ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.

– Android Developer

官方文档之指出,ViewDragHelper 是一个工具类,通常用于自定义 ViewGroup 之中,为拖拽和重新布局其子 View 提供帮助。

其实ViewDragHelper 并不是第一个用于分析手势处理的类, GestureDetector 也是,但是在拖动相关的手势分析方面,GestureDetector 只能说是勉为其难。

ViewdragHelper 其本质也是分析 onInterceptTouchEvent 和 onTouchEvent 的 MotionEvent 参数,然后根据分析的结果去改变一个容器中被拖动子 View 的位置(通过 offsetTopAndBottom 和 offsetLeftANdRight 方法)。


初始化

ViewDragHelper 一般用在一个自定义 ViewGroup 的内部,内部有一个 View mDragView 作为成员变量

1
public class DragLayout extends LinearLayout{
2
	private final ViewDragHelper mDragHelper;
3
	private View mDragView;
4
	
5
	public DragLayout(Context context){this(context,null);}
6
	
7
	public DragLayout(Context context,AttributeSet attrs){
8
		super(context,attr);
9
		//1.0f是敏感度参数,越大越敏感
10
		mDragHelper = ViewDragHelper.create(this,1.0f,new DragHelperCallback());
11
	}
12
}

要想让 ViewDragHelper 能够处理拖动需要将其触摸事件传递给它,这点和 GestureDetector一样。

1
@Override
2
public boolean onInterceptTouchEvent(MotionEvent ev){
3
	final int action = MotionEventCompat.getActionMasked(ev);
4
	
5
	if(action==MotionEvent.ACTION_CANCEL || action==MotionEvent.ACTION_UP){
6
		mDragHelper.cancel();
7
		return false;
8
	}
9
	
10
	// 这里有点疑问,通常 ,DragHelper 不拦截,不代表 ViewGroup 就不拦截啊。
11
	return mDragHelper.shoudlInterceptTouchEvent(ev);
12
}
13
14
@Override
15
public boolean onTouchEvent(MotionEvent ev){
16
	mDragHelper.processTouchEvent(ev);
17
	
18
	// 我觉得,也可以对 ev 进一步处理吧,毕竟ViewGroup也不仅仅是处理拖拽嘛
19
	return true;
20
}

接下来,你就可以在回调中处理各种拖动行为了

拖动处理

坐标

1.处理横向的拖动
第二个参数 left 指当前拖动子 View 应该到达的 x 坐标。所以按照常理,这个方法原封返回 left 就可以了。不过这里我们多加一些处理,子View遇到边界之后不再拖动。

1
public int clampViewPositionHorizontal(View child,int left,int dx){
2
	final int leftBound = getPaddingLeft();
3
  final int rightBound = getWidth() - mDragView.getWidth();
4
  final int newLeft = Math.min(Math.max(left,leftBound),rightBound);
5
  
6
  return newLeft;
7
}

2.处理纵向的滑动
同上,具体代码我就不贴了

注意
这两个方法都要重写,比如你只重写了横向滑动相关的,这并不代表它纵向上就不滑动了,我一开始也是这么以为的,结果子View一滑动就跳到parent的顶部,纠结了很久。后来发现,这两个方法都有默认的返回值:0,所以才会出现上面的情况。
如果你不想纵向滑动,那就计算下原始的top值,并返回这个top值就可以了,它就不会纵向滑动了。

3.tryCaptureView函数关联子View [determin which view can drag]
tryeCaptureView也是定义在callback中的函数,它会决定当前的这个drag可否生效

1
mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
2
		.....
3
4
   // 判断当前View是否可以拖拽
5
   @Override
6
   public boolean tryCaptureView(@NonNull View child, int pointerId) {
7
        return true;
8
   }
9
});

child表示当前 motionevent作用的子View,我们用来判断是否它可以被拖拽。但实际上我们并不知道这个child实际上是谁,所以通常的做法是设计一个 API,类似

1
public void setDragable(View child);

提供给用户,我们来试一下:

1
// 判断当前View是否可以拖拽
2
@Override
3
public boolean tryCaptureView(@NonNull View child, int pointerId) {
4
     //return true;
5
     return child.getTag()!=null;
6
}
7
8
//-----api------
9
public void setDragable(View child){                   
10
    child.setTag(new Object());               
11
}

然后在MainActivity里面通过 dragLayout.setDragable(view)就可以设置可滑动了。

滑动边缘

滑动边缘也同样分为滑动左边缘或者右边缘(EDGE_LEFT 和 EDGE_RIGHT)。可能你会有所疑问,到底是滑动 ViewGroup 的边缘还是滑动子 View 的边缘啊,注意哈,本文中的滑动拖拽都是站在 ViewGroup 的角度来说的。

例如打开左边缘滑动开关:

1
mDragHelper.setEgdeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

Callback中监听边缘滑动有两个相关的方法

onEdgeTouched:

1
// Called when one of the subscribed edges in the parent view has been touched by the user while no child view is currently captured.
2
// edgeFlags: 表示当前哪个edge触摸了,edgeFlags也可能表示有几个edge 被触摸了(复合值)
3
// pointerId:
4
public void onEdgeTouched(int edgeFlags,int pointerId);

关于 edgeFlags, 在 ViewDragHelper 中定义了下面:

1
public static final int EDGE_LEFT = 1 << 0;
2
public static final int EDGE_RIGHT = 1 << 1;
3
public static final int EDGE_TOP = 1 << 2;
4
public static final int EDGE_BOTTOM = 1 << 3;

onEdgeDragStarted:
当边缘拖动开始时调用

1
@Override
2
public void onEdgeDragStarted(int edgeFlags,int pointerId){
3
     // do sth             
4
}

Useful APIs

这个方法决定是否捕获当前正在 drag 区域的 childView,这个是一个内部调用

1
public boolean tryCaptureView(@NonNull child: View, pointerId: Int)

这个方法则可以手动指定要移动的 childView,要注意其与 tryCaptureView 的区别,这个通常可以手动调用

1
public void captureChildView(@NonNull View childView, int activePointerId)

在拖拽结束的时候回调,利用这个方法,比如我们可以实现拖拽结束后回到原位置等骚操作(一般用弹性滑动来处理)

1
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel)

Thanks:

  1. AndroidDeveloper/ViewDragHelper
  2. ViewDragHelper详解
  3. ViewDragHelper侧滑栏
  4. Android ViewDragHelper源码解析
CATALOG
  1. 1. 常见滑动方式
    1. 1.1. 使用ScrollTo/ScrollBy
    2. 1.2. 动画:
  2. 2. ViewDragHelper
    1. 2.1. ViewDrapHelper基本用法
      1. 2.1.0.1. 初始化
      2. 2.1.0.2. 拖动处理
      3. 2.1.0.3. 滑动边缘
      4. 2.1.0.4. Useful APIs
  3. 2.2. Thanks: