实现滑动效果的方式多种多样,我在这里介绍下面几种常用方式和需要注意的点。
第一种:scrollTo和scrollBy
- 在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View的位置。srollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示移动的增量为dx,dy。
- srollTo,srollBy移动的是View的content,即让View的内容移动,如果在ViewGroup中使用srollTo、scrollBy方法,那么移动的将是所有子View,但如果在View中使用,那么移动的将是View的内容,例如TextView,content就是它的文本;ImageView,content就是它的drawable对象。
自定义MyScrollByView 可以实现随着手指移动
关键代码((View)getParent()).scrollBy(-deltaX , -deltaY);让View所在的ViewGroup中来使用srollBy方法,移动它的子View。
public class MyScrollByView extends View{
private int x,y;
private int lastX,lastY;
private int deltaX,deltaY;
private static final String TAG = "MyScrollByView";
public MyScrollByView(Context context) {
this(context,null);
}
public MyScrollByView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyScrollByView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
Log.i(TAG, "onTouchEvent: lastX = " + lastX + " , lastY = " + lastY);
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
x = (int) event.getX();
y = (int) event.getY();
deltaX = x - lastX;
deltaY = y - lastY;
Log.i(TAG, "onTouchEvent: x = " + x + " , y = " + y + " , deltaX = " + deltaX + " , deltaY = " + deltaY);
//没有想要的移动效果,这里移动是控件的content的内容,而非整个view
//scrollBy(-deltaX , -deltaY);
//移动整个view
((View)getParent()).scrollBy(-deltaX , -deltaY);
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "onTouchEvent: ACTION_UP");
break;
}
//return true将ACTION_DOWN以后的事件直到ACTION_UP交给自己处理。如果不是true,处理事件可能会被上级处理
return true;
}
}
第二种:Scroller
Scroller类与scrollTo、scrollBy方法十分相似,区别在于Scroll可以实现平滑移动的效果,而不是瞬间完成的移动。
- 初始化Scroller
- 重写computeScroll()方法,实现模拟滑动
系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际上就是使用的scrollTo方法。再结合Scroller对象,帮助获取到当前的滚动值。我们可以通过不断地瞬间移动一个小的距离来实现整体上的平滑移动效果。 - 使用平滑移动的事件中,使用Scroll类的startScroll()方法来开启平滑移动过程。
自定义MyScrollerView,随着手指移动,放手后缓慢回到原地
public class MyScrollerView extends View {
private Scroller mScroll;
private static final String TAG = "MyScrollerView";
private int x,y;
private int lastX,lastY;
private int deltaX,deltaY;
public MyScrollerView(Context context) {
this(context,null);
}
public MyScrollerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyScrollerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroll = new Scroller(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "onDraw: ");
}
@Override
public void computeScroll() {
super.computeScroll();
//判断Scroller是否执行完毕
if(mScroll.computeScrollOffset()){
((View)getParent()).scrollTo(mScroll.getCurrX(),mScroll.getCurrY());
Log.i(TAG, "computeScroll: mScroll.getCurrX() = " + mScroll.getCurrX() + " , mScroll.getCurrY() = " + mScroll.getCurrY());
//通过重绘来不断调用computeScroll方法 invalidate()->onDraw()->computeScroll()
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent: ACTION_DOWN");
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent: ACTION_MOVE");
x = (int) event.getX();
y = (int) event.getY();
deltaX = x - lastX;
deltaY = y - lastY;
((View)getParent()).scrollBy(-deltaX,-deltaY);
break;
case MotionEvent.ACTION_UP:
View viewGroup = (View) getParent();
Log.i(TAG, "onTouchEvent: ACTION_UP viewGroup.getScrollX() = " + viewGroup.getScrollX() + " , viewGroup.getScrollY() = " + viewGroup.getScrollY());
mScroll.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),-viewGroup.getScrollX(),-viewGroup.getScrollY(),500);
invalidate();//通知View进行重绘
break;
}
return true;
}
}
效果演示
第三种:ViewDragHelper
Google在其support库中为我们提供了DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧边栏滑动的效果中藏着ViewDragHelper。通过ViewDragHelper,基本可以实现各种不同的滑动、拖放需求。
- 初始化ViewDragHelper
ViewDragHelper通常定义在一个ViewGroup的内部,并通过其静态工厂方法进行初始化。 - 拦截事件
重写事件拦截方法,将事件传递给ViewDragHelper进行处理。 - 处理computeScroll()
使用ViewDragHelper同样需要重写下computeScroll()方法,因为ViewDragHelper内部也是通过Scroller来实现平滑移动的。 - 处理回调Callback
tryCaptureView(),通过这个方法,我们可以指定在创建ViewDragHelper时,哪一个子View可以被移动。
clampViewPositionHorizontal(),clampViewPositionVertical()实现该方向上的滑动效果。通常情况下,只需要返回top和left即可,但当需要更加精确地计算pading等属性的时候,就需要对left,top进行处理,返回合适大小的值
onViewReleased(),手指离开屏幕后实现的操作。
onViewCaptured(),在用户触摸到View后回调。
onViewDragStateChanged(),在拖拽状态改变时回调,比如idle,dragging等状态。
onViewPositionChanged(),在位置改变时回调,常用于滑动时更改scale进行缩放等效果。
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
//何时开始你检测触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测
return mMainView == child;
}
// 水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//left表示child水平方向上的滑动的距离,dx表示和前一次的增量。
return left;
}
// 垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;//返回值为0,即不发生滑动
}
//手指离开屏幕后实现的操作
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if(mMainView.getLeft()<500){
//关闭菜单
//相当于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}else{
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}
}
};
自定义MyViewDragHelper,实现抽屉式布局
public class MyViewDragHelper extends ViewGroup {
private ViewDragHelper mViewDragHelper;
private View mMainView,mMenuView;
private int mWidth;
private static final String TAG = "MyViewDragHelper";
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
//何时开始你检测触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测
return mMainView == child;
}
// 水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;//left表示child水平方向上的滑动的距离,dx表示和前一次的增量
}
// 垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;//返回值为0,即不发生滑动
}
//手指离开屏幕后实现的操作
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if(mMainView.getLeft()<500){
//关闭菜单
//相当于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}else{
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(MyViewDragHelper.this);
}
}
};
public MyViewDragHelper(Context context) {
this(context,null);
}
public MyViewDragHelper(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyViewDragHelper(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this,callback);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
Log.i(TAG, "onFinishInflate: ");
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
Log.i(TAG, "onSizeChanged: mWidth = " + mWidth);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mMenuView.layout(0,0,1000,b);
mMainView.layout(l,t,r,b);
Log.i(TAG, "onLayout: changed = " + changed + " , l = " + l + " , t = " + t + " , r = " + r + " , b = " + b);
}
@Override
public void computeScroll() {
if(mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
实现效果
当然通过动画也是实现滑动,我通过专门写一篇文章来讲述动画