【Android】View 的滑动

View 的滑动是 Android 实现自定义控件的基础,同时在开发中我们也难免会遇到 View 的滑动处理。其实不管是哪种滑动方式,其基本思想都是类似的:当点击事件传到 View 时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。

实现 View 滑动有很多种方法,在这里主要讲解6种滑动方法,分别是 layout()、offsetLeftAndRight() 与 offsetTopAndBottom()、LayoutParams、Animation、scollTo() 与 scollBy(),以及 Scroller。

一、layout() 方法

View 进行绘制的时候会调用 onLayout() 方法来设置显示的位置,因此我们同样也可以通过修改 View 的 left、top、right、bottom 这4种属性来控制 View 的坐标。首先我们要自定义一个 View,在 onTouchEvent() 方法中获取触摸点的坐标,代码如下所示:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    // 获取手指触摸点的横坐标和纵坐标
    val x = event?.x?.toInt()
    val y = event?.y?.toInt()

    when (event?.action) {
        MotionEvent.ACTION_DOWN -> {
            lastX = x ?: 0
            lastY = y ?: 0
        }

        ...
    }

    ...
}

接下来我们在 ACTION_MOVE 事件中计算偏移量,再调用 layout() 方法重新放置这个自定义 View 的位置即可。

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            // 调用 layout 方法来重新放置它的位置
            layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
        }
    }

    ...
}

在每次移动时都会调用 layout() 方法对屏幕重新布局,从而达到移动 View 的效果。自定义 View 的全部代码如下所示:

class CustomView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {

    private var lastX = 0
    private var lastY = 0

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        // 获取手指触摸点的横坐标和纵坐标
        val x = event?.x?.toInt()
        val y = event?.y?.toInt()

        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x ?: 0
                lastY = y ?: 0
            }

            MotionEvent.ACTION_MOVE -> {
                // 计算移动的距离
                val offsetX = x ?: (0 - lastX)
                val offsetY = y ?: (0 - lastY)

                // 调用 layout 方法来重新放置它的位置
                layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
            }
        }

        return true
    }
}

随后,我们在布局中引用自定义 View 就可以了:

<com.tyhoo.android.demo.CustomView
    android:id="@+id/test_view"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@android:color/holo_red_light"
    ... />

运行程序,效果如图1所示:
请添加图片描述

图1

图1中的方块就是我们自定义的 View,它会随着我们手指的滑动改变自己的位置。

二、offsetLeftAndRight() 与 offsetTopAndBottom()

这两种方法和 layout() 方法的效果差不多,其使用方式也差不多。我们将 ACTION_MOVE 中的代码替换成如下代码:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            // 对 left 和 right 进行偏移
            offsetLeftAndRight(offsetX)
            // 对 top 和 bottom 进行偏移
            offsetTopAndBottom(offsetY)
        }
    }

    ...
}

三、LayoutParams

LayoutParams 主要保存了一个 View 的布局参数,因此我们可以通过 LayoutParams 来改变 View 的布局参数从而达到改变 View 位置的效果。同样,我们将 ACTION_MOVE 中的代码替换成如下代码:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            val layoutParams = layoutParams as ConstraintLayout.LayoutParams
            layoutParams.leftMargin = left + offsetX
            layoutParams.topMargin = top + offsetY
            setLayoutParams(layoutParams)
        }
    }

    ...
}

因为父控件是 ConstraintLayout,所以我们用了 ConstraintLayout.LayoutParams。如果父控件是 RelativeLayout,则要使用RelativeLayout.LayoutParams。除了使用布局的 LayoutParams 外,我们还可以用 ViewGroup.MarginLayoutParams 来实现:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            val layoutParams = layoutParams as ViewGroup.MarginLayoutParams
            layoutParams.leftMargin = left + offsetX
            layoutParams.topMargin = top + offsetY
            setLayoutParams(layoutParams)
        }
    }

    ...
}

四、Animation

可以采用 View 动画来移动,在 res 目录新建 anim 文件夹并创建 translate.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

接下来在 Kotlin 代码中调用就好了,代码如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<CustomView>(R.id.test_view)
        testView.animation = AnimationUtils.loadAnimation(this, R.anim.translate)
    }
}

运行程序,效果如图2所示:
请添加图片描述

图2

运行程序,我们设置的方块会向右平移300像素,然后又会回到原来的位置。为了解决这个问题,我们需要在 translate.xml 中加上 fillAfter=“true”,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

运行程序,效果如图3所示:
请添加图片描述

图3

运行代码后会发现,方块向右平移300像素后就停留在当前位置了。

需要注意的是,View 动画并不能改变 View 的位置参数。如果对一个 View 进行如上的平移动画操作,当 View 平移300像素停留在当前位置时,我们点击这个 View 并不会触发点击事件,但在我们点击这个 View 的原始位置时却触发了点击事件。对于系统来说这个 View 并没有改变原有的位置,所以我们点击其他位置当然不会触发这个 View 的点击事件。

五、scrollTo() 与 scollBy()

scrollTo(x, y) 表示移动到一个具体的坐标点,而 scrollBy(dx, dy) 则表示移动的增量为 dx、dy。其中,scollBy 最终也是要调用 scollTo 的。View 的 scollTo 和 scollBy 的源码如下所示:

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);
}

scollTo、scollBy 移动的是 View 的内容,如果在 ViewGroup 中使用,则是移动其所有的子 View。我们将 ACTION_MOVE 中的代码替换成如下代码:

override fun onTouchEvent(event: MotionEvent?): Boolean {
    ...

    when (event?.action) {
        ...

        MotionEvent.ACTION_MOVE -> {
            // 计算移动的距离
            val offsetX = x ?: (0 - lastX)
            val offsetY = y ?: (0 - lastY)

            (parent as View).scrollBy(-offsetX, -offsetY)
        }
    }

    return true
}

这里若要实现自定义 View 随手指移动的效果,就需要将偏移量设置为负值。为什么要设置为负值呢?这是参考对象不同导致的差异。所以我们用 scrollBy 方法的时候要设置负数才会达到自己想要的效果。

六、Scroller

我们在用 scollTo/scollBy 方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller 本身是不能实现 View 的滑动的,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果。在这里我们实现自定义 View 平滑地向右移动。首先我们要初始化 Scroller,代码如下所示:

class CustomView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {

    ...
    
    private var scroller: Scroller? = null

    init {
        scroller = Scroller(context)
    }

    ...
}

接下来重写 computeScroll() 方法,系统会在绘制 View 的时候在 draw() 方法中调用该方法。在这个方法中,我们调用父类的 scrollTo() 方法并通过 Scroller 来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate() 方法不断地进行重绘,重绘就会调用 computeScroll() 方法,这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。

override fun computeScroll() {
    super.computeScroll()
    scroller?.let {
        if (it.computeScrollOffset()) {
            (parent as View).scrollTo(it.currX, it.currY)
            invalidate()
        }
    }
}

我们在自定义 View 中写一个 smoothScrollTo 方法,调用 Scroller 的 startScroll() 方法,在 2000ms 内沿 X 轴平移 delta 像素,代码如下所示:

fun smoothScrollTo(destX: Int, destY: Int) {
    val scrollX = scrollX
    val delta = destX - scrollX
    scroller?.startScroll(scrollX, 0, delta, 0, 2000)
    invalidate()
}

最后我们再调用自定义 View 的 smoothScrollTo() 方法。这里我们设定自定义 View 沿着 X 轴向右平移 400 像素。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val testView = findViewById<CustomView>(R.id.test_view)
        testView.smoothScrollTo(-400, 0)
    }
}

运行程序,效果如图4所示:
请添加图片描述

图4

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/425307.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

行为树入门:BehaviorTree.CPP Groot2练习(前置后置条件)(3)

前置与后置条件理论 前置条件 例程&#xff1a; //hp_get叶节点class hp_get : public BT::SyncActionNode{public:hp_get(const std::string& name, const BT::NodeConfig& config) :BT::SyncActionNode(name, config){}// 给该节点申明端口static BT::PortsList pro…

使用ChatGPT写代码靠谱吗?

原文链接&#xff1a;使用ChatGPT写代码靠谱吗&#xff1f; 写在前面 对于ChatGPT从我们“惊讶”到现在已经快一年多了&#xff0c;但是&#xff0c;对于个人来说&#xff0c;使用还是比较少的。更确切的来说&#xff0c;也许有些同学是没有使用过。 ChatGPT功能确实比较强大…

代码随想录算法训练营第三十天| 回溯篇总结

文章目录 前言一、组合问题二、切割问题三、子集问题四、排列问题五、性能分析总结 前言 回溯法就是暴力搜索&#xff0c;并不是什么高效的算法&#xff0c;最多再剪枝一下。 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合 排列问题&#xff1a;N个数按一定规则全…

快速上手vercel,免费部署上线你的前端项目,3分钟学会

在你还不了解 vercel的时候&#xff0c;我已经部署了三个自己的项目了&#xff0c;都是免费&#xff0c;而且实时同步github并更新&#xff0c;当然也可以你自己本地直接部署到vercel&#xff0c;不经过github&#xff0c;今天三分钟教会你&#xff0c;但是注意&#xff1a;国内…

花28块,薅自己的羊毛!我错在哪里?

这些年返利类型的网站也是参差不齐&#xff0c;他就发现了赚钱的大门 而这个人就是我的大姨&#xff0c;他的喜好和别人不一样&#xff0c; 特别爱薅羊毛&#xff0c;但他都不知道我也被他拉在那个群里了&#xff01; 她对自媒体颇感兴趣&#xff0c;并创建了一个团体的小群…

Vue+SpringBoot打造大学计算机课程管理平台

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 实验课程档案模块2.2 实验资源模块2.3 学生实验模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 实验课程档案表3.2.2 实验资源表3.2.3 学生实验表 四、系统展示五、核心代码5.1 一键生成实验5.2 提交实验5.3 批阅实…

从0开始回顾Mysql --- MySQL初体验

大白话从0开始回顾MySQL&#xff0c;去除了一些繁琐的操作的演示以及内容&#xff0c;如MySQL安装等&#xff0c;本篇文章适合复习MySQL语法&#xff0c;学习MySQL语句&#xff0c;对MySQL不太熟练的同学&#xff0c;希望对大家有一些帮助。 MySQL初体验 首先&#xff0c;我将…

C语言----冒泡排序进阶

冒泡排序大家应该到写过吧。但大家可能知道到的冒泡排序有两种方法。而我呢&#xff0c;最近学习到了另外一种方法&#xff0c;现在知道三种方法了。所以想与大家分享一下。但是缺点是第三种是第二种的自实现版。第一种就是我们平常写的普通冒泡排序。第二种就是qsort。第三种就…

剑指offer刷题记录Day2 07.数组中重复的数字 ---> 11.旋转数组的最小数字

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 1、重建二叉树①代码实现&#xff08;带注释&am…

MQL5学习之简单移动平均线MA的编写

昨天还是有点高估自己了&#xff0c;MACD相对较难一点&#xff0c;改学MA的编写&#xff0c;首先明确MA的计算&#xff0c;假如有4个值&#xff0c;p[1&#xff0c;2&#xff0c; 3&#xff0c; 4], period3, 则v[0]p[0], v[1]p[1],v[2](p[0]p[1]p[2])/32, v[3](v[2]*3p[3]-p…

rust多个mod文件引用和文件夹mod使用注意事项

如果mod文件都在同一级目录&#xff0c;则直接使用就可以&#xff0c;因为rust文件都是一个隐藏的mod&#xff0c;但是如果mod文件在另外一个目录下面&#xff0c;就需要在目录下面声明一个mod.rs文件&#xff0c;这样才能将那个目录识别为一个mod&#xff0c;可以在mod.rs里面…

分布式事务详解-高频面试题

分布式事务都有哪些 其实说到分布式事务 我们不得不提事务的分类 事务可以分为本地事务&#xff0c;和分布式事务&#xff0c; 本地事务就是单体系统下基于数据库的ACID来实现的事务&#xff0c;而分布式事务是指在分布式环境下保证多个系统事务一致性的问题 而分布式事务 其…

初阶数据结构之---栈和队列(C语言)

引言 在顺序表和链表那篇博客中提到过&#xff0c;栈和队列也属于线性表 线性表&#xff1a; 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构。线性表在逻辑上是线性结构&#xff0c;也就是说是连…

Java项目:33 基于Java Web的网上拍卖系统(含源码数据库+文档免费送)

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 主要功能包括&#xff1a; 1.前台模块 &#xff08;1&#xff09;普通用户登录/注册。 &#xff08;2&#xff09;分类查看商品(普通商品与促销商品…

集成2.5G/5G/10G高速率网络变压器的RJ45网口连接器产品特点介绍

Hqst华轩盛(石门盈盛)电子导读&#xff1a;集成2.5G/5G/10G高速率网络变压器的RJ45网口连接器产品特点介绍&#xff1a; 第一、 高速率&#xff1a;支持高达2.5Gbps、5Gbps和10Gbps的传输速率&#xff0c;能够满足高带宽的网络应用需求。 第二、 集成2.5G/5G/10G高速率网…

【C++】set、multiset与map、multimap的使用

目录 一、关联式容器二、键值对三、树形结构的关联式容器3.1 set3.1.1 模板参数列表3.1.2 构造3.1.3 迭代器3.1.4 容量3.1.5 修改操作 3.2 multiset3.3 map3.3.1 模板参数列表3.3.2 构造3.3.3 迭代器3.3.4 容量3.3.5 修改操作3.3.6 operator[] 3.4 multimap 一、关联式容器 谈…

Vue开发实例(五)修改项目入口页面布局

修改项目入口 一、创建新入口二、分析代码&#xff0c;修改入口三、搭建项目主页面布局1、Container 布局容器介绍2、创建布局3、布局器铺满屏幕4、创建Header页面5、加入Aside、Main和Footer模块 一、创建新入口 创建新的入口&#xff0c;取消原来的HelloWorld入口 参考代码…

测试需求平台9-Table 组件应用产品列表优化

✍此系列为整理分享已完结入门搭建《TPM提测平台》系列的迭代版&#xff0c;拥抱Vue3.0将前端框架替换成字节最新开源的arco.design&#xff0c;其中约60%重构和20%新增内容&#xff0c;定位为从 0-1手把手实现简单的测试平台开发教程&#xff0c;内容将囊括基础、扩展和实战&a…

文件操作和IO(2):Java中操作文件

目录 一、File的属性 二、File的构造方法 三、File的方法 四、代码示例 1、getName&#xff0c;getParent&#xff0c;getPath方法 2、getAbsolutePath&#xff0c;getCanonicalPath方法 3、exists&#xff0c;isDirectory&#xff0c;createNewFile方法 4、createNewF…

【04】C语言括号匹配问题

欢迎来到土土的博客~&#x1f973;&#x1f973;&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff1a;大耳朵土土垚的博客 &#x1f4a5; 所属专栏&#xff1a;C语言系列函数实现 题目描述&#xff1a; 给定一个只包括 ‘(’&#xff0c;‘)’&#xf…