通过一个案例, 来看看在Android 中该如何实现滑动效果。 定义一个View, 并置于一个LinearLayout 中, 实现一个简单布局, 代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.yuyang.dragviewtest.DragView
android:layout_width="100dp"
android:layout_height="100dp" />
</LinearLayout>
目的让这个自定义的View随着手指在屏幕上滑动而滑动初始效果如下:
一、layout方法:
一、通过getX(),getY()来获取坐标值:
public class DragView1 extends View {
private int lastX;
private int lastY;
public DragView1(Context context) {
super(context);
ininView();
}
public DragView1(Context context, AttributeSet attrs) {
super(context, attrs);
ininView();
}
public DragView1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ininView();
}
private void ininView() {
// 给View设置背景颜色,便于观察
setBackgroundColor(Color.BLUE);
}
// 视图坐标方式
@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;
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
break;
}
return true;
}
}
二、通过getRawX(), getRawY() 来获取坐标值:
public class DragView2 extends View {
private int lastX;
private int lastY;
public DragView2(Context context) {
super(context);
ininView();
}
public DragView2(Context context, AttributeSet attrs) {
super(context, attrs);
ininView();
}
public DragView2(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ininView();
}
private void ininView() {
setBackgroundColor(Color.BLUE);
}
// 绝对坐标方式
@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;
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
// 重新设置初始坐标
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
}
使用绝对坐标, 执行MOVE 逻辑后每次都要重新设置初始坐标。
二、 offsetLeftAndRight() 和 offsetTopAndBottom():
// 视图坐标方式
@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;
// 在当前left、top、right、bottom的基础上加上偏移量
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
//layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
break;
}
return true;
}
三、LayoutParams:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点坐标
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
根据View所在父布局的类型来设置不同类型的LayoutParams, 前提是你必须需要一个父布局, 通过LayoutParams来改变View的位置通常是改变这个View的Margin属性,通常使用 ViewGroup.MarginLayoutParams 来实现这样的功能,不需要考虑父布局类型。
四、scrollTo 与 scrollBy:
scrollTo(x,y) 表示移动到一个点, scrollBy(dx, dy) 表示移动的增量为dx, dy。直接在View使用这两个方法中拖动的是View的content而不是View, 我们想拖动View 就必须获得View的 ViewGroup, 代码如下:
((View) getParent()).scrollBy(-offsetX, -offsetY);
要实现跟随手指移动,需要将偏移量改为负值。
五、Scroller:
Scroller类与scrollTo, scrollBy 相似, 不同的是scrollBy是通过将一段位移通过不断切分 使其看起来是平滑的过渡, 但其实是瞬间的移动, 而Scroller类则是平滑移动, 不再是瞬间的移动。看下面个例子:
1、 初始化Scroller, 通过构造方法创建Scroller 对象:
private void ininView(Context context) {
setBackgroundColor(Color.BLUE);
// 初始化Scroller
mScroller = new Scroller(context);
}
2、重写computeScroll() ,它是Scroller的核心,实现模拟滑动:
@Override
public void computeScroll() {
super.computeScroll();
// 判断Scroller是否完成了整个滑动
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY());
// 通过重绘来不断调用computeScroll
invalidate();
}
}
invalidate()--->draw()--->computeScroll(), 实现循环获取scrollX 和 scrollY的目的。实现平滑过渡。
3、startScroll开启模拟过程:
startScroll() 方法具有两个重载方法。
startScroll(int startX, int startY, int dx, int dy, int duration)
startScroll(int startX, int startY, int dx, int dy)
它们的区别是一个可以设置过渡时长。要监听手指离开屏幕的事件, 并在该事件中通过调用startScroll() 方法完成平滑移动。
在startScroll()方法中获取子View移动的距离, 并将其偏移量设置为相反数, 从未将其子View滑动到原位置。 事件中需要添加incadate() 方法,完成屏幕刷新。
case MotionEvent.ACTION_UP:
// 手指离开时,执行滑动过程
View viewGroup = ((View) getParent());
mScroller.startScroll(
viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY(),3000);
invalidate();
break;
六、属性动画:
七、ViewDargHelper:
初始状态 滑动展开菜单状态
1、初始化ViewDragHelper
private void initView() {
mViewDragHelper = ViewDragHelper.create(this, callback);
}
第一个参数是要监听的View, 通常是parentView, 第二个是Callback 回调, 这个回调就是ViewDragHelper 的逻辑核心。
2、拦截事件
然后重写事件拦截方法, 将事件传递给ViewDragHelper 进行处理:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
3、处理computeScroll()
ViewDragHelper 也是通过Scroller 来进行滑动的。
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
4、处理回调Callback
这是关键的Callback 实现:
private ViewDragHelper.Callback callback =
new ViewDragHelper.Callback() {
// 何时开始检测触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测
return mMainView == child;
}
};
通过IDE自动填写的 tryCaptureView() 方法, 我们可以指定在创建ViewDragHelper 时, 参数parentView 中的哪一个子View 可以被移动,在这个例子中定义了两个View, 通过如下代码, 只将MainViwe 设置为可被拖动的。
具体的滑动方法为clampViewPositionVertical() 和clampViewPositionHorizontal(), 分别对应垂直和水平方向的滑动,这是实现滑动必须实现的方法,
// 处理垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0; //此处返回值为0, 和没重写效果一样, 子垂直方向没有滑动
}
// 处理水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
通常情况下返回top、 left即可, 但需要更加精密的计算padding等属性时, 就需要对left 进行一些处理, 并返回合适大小。
仅仅重写上面三个方法,就可以实现一个简单的滑动效果。 让MainView跟随手指滑动。
接下来当手指离开后子View回到初始位置效果, 在Callback中, 提供了onViewReleased() 方法, 可以简单的实现当手纸离开屏幕后实现的操作。 其内部也是通过Scrollrt类来实现的,
// 拖动结束后调用
@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(DragViewGroup.this);
} else {
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
设置让MainView移动后距离左边距小于500 像素的时候, 使用 mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
将MainView 还原到初始状态。否则移动到(300,0), 即显示MenuView. 接着我们继续完善案例, 实现类似QQ侧滑栏菜单的效果, 自定义一个ViewGroup, 在onFinishInflate() 方法中, 按顺序将子View 分别定义成MeunView 和 MainView, 并在onSizeChanged()方法中获得View的宽度。 如果需要根据View的宽带处理滑动后的效果, 就可以使用这个值来进行判断。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
}
完整代码如下:
public class DragViewGroup extends FrameLayout {
private ViewDragHelper mViewDragHelper;
private View mMenuView, mMainView;
private int mWidth;
public DragViewGroup(Context context) {
super(context);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewGroup(Context context,
AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
private void initView() {
mViewDragHelper = ViewDragHelper.create(this, callback);
}
private ViewDragHelper.Callback callback =
new ViewDragHelper.Callback() {
// 何时开始检测触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测
return mMainView == child;
}
// 触摸到View后回调
@Override
public void onViewCaptured(View capturedChild,
int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 当拖拽状态改变,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 当位置改变的时候调用,常用与滑动时更改scale等
@Override
public void onViewPositionChanged(View changedView,
int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
// 处理垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
// 处理水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
// 拖动结束后调用
@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(DragViewGroup.this);
} else {
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
在ViewDragHelper.Callback中, 系统定义了大量的监听事件来帮助我们处理各种事件,去处理程序中的滑动效果。
总结:
一、scrollTo/ scrollBy: 操作简单, 适合对View 内容的滑动。
二、动画: 操作简单, 主要适合用于没有交互的View, 和实现复杂的动画效果
三、改变参数布局: 操作稍复杂, 适用于有交互的View
--------------------------------------------------------------