【Android】View滑动的实现

一、View的位置参数

在了解View滑动的实现之前,我们先要了解View的位置参数

View的位置主要由由四个顶点来决定,分别对应于四个属性:top、left、right、bottom

复制代码
top:左上角纵坐标
left:左上角横坐标
right:右下角横坐标
bottom:右下角纵坐标

这些参数都是相对于View的父容器的坐标,是一种参数坐标

在Android中x轴和y轴的正方向为右和下

View位置坐标和父容器的关系如下图:

由此我们可以得到View宽高和坐标的关系

复制代码
width=right-left
height=bottom-top

我们可以通过方法来获取位置参数

java 复制代码
left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();

除此之外,还有几个额外参数

复制代码
x,y:View左上角的坐标
translationX和translationY:左上角相对于父容器的偏移量
x=left+translationX
y=top+translationY

View在平移时top和left表示左上角原始信息,其值不会改变

x,y,translationX和translationY会发生改变

二、实现View的滑动

scrollTo/scrollBy

scrollTo和scrollBy是View自己提供的专门用于实现此功能的两个方法,以下是这两个方法的源码

java 复制代码
// View.java 中的相关源码

/**
 * 实现View内容的绝对滑动
 * @param x 目标位置的水平滚动偏移量
 * @param y 目标位置的垂直滚动偏移量
 */
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();
        }
    }
}

/**
 * 实现View内容的相对滑动
 * @param x 水平方向要滚动的距离
 * @param y 垂直方向要滚动的距离
 */
public void scrollBy(int x, int y) {
    // 调用scrollTo,在当前基础上累加偏移量
    scrollTo(mScrollX + x, mScrollY + y);
}

由上我们发现,scrollBy实际是通过调用scrollTo实现的,scrollTo是基于所传递参数的绝对滑动,而scrollBy是基于当前位置的相对滑动。

我们重点了解一下scrollTo方法中的两个参数:

复制代码
mScrollX:View左边缘和View内容左边缘在水平方向上的距离,单位为像素,可通过getScrollX方法得到

mScrollY:View上边缘和View内容上边缘在竖直方向上的距离,单位为像素,可通过getScrollY方法得到

根据上面的分析,我们可以知道,scrollTo/scrollBy只能将View的内容进行移动,不能改变View本身的位置。

在onTouchEvent中处理

1.layout方法

view进行绘制时会调用onLayout()方法设置显示的位置,因此我们可以通过改变View的left、top、right、bottom这四种属性实现移动

java 复制代码
@Override
    public boolean onTouchEvent(MotionEvent event) {
        // 获取触摸点的x,y
        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 moveX = x - lastX;
                int moveY = y - lastY;
                //  更改位置
                layout(getLeft() + moveX, getTop() + moveY,
                        getRight() + moveX, getBottom() + moveY);
                break;
        }
        return true;
    }

2.offsetLeftAndRight()和offsetTopAndBottom

这两种方法分别可以实现对View左右、上下移动的操作,传入的参数是各个方向的偏移量

java 复制代码
@Override
    public boolean onTouchEvent(MotionEvent event) {
        // 获取触摸点的x,y
        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 moveX = x - lastX;
                int moveY = y - lastY;
                //  更改位置
                offsetLeftAndRight(moveX);
                offsetTopAndBottom(moveY);
                break;
        }
        return true;
    }

使用动画

通过改变View的translationX和translationY属性对View进行平移,这里使用属性动画完成,因为属性动画真正改变了View的位置属性。

属性动画主要有以下两种实现方式:

ObjectAnimator

ObjectAnimator 可以对 ViewtranslationXtranslationY 属性进行动画设置,从而实现水平方向和垂直方向的平移效果。translationXtranslationY 是相对于 View 的初始位置的偏移量,单位是像素。

java 复制代码
// 水平方向平移动画,向右平移100像素,动画时长500毫秒
ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).setDuration(500);; 
animatorX.start();

// 垂直方向平移动画,向下平移100像素
ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", 0f, 100f).setDuration(500); 
animatorY.start();

ValueAnimator

ValueAnimator最基本的属性动画类,通过值变化驱动动画

java 复制代码
// 基本用法
ValueAnimator animator = ValueAnimator.ofFloat(0f, 360f);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float value = (float) animation.getAnimatedValue();
        // 使用计算出的值更新View属性
        view.setRotation(value);
    }
});
animator.start();

改变布局参数

LayoutParams 是 Android 中用于描述 View 在父容器中布局参数的类。它包含了 View 的大小、位置、边距等布局信息,我们可以通过改变View的布局参数实现滑动。

java 复制代码
// 通过getLayoutParams()获取布局参数
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
//修改布局参数属性
params.leftMargin += dx;
params.topMargin += dy;

//设置新的LayoutParams
view.setLayoutParams(params);
//或view.requestLayout();请求重新布局

三、View的弹性滑动

简单的滑动实现效果比较生硬,用户的体验效果较差,因此我们要在此基础上实现渐进式滑动,以优化滑动效果。

使用Scroller

Scroller是弹性滑动对象,用于实现View的弹性滑动,即有过渡效果的滑动,需要和View的computeScroll方法配合使用。

使用方法

java 复制代码
//初始化Scroller对象
Scroller scroller=new Scroller(mContext);
//调用startScroll,开始平滑滚动
private void smoothScrollTo(int destX,int destY){
   int scrollX=getScrollX();
   int delta=destX-scrollX;
   //1000ms内滚动到指定位置,效果为缓慢滑动
   mScroller.startScroll(scrollX,0,delta,0,1000);
   invalidate();//触发重绘
}
//重写computeScroll()方法,绘制期间会不断调用computeScroll()实现滑动
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();//再次触发重绘
    }
}

原理:

startScroll方法本身不处理滑动,只保存我们传递的参数,真正实现弹性滑动的是invalidate()方法。

invalidate()会导致事件重绘,去调用View的computeScroll(),而computeScroll()本身为空实现,需要重写。我们在computeScroll()内部调用 scrollTo实现滑动,之后调用postInvalidate()再次重绘,又会调用computeScroll()方法,如此反复,直到滑动结束。

在computeScroll()方法中,我们还使用了computeScrollOffset(),这个方法用来判断滑动是否结束。

动画

动画实现的滑动天然具有弹性效果,我们可以通过上述的ObjectAnimator实现属性动画,通过设置滑动的时长获得想要的效果。

java 复制代码
// 水平方向平移动画,向右平移100像素,动画时长500毫秒
ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).setDuration(500);; 
animatorX.start();

延时策略

核心思想:通过发送一系列延时消息从而达到渐进式效果,可以使用Handler、View的postDelayed方法或线程的sleep方法实现。

java 复制代码
//postDelayed方法实现
view.postDelayed(new Runnable() {
    @Override
    public void run() {
        smoothScrollTo(100, 0); 
    }
}, 300);
java 复制代码
//Handler实现,此种方法无法精确定时,因为系统调度需要时间
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount=0;
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if (msg.what == MESSAGE_SCROLL_TO) {
            mCount++;
            
            if (mCount <= FRAME_COUNT) {
                // 使用弹性公式计算进度
                float fraction = mCount / (float) FRAME_COUNT;
                // 计算当前位置并滚动
                int scrollX = (int) (fraction * 100);
                mView.scrollTo(scrollX, 0);
                
                // 继续下一帧
         mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
            }
        }
    }
};
相关推荐
芝麻开门-新起点8 小时前
Android 和 iOS 系统版本及开发适配
android·ios·cocoa
2501_915918419 小时前
iOS描述文件功能解析
android·macos·ios·小程序·uni-app·cocoa·iphone
用户69371750013849 小时前
一文彻底搞懂 Android 依赖配置:implementation vs testImplementation,再也不混淆!
android
TimeFine11 小时前
Android WebView暗夜模式适配
android
studyForMokey11 小时前
【Android Activity】生命周期深入理解
android·kotlin
浅影歌年11 小时前
Android 嵌入h5顶部状态栏空白
android
来来走走13 小时前
kotlin学习 lambda编程
android·学习·kotlin
无知的前端14 小时前
一文精通-Kotlin中双冒号:: 语法使用
android·kotlin
Andy15 小时前
Mysql基础2
android·数据库·mysql