前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android ViewDragHelper及移动处理总结

Android ViewDragHelper及移动处理总结

作者头像
xiangzhihong
发布2018-02-05 21:10:13
1.5K0
发布2018-02-05 21:10:13
举报
文章被收录于专栏:向治洪

概述

2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用。我们知道在我们实际的开发中往往会涉及到很多的拖动效果,而ViewDragHelper解决了android中手势处理过于复杂的问题。

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

为了方便大家的理解,我们首先来看一下android View对移动事件的处理。

View移动方法总结

layout

在自定义控件中,View绘制的一个重写方法layout(),用来设置显示的位置。所以,可以通过修改View的坐标值来改变view在父View的位置,以此可以达到移动的效果!但是缺点是只能移动指定的View,如常见的:

代码语言:javascript
复制
view.layout(l,t,r,b);

offsetLeftAndRight /offsetTopAndBottom

非常方便的封装方法,只需提供水平、垂直方向上的偏移量,展示效果与layout()方法相同。

代码语言:javascript
复制
view.offsetLeftAndRight(offset);//同时改变left和right  view.offsetTopAndBottom(offset);//同时改变top和bottom

LayoutParams

此类保存了一个View的布局参数,可通过LayoutParams动态改变一个布局的位置参数,以此动态地修改布局,达到View位置移动的效果!但是在获取getLayoutParams()时,要根据该子View对应的父View布局来决定自身的LayoutParams 。所以一切的前提是:必须要有一个父View,否则无法获取LayoutParams。

代码语言:javascript
复制
LinearLayout.LayoutParamslayoutParams = (LinearLayout.LayoutParams)getLayoutParams(); 
layoutParams.leftMargin = getLeft() + dx; layoutParams.topMargin = getTop() + dy; setLayoutParams(layoutParams); 

scrollTo/scrollBy

通过改变scrollX和scrollY来移动,但是可以移动所有的子View。scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量为dx,dy。

注意:这里使用scrollBy(xOffset,yOffset);,你会发现并没有效果,因为以上两个方法移动的是View的content。若在ViewGroup中使用,移动的是所有子View;若在View中使用,移动的是View的内容(比如TextView)。所以,不可在view中使用以上方法!

要想使用scrollBy,应该在View所在的ViewGroup中使用:

代码语言:javascript
复制
((View)getParent()).scrollBy(offsetX, offsetY); 

canvas

通过改变Canvas绘制的位置来移动View的内容,用的少,一般用在自定义的View中,比如老早之前实现手写板:

代码语言:javascript
复制
canvas.drawBitmap(bitmap, left, top, paint)

说完View的移动相关的属性,我们来看一下大名鼎鼎的ViewDragHelper。

ViewDragHelper

要理解ViewDragHelper,我们需要掌握以下几点:

  1. ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁;
  2. ViewDragHelper的实例是通过静态工厂方法创建的;
  3. ViewDragHelper可以检测到是否触及到边缘;
  4. ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中实现;
  5. ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置。

ViewDragHelper使用

  1. ViewDragHelper的初始化 ViewDragHelper一般用在一个自定义ViewGroup的内部,比如下面自定义了一个继承于LinearLayout的DragLayout,DragLayout内部有一个子view mDragView作为成员变量:
代码语言:javascript
复制
public class DragLayout extends LinearLayout {
private final ViewDragHelper mDragHelper;
private View mDragView;
public DragLayout(Context context) {
  this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
}

创建一个带有回调接口的ViewDragHelper。

代码语言:javascript
复制
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}

说明:其中其二个参数是敏感度,参数参数越大越敏感。

然后ViewDragHelper将触摸事件传递给ViewDragHelper进行处理。如:

代码语言:javascript
复制
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  final int action = MotionEventCompat.getActionMasked(ev);
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      mDragHelper.cancel();
      return false;
  }
  return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
  mDragHelper.processTouchEvent(ev);
  return true;
}
  1. 拖动行为处理 在DragHelperCallback的回调方法中有很多的方法可以检测View的事件,如常见的clampViewPositionHorizontal、clampViewPositionVertical,并且clampViewPositionHorizontal 和 clampViewPositionVertical必须要重写,因为默认它返回的是0。 来看clampViewPositionHorizontal的处理。 在DragHelperCallback中实现clampViewPositionHorizontal方法, 并且返回一个适当的数值就能实现横向拖动效果。
代码语言:javascript
复制
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
  Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
  final int leftBound = getPaddingLeft();
  final int rightBound = getWidth() - mDragView.getWidth();
  final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
  return newLeft;
}
  1. 其他事件处理

滑动边缘事件检测

分为滑动左边缘还是右边缘:EDGE_LEFT和EDGE_RIGHT,下面的代码设置了可以处理滑动左边缘:

代码语言:javascript
复制
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

如上,我们设置为左边缘检测,当onEdgeTouched方法会在左边缘滑动的时候被调用,这种情况下一般都是没有和子view接触的情况。

代码语言:javascript
复制
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
    super.onEdgeTouched(edgeFlags, pointerId);
    Toast.makeText(getContext(), "edgeTouched", Toast.LENGTH_SHORT).show();
}

如果你想在边缘滑动的时候根据滑动距离移动一个子view,可以通过实现onEdgeDragStarted方法,并在onEdgeDragStarted方法中手动指定要移动的子View,如之前仿音悦台的页面交互就用到了子View的检测。

ViewDragHelper实战

其实就之前是的的仿音悦台的页面交互效果吧,在13年就有国外的大神实现了https://github.com/flavienlaurent/flavienlaurent.com

我们来看一段完整的代码:

代码语言:javascript
复制
public class YoutubeLayout extends ViewGroup {
private final ViewDragHelper mDragHelper;
private View mHeaderView;
private View mDescView;
private float mInitialMotionX;
private float mInitialMotionY;
private int mDragRange;
private int mTop;
private float mDragOffset;
public YoutubeLayout(Context context) {
  this(context, null);
}
public YoutubeLayout(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
    mHeaderView = findViewById(R.id.viewHeader);
    mDescView = findViewById(R.id.viewDesc);
}
public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
public void maximize() {
    smoothSlideTo(0f);
}
boolean smoothSlideTo(float slideOffset) {
    final int topBound = getPaddingTop();
    int y = (int) (topBound + slideOffset * mDragRange);
    if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
        ViewCompat.postInvalidateOnAnimation(this);
        return true;
    }
    return false;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
  @Override
  public boolean tryCaptureView(View child, int pointerId) {
        return child == mHeaderView;
  }
    @Override
  public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
      mTop = top;
      mDragOffset = (float) top / mDragRange;
        mHeaderView.setPivotX(mHeaderView.getWidth());
        mHeaderView.setPivotY(mHeaderView.getHeight());
        mHeaderView.setScaleX(1 - mDragOffset / 2);
        mHeaderView.setScaleY(1 - mDragOffset / 2);
        mDescView.setAlpha(1 - mDragOffset);
        requestLayout();
  }
  @Override
  public void onViewReleased(View releasedChild, float xvel, float yvel) {
      int top = getPaddingTop();
      if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {
          top += mDragRange;
      }
      mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
  }
  @Override
  public int getViewVerticalDragRange(View child) {
      return mDragRange;
  }
  @Override
  public int clampViewPositionVertical(View child, int top, int dy) {
      final int topBound = getPaddingTop();
      final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
      final int newTop = Math.min(Math.max(top, topBound), bottomBound);
      return newTop;
  }
}
@Override
public void computeScroll() {
  if (mDragHelper.continueSettling(true)) {
      ViewCompat.postInvalidateOnAnimation(this);
  }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  final int action = MotionEventCompat.getActionMasked(ev);
  if (( action != MotionEvent.ACTION_DOWN)) {
      mDragHelper.cancel();
      return super.onInterceptTouchEvent(ev);
  }
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      mDragHelper.cancel();
      return false;
  }
  final float x = ev.getX();
  final float y = ev.getY();
  boolean interceptTap = false;
  switch (action) {
      case MotionEvent.ACTION_DOWN: {
          mInitialMotionX = x;
          mInitialMotionY = y;
            interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
          break;
      }
      case MotionEvent.ACTION_MOVE: {
          final float adx = Math.abs(x - mInitialMotionX);
          final float ady = Math.abs(y - mInitialMotionY);
          final int slop = mDragHelper.getTouchSlop();
          if (ady > slop && adx > ady) {
              mDragHelper.cancel();
              return false;
          }
      }
  }
  return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
  mDragHelper.processTouchEvent(ev);
  final int action = ev.getAction();
    final float x = ev.getX();
    final float y = ev.getY();
    boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
    switch (action & MotionEventCompat.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN: {
          mInitialMotionX = x;
          mInitialMotionY = y;
          break;
      }
      case MotionEvent.ACTION_UP: {
          final float dx = x - mInitialMotionX;
          final float dy = y - mInitialMotionY;
          final int slop = mDragHelper.getTouchSlop();
          if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {
              if (mDragOffset == 0) {
                  smoothSlideTo(1f);
              } else {
                  smoothSlideTo(0f);
              }
          }
          break;
      }
  }
  return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
}
private boolean isViewHit(View view, int x, int y) {
    int[] viewLocation = new int[2];
    view.getLocationOnScreen(viewLocation);
    int[] parentLocation = new int[2];
    this.getLocationOnScreen(parentLocation);
    int screenX = parentLocation[0] + x;
    int screenY = parentLocation[1] + y;
    return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
            screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
    int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
            resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  mDragRange = getHeight() - mHeaderView.getHeight();
    mHeaderView.layout(
            0,
            mTop,
            r,
            mTop + mHeaderView.getMeasuredHeight());
    mDescView.layout(
            0,
            mTop + mHeaderView.getMeasuredHeight(),
            r,
            mTop  + b);
}

页面引用xml

代码语言:javascript
复制
<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:tag="list"
            />
    <com.example.vdh.YoutubeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/youtubeLayout"
            android:orientation="vertical"
            android:visibility="visible">
        <TextView
                android:id="@+id/viewHeader"
                android:layout_width="match_parent"
                android:layout_height="128dp"
                android:fontFamily="sans-serif-thin"
                android:textSize="25sp"
                android:tag="text"
                android:gravity="center"
                android:textColor="@android:color/white"
                android:background="#AD78CC"/>
        <TextView
                android:id="@+id/viewDesc"
                android:tag="desc"
                android:textSize="35sp"
                android:gravity="center"
                android:text="Loreum Loreum"
                android:textColor="@android:color/white"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#FF00FF"/>
    </com.example.vdh.YoutubeLayout>
</FrameLayout>

其实就是两个子2View,ViewDragHelper的事件检测,然后回调里面的方法 进行页面的Onlayout,进而控制页面刷新等等。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-01-05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • View移动方法总结
    • layout
      • offsetLeftAndRight /offsetTopAndBottom
        • LayoutParams
          • scrollTo/scrollBy
            • canvas
            • ViewDragHelper
              • ViewDragHelper使用
                • 滑动边缘事件检测
            • ViewDragHelper实战
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档