Android View 滑动

Android View 滑动

View 滑动

有三种方式可以实现View的滑动:

  1. 通过View本身提供的scrollTo/scrollBy方法来实现滑动
  2. 通过动画给View施加平移效果来实现滑动
  3. 通过改变Viev的LayoutParams使得View重新布局从而实现滑动

使用scrollTo/scrollBy

View提供了scrollTo/scrollBy来实现滑动,实现如下:

/**
 * Set the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the x position to scroll to
 * @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

/**
 * Move the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the amount of pixels to scroll by horizontally
 * @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

从上面的源码可以看出,scrollTo实现了基于所传递参数的绝对滑动,scrollBy实现了基于当前位置的相对滑动,实际上也是调用了scrollTo方法,这两个方法中,mScrollX和mScrollY这两个属性非常重要,这两个属性可以通过getScrollX和getScrollY方法分别得到。

关于这两个属性简单概述就是:mScrollX、mScrollY分时是view里的内容相对view位置的滑动距离距离,但比较混淆的一点是mScrollX与mScrollY取值的正负与滑动方向是相反的,如下图所示:

在这里插入图片描述

使用动画

滑动的另一种方式,主要是操作View的translationX,translationY属性,即可以采用传统的View动画,也可以采用属性动画,如果用属性动画的话,为了兼容3.0以下的版本需要使用开源库nineoldandroids

实现在100ms里让一个View从初始的位置向右下角移动100个像素,如下:

View动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
     android:fillAfter="true"
     android:zAdjustment="normal">

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"
        />

</set>

属性动画:

ObjectAnimator.ofFloat(testButton,"translationX",0,100).setDuration(100).start();

View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括高宽,并且如果希望动画后的状态得以保存还必须将fillAfter属性设置为true,否则动画完成之后就会消失,比如我们要把View向右移动100个像素,如果fillAfter为false,那么动画完成的一刹那,View就会恢复之前的状态,fillAfter为true的话就会停留在最终点,属性动画不会有这样的问题。

改变布局参数

通过改变布局参数,即改变LayoutParams来实现滑动,比如把一个Button向右平移100px,只需要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可,或者在Button左边放置一个默认宽度为0的view,当需要向右移动Button时,只需要重新设置空View的宽度即可,就自动被挤向右边,即实现了向右平移的效果。设置View的LayoutParams的方式如下:

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) button.getLayoutParams();
params.width +=100;
params.leftMargin +=100;
button.requestLayout();
//或者button.setLayoutParams(params);

三种滑动方式对比

  • scrollTo/scrollBy:操作简单,只能滑动View内容,不能滑动View本身
  • 动画:操作简单,能实现复杂的动画效果
  • 改变布局参数:操作稍微复杂,适用于有交互的View

下面通过动画来实现view跟手滑动的效果:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:{
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            int translationX = ViewHelper.getTranslationX(this) + deltaX;
            int translationY = ViewHelper.getTranslationY(this) + deltaY;
            ViewHelper.setTranslationX(this,translationX);
            ViewHelper.setTranslationY(this,translationY);
            break;
        }
        case MotionEvent.ACTION_UP:
            break;
        defaultbreak}
    mLastX = x;
    mLastY = y;
    return true;
}

重写onTouchEvent方法并且处理ACTION_MOVE事件,根据两次滑动之间的距离就可以实现它的滑动,首先通过getRawX和getRawY方法来获取手指当前的坐标,因为需要获取当前点击事件在屏幕中的坐标而不是相对于View本身的坐标,其次,要得到两次滑动之间的位移,有了这个位移就可以移动当前的View,移动方法采用的是动画兼容库nineoldandroids中的ViewHelper类所提供的setTranslationX和setTranslationy,没有3.0以上版本才能使用的限制,与此类似的还有setX、setScaleX、setAlpha等方法。

弹性滑动

弹性滑动就是将一次大的滑动分成若干个小的滑动,并在一个时间段完成,实现方式很多,比如Scroller,Handler#postDelayed以及Thread#Sleep等。

使用Scroller

Scroller的使用方法:

Scroller mScroller = new Scroller(getContext());

private void smoothScrollTo(int destX,int destY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    //1000ms内滑向destX,效果是慢慢滑动
    mScroller.startScroll(scrollX,0,deltaX,0,1000);
    invalidate();
}

@Override
public void computeScroll() {
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
}

描述一下他的工作原理,当我们构建一个scroller对象并且调用它的startScroll方法,scroller内部其实并没有做什么,他只是保存了我们传递的参数,这几个参数从startScroll的原型就可以看出,如下的代码:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

可以看到startScroll内部并没有做滑动相关的事,真正让View弹性滑动的是startScroll下面的invalidate方法,该方法会导致View重绘,当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和ScrollY,然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,如此反复,直到整个滑动过程结束。

再来看下Scroller的computeScrollOffset方法的实现:

/**
 * Call this when you want to know the new location.  If it returns true,
 * the animation is not yet finished.
 */
public boolean computeScrollOffset() {
    ...
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        ...
        }
    }
    return true;
}

该方法会根据时间的流逝的百分比来计算当前的scrollX和scrollY的值,方法返回false表示结束,返回true表示滑动还未结束,就需要继续滑动View

最后概括一下Scroller工作原理,Scroller本身并不滑动,需要配合computeScroll方法才能完成弹性滑动的效果,通过不断的让View重绘,而每次都有一些时间间隔,通过这个时间间隔就能得到他的滑动位置,这样就可以用ScrollTo方法来完成View的滑动了,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动形成了弹性滑动。

通过动画

动画本身就是一种渐进的过程,因此通过他来实现滑动天然就具有弹性效果,比如以下代码让一个view在100ms内左移100像素。

ObjectAnimator.ofFloat(testView, "translationX", 0, 100).setDuration(100).start();

不过这里想说的并不是这个问题,我们可用利用动画的特性来实现一些动画不能实现的效果,还拿scorllTo来说,我们想模仿scroller来实现View的弹性滑动,那么利用动画的特性我们可用这样做:

final int startX = 0;
final int deltaX = 100;
final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
        float fraction = animator.getAnimatedFraction();
        button.scrollTo(startX + (int)(deltaX * fraction),0);
    }
});
animator.start();

在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离,最后通过scrolITo方法来完成View的滑动。

使用延时策略

通过发送一系列延时消息从而达到一种渐近式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于sleep方法来说,通过在while循环中不断的滑动View和sleep,就可以实现弹性滑动的效果。

下面使用Handler来做个示例,在大约1000ms内将View的内容向左移动100像素:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;

private int count = 1;

private Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case MESSAGE_SCROLL_TO: {
                count++;
                if(count <= FRAME_COUNT) {
                    float fraction = count / (float)FRAME_COUNT;
                    int scrollX = (int)(fraction * 100);
                    button.scrollTo(scrollX,0);
                    handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                }
                break;
            }
            default:
                break;
        }
    }
};
发布了174 篇原创文章 · 获赞 119 · 访问量 55万+

猜你喜欢

转载自blog.csdn.net/lj402159806/article/details/96746542
今日推荐