【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);
            }
        }
    }
};
相关推荐
阿巴斯甜18 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker18 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952719 小时前
Andorid Google 登录接入文档
android
黄林晴20 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android