参考资料
郭霖 Scroller完全解析
鸿洋 ViewDragHelper完全解析
鸿洋 ViewDragHelper实战 自己打造Drawerlayout
-目录
- 1)layout
- 2)offsetLeftAndRight() offsetTopAndBottom()
- 3)LayoutParams()
- 4)scrollTo() scrollBy()
- 5)Scroller
- 6)属性动画
- 7)ViewDragHelper
-实现滑动的7种方法
public class DragView extends View {
private static final String TAG = "DragView";
private int lastX, lastY;
private Scroller scroller;
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, 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);
//方法二
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
//方法三
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
// layoutParams.leftMargin = getLeft()+offsetX;
// layoutParams.topMargin = getTop()+offsetY;
// setLayoutParams(layoutParams);
//方法四
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
case MotionEvent.ACTION_UP:
View view = (View)getParent();
Log.i(TAG, "getScrollX: "+view.getScrollX());
Log.i(TAG, "getScrollY: "+view.getScrollY());
scroller.startScroll(view.getScrollX(),view.getScrollY(),-view.getScrollX(),-view.getScrollY());
invalidate();
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()){
Log.i(TAG, "getCurrX: "+scroller.getCurrX());
Log.i(TAG, "getCurrY: "+scroller.getCurrY());
((ViewGroup)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();
}
}
}
1) layout
2) offsetLeftAndRight() offsetTopAndBottom()
3) LayoutParams()
//使用MarginLayoutParams更加方便还不用考虑父布局是LinearLayout还是RelativeLayout
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft()+offsetX;
layoutParams.rightMargin = getRight()+offsetY;
setLayoutParams(layoutParams);
4) scrollTo() scrollBy()
任何一个控件都是可以滚动的,因为View类中有scrollTo()和scrollBy()两个方法,scrollBy()是让View相对于当前位置滚动某段距离,scrollTo()是让View相对于初始位置滚动某段距离。
scrollTo,scrollBy方法移动的是View的内容,如果ViewGroup中使用scrollTo,scrollBy,那么移动的将是所有子View。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
layout = (LinearLayout) findViewById(R.id.layout);
scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
scrollToBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.scrollTo(-60, -100); //注意此处是layout的scrollTo()
}
});
scrollByBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.scrollBy(-60, -100);//注意此处是layout的scrollBy()
}
});
}
下图中为什么scrollBy(-60, -100),按钮确是向手机坐标系的x和y轴正向移动呢?
答:可以想象屏幕是一个放大镜,而下面是一个巨大的画布,使用scrollBy方法,将layout向X轴负方向(左)平移60,向Y轴负方向(上)平移100,则layout内的子view相当于向X轴和Y轴的正方向上移动了。
5) Scroller
使用Scroller模仿ViewPager的例子
startScroll(int startX,int startY,int dx, int dy,int duration)
startScroll(int startX,int startY,int dx, int dy)
/**
* Created by 涂高峰 on 2017/6/21.
*/
public class ScrollerLayout extends ViewGroup {
private static final String TAG = "ScrollerLayout";
private Scroller mScroller;
private int mDownX,mMoveX;
private int leftBorder,rightBorder;
private int mTouchSlop;
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
//大于这个距离,系统认为是移动
mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i=0; i<count; i++){
View child = getChildAt(i);
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i=0; i<count; i++){
View child = getChildAt(i);
child.layout(i*child.getMeasuredWidth(), 0, (i+1)*child.getMeasuredWidth(), child.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount()-1).getRight();
Log.i(TAG, "leftBorder: "+leftBorder);
Log.i(TAG, "rightBorder: "+rightBorder);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDownX = x;
mMoveX = x;
break;
case MotionEvent.ACTION_MOVE:
//按下的坐标与当前移动坐标绝对值 大于 系统默认的移动距离
//拦截此移动事件,不向子view传递,进入自身的onTouchEvent
if (Math.abs(mDownX - x)>mTouchSlop){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//如果子控件为Button之类的clickable控件,则会由button消费掉down事件,当viewgroup滑动时,会拦截move事件并处理
//但是若子控件为TextView之类的非clickable控件,则viewgroup和textview都不会消费掉down事件.
//由于没有任何view消费down事件,后续事件将由上层消费,而不会往下传递给viewgroup.所以此处需要将down事件消费掉,从而能继续接收后续事件
return true;
case MotionEvent.ACTION_MOVE:
//偏移量
int offsetX = mMoveX-x;
//左边界处理
if (getScrollX()+offsetX < leftBorder){
scrollTo(leftBorder,0);
return true;
}
//右边界处理
if (getScrollX()+offsetX + getWidth()> rightBorder){
scrollTo(rightBorder-getWidth(),0);
return true;
}
//滑动处理
scrollBy(offsetX,0);
mMoveX = x;
break;
case MotionEvent.ACTION_UP:
//手指抬起,判断是哪个子控件的index
//小于第一个子控件的一半宽度则认为是第一个子控件
//大于第一个子控件的一半宽度则认为是下一个子控件
int index = (getScrollX()+getWidth()/2)/getWidth();
Log.i(TAG, "index: "+index); //结果为 0 1 2
//根据子空间index计算偏移量
int dy = index * getWidth() - getScrollX();
Log.i(TAG, "dy: "+dy);
mScroller.startScroll(getScrollX(),0,dy,0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
//重绘会调用此方法,此方法中的invalidate又会触发重绘,从而循环实现弹性滑动
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
}
6) 属性动画(动画中讲解)
7) ViewDragHelper
在自定义ViewGroup中,很多效果都包含用户手指去拖动其内部的某个View(eg:侧滑菜单等),针对具体的需要去写好onInterceptTouchEvent和onTouchEvent这两个方法是一件很不容易的事,需要自己去处理:多手指的处理、加速度检测等等。
好在官方在v4的支持包中提供了ViewDragHelper这样一个类来帮助我们方便的编写自定义ViewGroup
1)ViewDragHelper类相关的API:
方法 | 说明 |
---|---|
create(ViewGroup forParent, ViewDragHelper.Callback cb) | 创建viewDragHelper |
captureChildView(View childView, int activePointerId) | 捕获子视图 |
checkTouchSlop(int directions, int pointerId) | 检查移动是否为最小的滑动速度 |
findTopChildUnder(int x, int y) | 返回指定位置上的顶部子视图 |
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) | 解决捕获视图自由滑动的位置 |
getActivePointerId() | 获取活动的子视图的id |
getCapturedView() | 获取捕获的视图 |
getEdgeSize() | 获取边界的大小 |
getMinVelocity() | 获取最小的速度 |
getTouchSlop() | 获取最小的滑动速度 |
getViewDragState() | 获取视图的拖动状态 |
isCapturedViewUnder(int x, int y) | 判断该位置是否为捕获的视图 |
isEdgeTouched(int edges) | 判断是否为边界触碰 |
setEdgeTrackingEnabled(int edgeFlags) | 设置边界跟踪 |
settleCapturedViewAt(int finalLeft, int finalTop) | 设置捕获的视图到指定的位置 |
smoothSlideViewTo(View child, int finalLeft, int finalTop) | 滑动侧边栏到指定的位置 |
shouldInterceptTouchEvent(MotionEvent ev) | 处理父容器是否拦截事件 |
processTouchEvent(MotionEvent ev) | 处理父容器拦截的事件 |
2)ViewDragHelper.Callback相关API:
方法 | 说明 |
---|---|
clampViewPositionHorizontal(View child, int left, int dx) | 控制横轴的移动距离 |
clampViewPositionVertical(View child, int top, int dy) | 控制纵轴的移动距离 |
getViewHorizontalDragRange(View child) | 获取视图在横轴移动的距离 |
getViewVerticalDragRange(View child) | 获取视图在纵轴的移动距离 |
onEdgeDragStarted(int edgeFlags, int pointerId) | 处理当用户触碰边界移动开始的回调 |
onEdgeLock(int edgeFlags) | 处理边界被锁定时的回调 |
onEdgeTouched(int edgeFlags, int pointerId) | 处理边界被触碰时的回调 |
onViewCaptured(View capturedChild, int activePointerId) | 当视图被捕获时的回调 |
onViewDragStateChanged(int state) | 当视图的拖动状态改变的时候的回调 |
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) | 当捕获的视图位置发生改变的时候的回调 |
onViewReleased(View releasedChild, float xvel, float yvel) | 当视图的拖动被释放的时候的回调 |
tryCaptureView(View child, int pointerId) | 判断此时的视图是否为想要捕获的视图时会调用 |
getOrderedChildIndex(int index) | 获取子视图的Z值 |
//方法的大致的回调顺序:
1)shouldInterceptTouchEvent:
DOWN:
getOrderedChildIndex(findTopChildUnder)
->onEdgeTouched
MOVE:
getOrderedChildIndex(findTopChildUnder)
->getViewHorizontalDragRange &
getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
->clampViewPositionHorizontal&
clampViewPositionVertical
->onEdgeDragStarted
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
2)processTouchEvent:
DOWN:
getOrderedChildIndex(findTopChildUnder)
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
->onEdgeTouched
MOVE:
->STATE==DRAGGING:dragTo
->STATE!=DRAGGING:
onEdgeDragStarted
->getOrderedChildIndex(findTopChildUnder)
->getViewHorizontalDragRange&
getViewVerticalDragRange(checkTouchSlop)
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
例子
1)任意移动
2)移动完毕后回到原位
3)边界移动时对View进行捕获(未成功。。)
public class VDHDemo extends LinearLayout {
private static final String TAG = "VDHDemo";
private ViewDragHelper mDragger;
private View mDragView;
private View mAutoBackView;
private Point mAutoBackOriPos = new Point();
public VDHDemo(Context context, AttributeSet attrs) {
super(context, attrs);
//第二个参数为敏感度(sensitivity),敏感度越大mTouchSlop就越小
//mTouchSlop为系统认为是移动的最小距离,即ViewConfiguration.get(context).getScaledPagingTouchSlop()
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//返回true表示可以捕获该view,可根据第一个参数决定捕获哪个view
//如: return xxView == child;
return mDragView==child || mAutoBackView==child;
// return true;
}
//边界控制
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft(); //左边界为viewgroup的paddingleft
final int rightBound = getWidth() - leftBound - getPaddingRight() - 200; //200为子view的宽度
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
//边界控制
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
//手指释放时回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// super.onViewReleased(releasedChild, xvel, yvel);
//若为mAutoBackView,则回到初始位置,调用settleCapturedViewAt()
//其内部为mScroller.startScroll(),别忘了invalidate和computeScroll
//注意你拖动的越快,返回的越快
if (releasedChild == mAutoBackView){
mDragger.settleCapturedViewAt(mAutoBackOriPos.x,mAutoBackOriPos.y);
invalidate();
}
}
//如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,
// 在onTouchEvent的DOWN的时候就确定了captureView
//如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,
// 而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,
// 只有这两个方法返回大于0的值才能正常的捕获。
@Override
public int getViewHorizontalDragRange(View child)
{
return getMeasuredWidth()-child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child)
{
return getMeasuredHeight()-child.getMeasuredHeight();
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mDragView = getChildAt(0);
mAutoBackView = getChildAt(1);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//onLayout结束后将mAutoBackView的返回原点设置为其初始的点
mAutoBackOriPos.x = mAutoBackView.getLeft();
mAutoBackOriPos.y = mAutoBackView.getTop();
}
@Override
public void computeScroll() {
if (mDragger.continueSettling(true)){
invalidate();
}
}
}