本文选自《android群英传》第五章。主要是滑动方面知识,讲解了坐标系、MotionEvent剄七种滑动的实现方法。
1-滑动的产生
滑动一个View本质就是移动View,通过改变View的坐标去实现这个效果。这里就需要监听用户触摸的事件。下面包含两个部分:Android窗口坐标系和屏幕触控事件MotionEvent
1-Android坐标系
以屏幕左上角为原点,向右为X正方向,向下为Y轴正方向。
Android中通过
getLocationOnScreen(intlocation[])
能获得当前视图的左上角在Andriod坐标系中的坐标。触控事件中,getRawX()和getRawY(),获得的同样是android坐标系中的坐标
2-视图坐标系
是当前视图以父视图左上角为原点建立的坐标系,用于描述当前视图在父视图中的坐标。
- 触控事件中通过
getX()、getY()
获得在视图坐标系中的坐标。
3-MotionEvent
一共有几种常用事件常量:ACTION_DOWN单点按下\UP单点离开\MOVE单点移动\CANCEL动作取消\OUTSIDE动作超出边界\POINTER_DOWN多点触摸按下\POINTER_UP多点触摸离开
一般情况通过onTouchEvent中的event.getAction()来获取事件类型的常量。
系统有很多获取坐标值和相对距离的方法。主要分为两类。
View提供的获取坐标的方法
getTop:View的顶部到父控件顶边的距离。
getLeft/Right/Bottom:对应View的左边/右边/底部分别到父控件左边/右边/底部的距离。
MotionEvent提供的方法
getX()获得点击事件event距离控件左边的距离。视图坐标。
getY()获得点击事件event距离控件顶部的距离。视图坐标。
getRawX()/RawY()获得点击事件距离整个屏幕左边/顶部的距离。绝对坐标。
2-实现滑动
1-Layout实现滑动
View控件有layout决定View的位置。在View控件的onTouchEvent()方法中去获得控件滑动前后的偏移。通过layout方法去重新设置。
自定义View
public class ScrollByLayoutView extends AppCompatImageView{
float downX;
float downY;
/**
* 三个构造函数千万不能少
*/
public ScrollByLayoutView(Context context) {
super(context);
}
public ScrollByLayoutView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollByLayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 进行偏移计算,之后调用layout
*/
public boolean onTouchEvent(MotionEvent event) {
float curX = event.getX(); //手指实时位置的X
float curY = event.getY(); //Y
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
downX = curX; //按下时的坐标
downY = curY;
Log.i("ScrollByLayoutView", "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
int offsetX = (int)(curX - downX); //X偏移
int offsetY = (int)(curY - downY); //Y偏移
//用getLeft得到当前控件距离父控件左边的距离+偏移量,得到变化后的距离,然后调用layout
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
Log.i("ScrollByLayoutView", "onTouchEvent: ACTION_MOVE");
break;
}
return true;
}
}
使用控件:
<com.example.xxx.ScrollByLayoutView
android:id="@+id/scroll_way1_imageview"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/jide"/>
这样就实现了滑动,对于offset偏移值的计算,也可以使用event的getRawX/Y来计算。
- 特别注意:使用绝对坐标需要在每次调用layout之后重新设置初始值(downX = curX)
2-offsetLeftAndRight和offsetTopAndBottom
直接替换layout
//对left和right, top和bottom同时偏移
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
3-LayoutParams
利用布局的参数来移动View控件。
//方法三:通过布局设置在父控件的位置。但是必须要有父控件, 而且要指定父布局的类型,不好的方法。
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
//方法四:用ViewGroup的MarginLayoutParams的方法去设置marign
// 相比于上面方法, 就不需要知道父布局的类型。
// 缺点:滑动到右侧控件会缩小
ViewGroup.MarginLayoutParams mlayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
mlayoutParams.leftMargin = getLeft() + offsetX;
mlayoutParams.topMargin = getTop() + offsetY;
setLayoutParams(mlayoutParams);
4-scrollTo\scrollBy
scrollTo去移动到制定坐标
scrollBy表示移动的增量
- 这两个方法是移动View的内容,因此需要在View的父控件中调用。
//方法五:scrollTo/scrollBy,在父控件中调用来操作父控件内部的控件
((View)getParent()).scrollBy(offsetX, offsetY);
然而结果是错误的,因为滑动的参考物并不是之前的。
这里的移动类似于移动了玻璃,所以View控件移向了完全相反的地方。需要取反。
((View)getParent()).scrollBy(-offsetX, -offsetY);
5-Scroller
和scrollTo/By比较类似,但是去别在于scrollTo/By的位移是瞬间完成的。而Scroller却是平滑移动的。减少了突兀感。
这里实现一个效果,就是滑动后,控件自动返回最初始位置,这里在ACTION_UP中实现。
三个步骤:
1.初始化scroller
自定义View构造中初始化。
Scroller mScroller;
public ScrollByLayoutView(Context context) {
super(context);
mScroller = new Scroller(context);
}
2.重载自定义View的computeScroll
public void computeScroll() {
super.computeScroll();
//判断scroller是否执行完毕。
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//通过重绘来不断调用 computeScroll
invalidate();
}
}
判断是否执行完毕
3.ACTION_UP中将View滑动回初始位置
case MotionEvent.ACTION_UP:
View viewGroup = (View) getParent();
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
-viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate();
break;
6-属性动画
参考动画章节
7-ViewDragHelper
Google在其support库中为我们提供了一个DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧滑效果,这两个布局,大大的方便了我们自己创建自己的滑动布局,然而,这两个强大的布局背后,却隐藏着一个鲜为人知,却功能强大的类——ViewDragHelper,通过ViewDragHelper,基本可以实现各种不同的侧滑,拖放需求,因此这个方法也是各种滑动解决方案的终极绝招。
QQ侧滑菜单
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();
}
/**-------------------------------------------
* 1、初始化数据:调用ViewDragHelper.create方法
* ------------------------------------------*/
private void initView() {
mViewDragHelper = ViewDragHelper.create(this,callback); //需要监听的View和回调callback
}
/**-------------------------------
* 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()
* 内部也是通过scroller来进行平移滑动, 这个模板可以照搬
* -------------------------------------------*/
@Override
public void computeScroll() {
if(mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
/**------------------------------
* 4、处理的回调:侧滑回调
* ----------------------------*/
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
/*-------------------------------
* 何时开始触摸:
* 1.指定哪一个子View可以被移动.
* 2.如果直接返回true,在该布局之内的所有子View都可以随意划动
* ------------------------------*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView开始检测
return mMainView == child;
}
/*-------------------------------
* 处理水平滑动:
* 1. 返回值默认为0,如果为0则不处理该方向的滑动。
* 2. 一般直接返回left,当需要精准计算pading等值时,可以先对left处理再返回
* ------------------------------*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
/*-------------------------------
* 处理垂直滑动:
* 1. 返回值默认为0,如果为0则不处理该方向的滑动。
* 2. 一般直接返回top,,当需要精准计算pading等值时,可以先对left处理再返回
* ------------------------------*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
/*---------------------------------------------
* 拖动结束后调用,类似ACTION_UP。
* 这里是实现侧滑菜单,一般滑动可以不用这段代码
* ---------------------------------------------*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指抬起后缓慢的移动到指定位置
if(mMainView.getLeft() <500){
//关闭菜单
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}else{
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
/**---------------------------------------------------
* 5、获取子控件用于处理
* 1. 上面完成了滑动功能,这里简单的按照第1、2的顺序指定子控件View的内容
* 2. onSizeChanged能够获得menu等子控件的宽度等信息,有需求可以后续处理
* ----------------------------------------------*/
//XML加载组建后回调
@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();
}
}
使用(作为父控件,里面依次放menu和main):
<com.example.xxxx.DragViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"/>
</com.example.xxxx.DragViewGroup>