Android View - 滑动

27 篇文章 0 订阅
11 篇文章 0 订阅

View的滑动主要有3种:
(1)调用scrollTo/scrollBy方法: 滑动的是View内容,操作比较简单。
(2)动画: 滑动的View位置,其实改变的是View的translationX或者translationY。(参考我的博文 Android View - 位置参数
(3)改变布局参数: 通过设置LayoutParams,使View改变位置,只要加上延时,不断得改变位置参数,就可以达到滑动效果。

scrollTo/scrollBy滑动

为了View的滑动,Android在View内实现了2个方法:scrollTo和scrollBy。
代码如下:

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();  
        }  
    }  
}  
public void scrollBy(int x, int y) {  
    scrollTo(mScrollX + x, mScrollY + y);  
}

其实scrollBy方法调用了scrollTo方法。scrollTo改变了mScrollX和mScrollY,然后重绘,调用onScrollChanged通知View(子类可以重写,监听滑动)。
我们说过,调用scrollTo移动的View的内容,比如我们调用某个View的scrollTo(100, 100),那么View的内容发生如下变化:

你会发现内容向x轴和y轴负方向都移动了100,为什么是负方向呢?因为调用scrollTo移动的是View的内容,你可以看做是View边界移动了100,所以View内容相对于View就移动了-100了。

其实调用scrollTo方法会瞬间完成View的内容移动,这个用户体验显然是不好的。Android还提供了一个Scroller类,配合View的滑动,实现在规定时间内,移动指定距离。

我们从源码分析Scroller类。先从构造函数看起:

public Scroller(Context context) {  
    this(context, null);  
}   
public Scroller(Context context, Interpolator interpolator) {  
    mFinished = true;  
    mInterpolator = interpolator;  
    float ppi = context.getResources().getDisplayMetrics().density * 160.0f;  
    mDeceleration = SensorManager.GRAVITY_EARTH   // g (m/s^2)  
                  * 39.37f                        // inch/meter  
                  * ppi                           // pixels per inch  
                  * ViewConfiguration.getScrollFriction();  
}  

我们使用Scroller时,可以定义插值器。插值器可以指定View滑动的变化速度,可以先快后慢,也可以先慢后快,也可以匀速。

还要介绍Scroller类的2个方法:

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;  
    // 计算指定移动距离应该分几次完成,如20秒内完成,则每次移动为1/20
    mDurationReciprocal = 1.0f / (float) mDuration;  
    // This controls the viscous fluid effect (how much of it)  
    mViscousFluidScale = 8.0f;  
    // must be set to 1.0 (used in viscousFluid())  
    mViscousFluidNormalize = 1.0f;  
    mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);  
}  

可以看出,startScroll方法只是设置了滑动的基本信息,并没有滑动呢。所以如果我们只是调用startScroll,View的滑动并不会开始哦。

computeScrollOffset

public boolean computeScrollOffset() {  
    // 如果结束了,就返回false
    if (mFinished) {  
        return false;  
    }  
    // 计算已经滑动的时间
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);  
    // 如果已经移动的时间小于指定移动的时间
    if (timePassed < mDuration) {  
        switch (mMode) {  
        case SCROLL_MODE:  
            // 计算当前时间应该滑动的距离
            float x = (float)timePassed * mDurationReciprocal;  
            if (mInterpolator == null)  
                x = viscousFluid(x);   
            else  
                x = mInterpolator.getInterpolation(x); 
            // 如x=10/20,那么10秒钟应该移动 mDeltaX * 10/20 的距离
            mCurrX = mStartX + Math.round(x * mDeltaX);  
            mCurrY = mStartY + Math.round(x * mDeltaY);  
            break;  
        case FLING_MODE:  
            float timePassedSeconds = timePassed / 1000.0f;  
            float distance = (mVelocity * timePassedSeconds)  
                    - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);  
            mCurrX = mStartX + Math.round(distance * mCoeffX);  
            // Pin to mMinX <= mCurrX <= mMaxX  
            mCurrX = Math.min(mCurrX, mMaxX);  
            mCurrX = Math.max(mCurrX, mMinX);  
            mCurrY = mStartY + Math.round(distance * mCoeffY);  
            // Pin to mMinY <= mCurrY <= mMaxY  
            mCurrY = Math.min(mCurrY, mMaxY);  
            mCurrY = Math.max(mCurrY, mMinY);  
            break;  
        }  
    }  
    // 如果到时间了,就设置当前位置为滑动最终位置,并设置结束标志
    else {  
        mCurrX = mFinalX;  
        mCurrY = mFinalY;  
        mFinished = true;  
    }  
    return true;  
}  

computeScrollOffset方法主要设置当前时间的位置,供View使用。

接下来我们看看View怎么使用Scroller,从View的绘制draw方法开始,只给出重要代码:

public void draw(Canvas canvas) {   
    ... 
    final int viewFlags = mViewFlags;  
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
    if (!verticalEdges && !horizontalEdges) {    
        if (!dirtyOpaque) onDraw(canvas);   
        dispatchDraw(canvas);  
        onDrawScrollBars(canvas);  
        return;  
    }  
    ...  
}

在draw方法会调用dispatchDraw方法,dispatchDraw会传入子View调用drawChild方法:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
    ...  
    if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&  
            (child.mPrivateFlags & DRAW_ANIMATION) == 0) {  
        return more;  
    }  
    child.computeScroll();  
    final int sx = child.mScrollX;  
    final int sy = child.mScrollY;  
    boolean scalingRequired = false;  
    Bitmap cache = null;  
    ...
}  

此时每个View的computeScroll会被调用:

public void computeScroll() {  
}  

而computeScroll却是一个空方法,供子类重写。

所以我们可以在这个方法调用Scroller的computeScrollOffset方法,让Scroller帮我们计算当前滑动的位置,并判断滑动是否完成,然后我们获取Scroller帮我们计算的位置,调用View的scrollTo方法滑动View一部分。

整个流程下来,是这样的:

代码实现:

import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.Scroller;

public class MyViewGroup extends FrameLayout {
    private Scroller scroller;
    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
        // 延时看的清楚一点
        postDelayed(new Runnable() {
            @Override
            public void run() {
                scroller.startScroll(0, 0, 100, 100, 2000);
                invalidate();
            }
        }, 1000);
    }
    public MyViewGroup(Context context) {
        super(context);
    }
    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            invalidate();
        }
    }
}

例子实现的效果果然是蓝色块向上和向左移动了100。

动画滑动

采用动画也可以实现View的滑动,主要操作translationX和translationY属性。简单介绍一下动画。
动画分两种:传统动画和属性动画。

传统动画分为帧动画(Frame Animation)和补间动画(Tweened Animation)。
帧动画是最容易实现的一种动画,这种动画更多的依赖于完善的UI资源,他的原理就是将一张张单独的图片连贯的进行播放,从而在视觉上产生一种动画的效果;有点类似于某些软件制作gif动画的方式。

补间动画又可以分为四种形式,分别是 alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转)。
补间动画的实现,一般会采用xml 文件的形式;代码会更容易书写和阅读,同时也更容易复用。

使用传统补间动画做View的滑动,滑动的是View的影像,并非真正改变View的位置。滑动结束瞬间,View便回到初始状态。如果设置了fillAfter属性为true,View会保留到动画后的状态,但是位置依然没有改变。如果你给这个View设置点击事件,此时点击这个View,会发现这个View不会响应点击事件,因为点击事件的范围还保留在原始状态的位置范围。所以给View设置了事件的,不适合使用传统动画。

属性动画是Android3.0之后才有的,不过有兼容包(nineoldandroids)。属性动画容易操作,代码简单,不会出现传统动画上面的负面影响,所以属性动画肯定是不二之选。

改变布局滑动

改变布局滑动,其实就是设置View的LayoutParams,设置View的padding或者margin来改变View的位置,达到View滑动的效果。和scrollTo/scrollBy不同的是,改变布局,其实是改变View的位置,而scrollTo/scrollBy改变的是View的内容,不过也是瞬间完成,体验效果不过,通常配合动画使用或者利用延时机制使用。

滑动冲突

分2中情况分析滑动冲突
(1)外部滑动和内部滑动不一致。
(2)外部滑动和内部滑动一致。

如果不懂View事件分发和拦截的,请参考Android View - 事件分发,拦截,处理机制,因为处理滑动冲突,需要理解View事件分发和拦截。

滑动不一致
这种情况比较容易解决,只要外部重写onInterceptTouchEvent拦截事件时,判断滑动方向,如果符合自己的滑动方向的,就拦截下来。如果不符合,则不拦截,交给内部View处理滑动。

滑动一致
这种情况稍微复杂,因为滑动方向一样。解决这种冲突的方案,一般是外部不拦截,先把事件交给内部,内部判断符合自己的条件,重写onTouchEvent处理事件,并返回true,事件到此结束。如果不符合自己的条件,重写onTouchEvent不处理事件,并返回false,交给外部处理。

处理滑动冲突还是比较复杂的,需要我们不断积累经验,才能处理好,慢慢体会吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值