之前说了android的坐标系和触控事件,下面看 下如何使用系统的方法来实现动态的修改一个view的坐标。
大概思路就是触摸view时,记下当前的坐标;当手指移动时,记下移动后的坐标,从而获取到偏移量并通过偏移量来修改view的坐标。
条条大路通罗马,同样实现滑动的方式也有多种。废话不多说,代码才是硬道理。
1.layout()方法
我们知道view的创建过程onMeasure(),onLayout(),onDraw()。其中onLayout()就是来设置显示的位置,因此我们就可以通过修改view的left,top,right,bottom等属性来控制view的位置。代码如下:
@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;
// 更新当前view的位置
layout(getLeft()+offsetX,getTop()+offsetY,
getRight()+offsetX,getBottom()+offsetY);
// 注意在更新view位置之后要重新设置初始坐标
lastX=rawX;
lastY=rawY;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
我们需要注意一点的是,使用绝对坐标系时在执行完ACTION_MOVE之后一定得重新设置初始坐标。效果如下
2.offsetLeftAndRight()和offsetTopAndBottom()
使用offsetLeftAndRight(),offsetTopAndBottom()效果和使用onLayout()方法一样.这两个方法相当于系统提供的一个对左右,上下移动的API的封装。只需要将方法1中的onLayout()方法替换为这两个方法即可:
// 同时对left和right进行偏移
offsetLeftAndRight(offsetX);
// 同时对top和bottom进行偏移
offsetTopAndBottom(offsetY);
3.LayoutParams
LayoutParams属性保存了一个view的布局参数,因此可以在程序中通过改变LayoutParams来动态改变一个布局的位置参数最终实现改变view位置的效果。步骤如下:
- 获取view的LayoutParams
使用getLayoutParams()方法可以获取一个view的LayoutParams。不过需要注意的是得根据view所在父布局的类型来设置不同的类型,如果当前view所在父布局为LinearLayout那么就可以使用LinearLayout.LayoutParams类似的如果在RelativeLayout中就要使用RelativeLayout.LayoutParams。当然这一切的前提你的view还需要有个父布局。 - 计算偏移量offsetX和offsetY
偏移量的计算方式和前两种方式的计算方法一样(这里换了一种方式取偏移量)。 - 通过setLayoutParams来改变其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;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
通过改变LayoutParams来改变一个View的位置时,通常改变的是这个view的Margin属性.所以除了使用布局LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这个功能。
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
这样一搞,我们就不需要考虑父布局的类型了.
4.scrollTo和scrollBy
单单从这两个方法的名字上我们就应该知道这两个方法的区别scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示在原来位置的基础上移动的增量为dx,dy.
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
//进行移动
scrollBy(offsetX,offsetY);
通过更改方法我们发现view并没有移动效果,但我们的方法也没有写错啊!下面就说一下这个的区别.
- 当我们使用scrollTo和scrollBy进行移动操作时,被移动的是view的内容.也就是说如果是一个ViewGroup调用该方法,那么被移动的是所有的子view;如果是View例如TextView那么被移动的就是它的文本,如果是ImageView那么被移动的就是它的drawable对象。所以如果想使用这两个方法来完成移动效果我们需要这样处理
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollTo(offsetX ,offsetY);
- 通过修改我们发现现在是可以实现移动效果了,但是好像有点不对.不是和我们预想的一样,没有跟着我们的手指移动
- 这里我们需要对视图的移动进行一下说明:
(第一张图)此时Button坐标为(20,10)这里我们先把手机屏幕看作是一个盖板,我们的视图是一个大的画布,透过盖板展示的就是我们在手机屏幕上看到的视图,上面我们调用scrollBy方法的时候就可以认为是盖板在移动.
(第二张图)移动之后Button坐标为(0,0).这里有点绕虽然我们的Button都是沿这X,Y轴的正方向移动的,但是在屏幕的可见区域内Button却向X轴,Y轴的负方向上移动了.还是最初的那个问题,参考系选择的不同产生的效果不同.我们在修改下代码:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX,-offsetY);
5.Scroller
- 既然上面都用到了scrollBy,scrollTo这两个方法,最好把Scroller这个类也看下。通过上面的介绍相信完成这个需求,点击一个按钮让一个ViewGroup的子View向右移动100像素。是不是so easy啊,但是我们也发现这个移动效果都是瞬间完成的,Google对于动画效果的建议是使用自然过度的动画,让用户不会感觉那么突兀于是Scroller这个类就出现了。
- 哈哈,你是不是知道怎么做了。scroller类的原理与scrollTo和scrollBy方法来实现view的移动原理基本类似。我们仔细看一下ACTION_MOVE事件就知道了,在ACTION_MOVE事件中我们让view移动的距离通过不断的触发事件分成了n个非常小的移动量。这样在整体上产生一个平滑移动的效果。
我们就看下Scroller这个类怎么来完成吧。
3.1初始化Scroller,首先我们创建一个Scroller对象
public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化Scroller
mScroller=new Scroller(context);
}
3.2 重写computeScroll()方法
computeScroll()方法是使用Scroller类的核心,系统在绘制view的时候会在draw()方法中调用该方法而这个方法实际上就是使用的scrollTo方法。在结合scroller对象,帮助获取到当前的滚动值,我们就可以通过不断的瞬间移动一个小的距离来实现整体上的平滑的移动效果,代码如下:
@Override
public void computeScroll() {
super.computeScroll();
// 判断scroller是否执行完毕
if (mScroller.computeScrollOffset()){
// 使用scrollTo方法,借助mScroller对象获取当前滚动值
((View) getParent()).scrollTo(mScroller.getCurrX(),
mScroller.getCurrY());
// 调用onDraw()方法,在onDraw方法中完成对computeScroll的不断调用
invalidate();
}
}
Scroller这个类提供了computeScrollOffset()方法来判断是否完成了整个滚动,同时也提供了getX(),getY()方法获得当前的滑动坐标。上面的代码需要注意的一个逻辑是invalidate()—>onDraw()—>computeScroller()。这样就实现了循环获取当前的scrollX,scrollY的值。在当模拟过程结束后,scroller.computeScrollOffset方法会返回false从而结束循环。
3.3 startScroll 开启模拟过程
经过上面的准备,最后我们在我们需要的地方调用scroller对象的startScroll()方法开启平滑移动过程。调用startScroll()方法我们会发现有另个重载方法
public void startScroll(int startX,int startY,int dx,int dy ,int duration)
public void startScroll(int startX,int startY,int dx,int dy)
可以看到区别就是一个具有指定的持续时长,而另一个没有。
6.ViewDragHelper
去github上逛一圈很快就发现有很多漂亮的滑动控件。Google在support库中提供了DrawerLayout和SlidingPaneLayout两个布局来帮助实现侧边滑动效果,但是通过代码发现在这两个功能强大的布局背后却有一个非常强大的类ViewDragHelper。通过这个类,基本可以实现各种不同的滑动,当然了使用起来也是比较复杂的。下面通过一个列子简单说一下,目标是实现QQ侧滑效果。
- 初始化ViewDragHelper
首先在构造方法里对ViewDragHelper进行初始化,通常定义在一个ViewGroup中,并通过静态工厂方法进行初始化
// 实例化ViewDragHelper
mViewDragHelper = ViewDragHelper.create(this, callback);
它的第一个参数就是要监听的View,也就是parentView,第二个参数是一个CallBack回调,这个回调就是整个ViewDragHelper的逻辑核心。
- 事件拦截
由于我们要监听与屏幕的交互,因此要重写事件拦截方法,将事件传递给ViewDragHelper进行处理代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 将触摸事件传递给ViewDragHelper进行处理
mViewDragHelper.processTouchEvent(event);
return true;
}
- 处理 computeScroll
这里我们依然重写computeScroll方法,因为ViewDragHelper内部也是通过Scroller这个类来实现的,可以参考这么来写。
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
- 处理回调callback
上面我们在实例化ViewDragHelper的时候传了一个callback的参数,并且提到这个回调是整个类实现逻辑的核心,下面我们就好好看下这个类。
创建这个回调对象的时候必须要重写一个方法,通过IDE已经帮我完成了,(不得说下AdndroidStudio就是好用,可公司还在用eclipse)
private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 通过这个回调接口的该方法,我们可以确定那个子view可以被移动
// 如果当前触摸的child是mMenuView时开始检测
return mMenuView==child;
}
};
另外这个回调接口里还有比较重要的方法,下面继续解释滑动方法clampViewPositionHorizontal(View child, int left, int dx)和clampViewPositionVertical(View child, int top, int dy)这两个方法分别对应水平和垂直方向上的滑动。如果要发生滑动效果这两个方法是必须要写的,因为这两个方法的默认返回值是0即发生滑动或者说滑动距离位0。
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child是待移动的子view
// left代表水平上child移动的距离
// dy代表比较前一次的增量
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// child是待移动的子view
// top代表垂直方向上child移动的距离
// dy代表比较前一次的增量
return top;
}
到这里滑动的效果已经可以实现了。这样看我们重写的computeScroll方法好像没有使用。别急,下面就用到了我们在方式5中是通过Scroller这个类来实现的滑动效果,并且还完成了手指抬起后回到原来的位置的功能,是在ACTION_UP事件中写的。强大的ViewDragHelper类似的实现怎么办呢,当然还是callback在回调接口中还有一个方法onViewRelased()通过这个方法可以废除简单的实现当手指离开屏幕后的操作,当然其内部也是通过Scroller类来实现这也就是为什么要重写computeScroll方法的原因了。啰嗦了那么多还是看看代码吧
// 拖动结束后调用
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
// 当侧滑的距离较小时,自己慢移到指定位置
if (mMainView.getLeft()<500){
// 相当于Scroller的startStroll方法
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(ViewDragHelperDemo.this);
}else {
// 如果侧滑拉动的距离大于一定距离,就移动到指定的位置
mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(ViewDragHelperDemo.this);
}
}
上面啰嗦了那么多,代码是不是非常简单啊!当拉动的距离大于500像素时就把侧滑菜单显示出来,当小于500像素的时候就自动关闭侧滑菜单。
以上就是实现滑动的几种方式,可以看到最后一个最强大,但是需要的东西也多除了逻辑上我们要清晰外,还牵连到事件的拦截处理,滑动处理。慢慢学习吧还有很多呢