【Android入门02】教你从0实现一个手势解锁页面——Let‘s unveil the world

演示gif

前言

上一篇文章中,我们已经学会了如何从0开发一个计算器应用。今天我们来学习一下,如何实现一个手势解锁页面。手势解锁是非常常用的解锁方式,方便快捷,在安全性上也不弱于常规的密码解锁,在日常生活中有很广泛的应用。
本文主要分为界面搭建和实现逻辑两个部分。

界面搭建

图片资源

手势解锁页面一共需要准备的图片资源有:圆点(未选中状态)、圆点(已选中状态)、横线、竖线、斜线(左斜线、右斜线)。(图片资源详见文末github链接)

搭建dotsContainer

首先用一个正方形的布局dotsContainer,把所有的圆点给框住。将该布局的约束连接上父布局,宽度和高度设置为0dp(匹配约束)。
dotsContainer

<androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/dotsContainer"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/dark_green"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

谷歌开发者文档中对匹配约束的描述
设置尺寸比和宽度高度为匹配约束

通过阅读谷歌开发者文档,我们可以看到通过设置匹配约束(Match Constraints)可以将布局向外拉伸,以尽可能地匹配约束。此外在宽度和高度均为匹配约束的条件下,可以设置宽度与高度的比例。这里我们设置宽度和高度的比例为1:1。
设置高度宽度比例

布局圆点(未选中)

这里我们使用和上一篇文章一样的技巧,对所有横排的圆点设置horizontal chain,所有竖排圆点设置vertical chain。
设置纵向约束
设置横向约束

布局圆点(已选中)

由于圆点(未选中)的约束已经设置完毕,所以我们在设置圆点(已选中)的约束时就轻松了许多。只要将圆点(已选中)在垂直方向和水平方向上与圆点(未选中)对齐即可。
设置圆点(已选中)的约束
这里我们设置圆点(已选中)的可见性为不可见(invisible)。因为在手指没有划过圆点的时候,圆点应该是呈未选中状态。
设置圆点(未选中)的可见性
这里通过阅读源码我们可以看到,***android:visibility属性一共有3个值,默认值是visible(可见),invisible是不显示但是占据屏幕空间,gone是既不显示也不占据空间,就好像从来没有添加过该控件一样。***一般情况下,如果我们需要设置控件为不可见,设置为invisible即可。

 <!-- Controls the initial visibility of the view.  -->
        <attr name="visibility">
            <!-- Visible on screen; the default value. -->
            <enum name="visible" value="0" />
            <!-- Not displayed, but taken into account during layout (space is left for it). -->
            <enum name="invisible" value="1" />
            <!-- Completely hidden, as if the view had not been added. -->
            <enum name="gone" value="2" />
        </attr>

这里还有一个小细节需要注意,我们应该将圆点(已选中)设置在圆点(未选中)的上层。如果圆点(已选中)是被圆点(未选中)覆盖住了的话,哪怕将圆点(已选中)的可见性设置为visible,在屏幕上也看不到这个控件。
设置控件间的层次关系

布局横线、竖线、斜线

为所有的横线、竖线、斜线添加约束,并设置可见性为invisible。这里的操作比较繁琐,博主自己也搞了一个多小时,此处不做过多赘述。如果没法成功添加约束,可以到github代码中直接复制粘贴这部分的代码。这里分别给出横线、竖线、斜线的示范代码,可以尝试着看看能不能独立完成。添加约束是个技术活,不积硅步无以至千里。
横线示范代码

<ImageView
            android:id="@+id/line23"
            android:layout_width="90dp"
            android:layout_height="90dp"
            android:tag="23"
            android:visibility="invisible"
            app:layout_constraintBottom_toBottomOf="@+id/dot2_unpicked"
            app:layout_constraintEnd_toEndOf="@+id/dot3_unpicked"
            app:layout_constraintStart_toStartOf="@+id/dot2_unpicked"
            app:layout_constraintTop_toTopOf="@+id/dot2_unpicked"
            app:srcCompat="@drawable/line_horizontal" />

竖线示范代码

<ImageView
            android:id="@+id/line14"
            android:layout_width="90dp"
            android:layout_height="90dp"
            android:tag="14"
            android:visibility="invisible"
            app:layout_constraintBottom_toBottomOf="@+id/dot4_unpicked"
            app:layout_constraintEnd_toStartOf="@+id/line12"
            app:layout_constraintStart_toStartOf="@+id/dot1_unpicked"
            app:layout_constraintTop_toTopOf="@+id/dot1_unpicked"
            app:srcCompat="@drawable/line_vertical" />

斜线示范代码

 <ImageView
            android:id="@+id/line15"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:tag="15"
            android:visibility="invisible"
            app:layout_constraintBottom_toTopOf="@+id/line58"
            app:layout_constraintEnd_toEndOf="@+id/dot2_unpicked"
            app:layout_constraintStart_toStartOf="@+id/dot4_unpicked"
            app:layout_constraintTop_toTopOf="@+id/dot1_unpicked"
            app:srcCompat="@drawable/line_right" />
重中之重

这里需要给除了圆点(未选中)之外的所有控件设置一个tag值,该圆点是第几个圆点tag值就设置为多少,该连线两端的圆点是几号圆点连线的tag值就设置为多少。tag值和id值保持一致。至于tag值的应用,在实现逻辑部分会着重讲到。线段的tag值
圆点的tag值

实现逻辑

基本界面搭建完毕后,就来到了最关键的实现逻辑部分。

基本实现思路

当手指点击某个按钮,或者划过某个按钮时,将屏幕上该区域对应的圆点 / 线段的可见性设置为visible。
将用户第一次在屏幕上绘制的路线记录起来,设置为密码。若第二次绘制的路线和该路线一样(绘制顺序不用完全一样,只要最终绘制出来的路线是一样的即可),则提示用户解锁成功。

实现细节
1.如何捕捉用户在屏幕上进行的操作?

可以通过重写onTouchEvent()方法实现。通过阅读源码我们可以看到,当一个触摸事件未被处理时就会调用onTouchEvent()。如果该点击事件已经被消费则return true,未被消费则return false。默认实现是return false。

    /**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
     * outside of your window bounds, where there is no view to receive it.
     *
     * @param event The touch screen event being processed.
     *
     * @return Return true if you have consumed the event, false if you haven't.
     * The default implementation always returns false.
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

这么说可能还不太好理解。由于这里涉及到安卓中的触摸事件传递机制,篇幅过长且对于初学者来说并不友好,所以此处不展开描述。(附上一篇相关博客供读者学习:触摸事件传递机制)这里你可以简单理解为重写onTouchEvent()可以对触摸事件进行处理,最后返回true表示处理完毕即可。

2.如何在onTouchEvent()中书写处理逻辑

onTouchEvent()接受的参数为MotionEvent类型(移动事件),我们来看看相关的源码。常用的属性如下:ACTION_DOWN表示手指点击屏幕的动作,ACTION_UP表示手指从屏幕上松开的动作,ACTION_MOVE表示手指在屏幕上移动的动作。(介于down和up的中间状态)

    /**
     * Constant for {@link #getActionMasked}: A pressed gesture has started, the
     * motion contains the initial starting location.
     * <p>
     * This is also a good time to check the button state to distinguish
     * secondary and tertiary button clicks and handle them appropriately.
     * Use {@link #getButtonState} to retrieve the button state.
     * </p>
     */
    public static final int ACTION_DOWN             = 0;

    /**
     * Constant for {@link #getActionMasked}: A pressed gesture has finished, the
     * motion contains the final release location as well as any intermediate
     * points since the last down or move event.
     */
    public static final int ACTION_UP               = 1;

    /**
     * Constant for {@link #getActionMasked}: A change has happened during a
     * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
     * The motion contains the most recent point, as well as any intermediate
     * points since the last down or move event.
     */
    public static final int ACTION_MOVE             = 2;

我们可以通过event的getAction()方法来获取当前正在被执行的动作,并用一个when()语句来对每一个不同的动作进行相应的操作。(类似于Java中的switch语句)

    /**
     * Return the kind of action being performed.
     * Consider using {@link #getActionMasked} and {@link #getActionIndex} to retrieve
     * the separate masked action and pointer index.
     * @return The action, such as {@link #ACTION_DOWN} or
     * the combination of {@link #ACTION_POINTER_DOWN} with a shifted pointer index.
     */
    public final int getAction() {
        return nativeGetAction(mNativePtr);
    }

这里给出重写onTouchEvent的代码模板:

override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                手指点击屏幕时要执行的操作
            }
            MotionEvent.ACTION_UP -> {
                手指从屏幕上松开时要执行的操作
            }
            MotionEvent.ACTION_MOVE -> {
                手指在屏幕上移动时要执行的操作
        }
        return true
    }
在onTouchEvent()中,如何处理ACTION_DOWN、ACTION_MOVE、ACTION_UP

在ACTION_DOWN中,我们要做的事情如下:
1.判断当前触摸点是否是圆点。
2.用一个变量记录当前触摸点。
3.如果当前触摸点确实是一个圆点,则将该圆点设置为选中状态(将圆点(已选中)控件的可见性设置为visible)。用一个变量flag记录当前是设置密码阶段还是检验密码阶段(一共就只有2个阶段,默认的流程是设置密码——检验密码——设置密码——检验密码,依此类推无限循环),并把该圆点的标签和对应的View保存起来。
这里我们开始用到刚刚在界面搭建阶段所强调的tag值。tag和id一样,可以起到标识控件的作用。***这里我们使用tag而不是用id的原因是,id往往需要“见名知义”,以方便开发者开发。***例如连接我们圆点1的id我们设置为dot1,tag值为1;连接圆点1和圆点2的线段id我们设置为line12,tag值为12。我们可以看到,tag值往往比id值更加的“纯粹”,他不需要包含一些其他字段来标识该控件的身份,只要表明该控件的属性即可,而标识控件身份的这件事则交给id来做。
具体代码如下:

val dotView = checkTouchView(event)
                lastSelectedView = dotView
                if (dotView != null) {
                    dotView.visibility = View.VISIBLE
                    if (!flag) {
                        setPassword.append(dotView.tag)
                        highlightedViewArray.add(dotView)
                    } else {
                        checkPassword.append(dotView.tag)
                        checkPasswordViewArray.add(dotView)
                    }
                }

在ACTION_MOVE中,我们要做的事情如下:
1.判断当前触摸点是否是圆点。
2.如果当前触摸点是圆点,且可见性为invisible,则继续执行下面的操作。(如果当前触摸点是圆点,但是可见性已经是visible,代表该圆点已经被点亮,也就是被我们使用过,此时无需再进行处理)。
3.判断当前圆点是否是第一个圆点(存在这种情况,我们在圆点外的区域点击屏幕,一路拖拽过来经过当前圆点),如果是的话点亮该圆点,保存该圆点的tag值和对应的View。如果当前圆点不是该第一个圆点,则我们应该点亮该圆点,以及该圆点和上一个圆点之间的线段。(如果该线段是合法的)。
这里我们应该如何进行线段合法性的判断呢?我们通过如下代码来计算出当前线段的tag值。这样计算是因为,我们对连接圆点7和圆点8的线段命名为“line78”,而不是“line87”,也就是说线段的id一定是线段两个端点所组成的数字中较小的那一个(78和87,取较小的那个数字作为id值)。

val lastTag = (lastSelectedView?.tag as String).toInt()
val currentTag = (dotView.tag as String).toInt()
val lineTag = min(lastTag, currentTag) * 10 + max(lastTag, currentTag)

当我们计算出当前线段的tag值后,只需要在保存所有合法线段tag值的集合中寻找有没有当前线段的tag值即可。(该集合是提前人为定义好的,是一个常量)。这里我们可以用contains()方法来快速判断集合中有无目标元素。如果该线段是合法的,则点亮该线段,点亮当前圆点,并对线段和圆点的View以及圆点tag值进行保存。
具体代码如下:

val dotView = checkTouchView(event)
if (dotView != null) {
	if (dotView.visibility == View.INVISIBLE) {
		if (lastSelectedView == null) {
			dotView.visibility = View.VISIBLE
            lastSelectedView = dotView
            if (!flag) {
            	setPassword.append(dotView.tag)
                highlightedViewArray.add(dotView)
            } else {
            	checkPassword.append(dotView.tag)
                checkPasswordViewArray.add(dotView)
            }
        } else {
        	val lastTag = (lastSelectedView?.tag as String).toInt()
            val currentTag = (dotView.tag as String).toInt()
            val lineTag = min(lastTag, currentTag) * 10 + max(lastTag, currentTag)
            if (lineTagArray.contains(lineTag)) {
            	val lineView = binding.dotsContainer.findViewWithTag<ImageView>(lineTag.toString())
	            lineView.visibility = View.VISIBLE
                dotView.visibility = View.VISIBLE
                lastSelectedView = dotView
                if (!flag) {
                	setPassword.append(dotView.tag)
                    highlightedViewArray.add(dotView)
                    highlightedViewArray.add(lineView)
                } else {
                    checkPassword.append(dotView.tag)
                    checkPasswordViewArray.add(dotView)
                    checkPasswordViewArray.add(lineView)
                }
            }
        }
    }
}            

在ACTION_UP中,我们要做的事情如下:
1.设置一个定时器,当松开手指一定时间后,就执行定时器中的内容。
2.将已经点亮所有控件设为不可见。
3.清空/重置所有相关资源
3.根据当前flag的状态,在Log中输出用户设置/输入的密码。如果当前是处于输入密码阶段,则判断输入的密码和设置的密码是否一致,并通过Toast告知用户。
这里我们着重讲讲如何判断用户输入的密码是否正确。之前我们也提到了,用户绘制图案的顺序无所谓,只要最终呈现出来的图案与密码对应的图案是一致的,就判定用户输入密码正确。例如【12369】和【96321】就可以判定为是同一个密码,因为他们对应的图案是一样的。刚刚我们用两个集合分别保存了用户第一次输入和第二次输入的密码对应的View,我们只需遍历其中一个集合,并检查该集合的元素是否包含在另一个集合即可。若有一个元素不包含,则证明2次输入不一致,此时判断密码错误;若遍历完整个集合都没有发现不包含的元素,并且2个集合的大小一样,证明这两个集合是完全一致的。
如果用==来判断两个集合是否相等,若两个集合中内容相等只是顺序不同,则会判定为这两个集合不一致,但是我们这里不需要考虑集合中内容的顺序,只需要考虑内容是否一样即可,所以还能这样实现。
这里有一个地方需要注意,在子线程中调用Toast.makeText()会报错:报错内容
解决方法如下:
解决方法
这里还需注意,我们应等到所有资源都清空后再重置子线程(调用Looper.loop()),否则资源不会得到合理释放,会影响下一次的运行结果。
清空资源后再重置子线程

具体代码如下:

Timer().schedule(object: TimerTask() {
	override fun run() {
		highlightedViewArray.forEach {
    	it.visibility = View.INVISIBLE
		}
        lastSelectedView = null
        if (!flag) {
        	Log.v("Mayberry's flag is $flag", "The password you set is $setPassword")
            flag = !flag
        } else {
            Log.v("Mayberry's flag is $flag", "The password you input is $checkPassword")
            var checkingResult = true
            Looper.prepare()
            highlightedViewArray.forEach {
            	if (!checkPasswordViewArray.contains(it)) {
                	checkingResult = false
                }
            }
        	if (checkingResult && highlightedViewArray.size == checkPasswordViewArray.size) {
            	Toast.makeText(applicationContext, "Your password is right", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(applicationContext, "Your password is wrong", Toast.LENGTH_LONG).show()
            }
            setPassword.clear()
            checkPassword.clear()
            highlightedViewArray.clear()
            checkPasswordViewArray.clear()
            flag = !flag
            Looper.loop()
        }
    }
}, 1000)
使用ViewBinding代替kotlin-android-extensions插件

kotlin-android-extensions插件已经被废弃了,官方更推荐开发者用ViewBinding进行代替。原因是kotlin-android-extensions插件的实现原理是为每一个activity新建一个HashMap来存储控件实例缓存,如果没有缓存,就用findViewById()来查找控件实例,并写入HashMap缓存,下次要获取该控件就直接从缓存中读取即可。 这样无形中增加了内存的负担和时间复杂度。
关于kotlin-android-extensions和ViewBinding的更多细节,可以参考郭霖大佬的文章:kotlin-android-extensions插件也被废弃了?扶我起来
谷歌开发者文档中对ViewBinding的描述

这里给出一段在Activity中使用Viewbinding的实例代码

private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    } 
dotsArray应该采用懒加载进行初始化

这里的dotsArray是用来存储每一个圆点的视图的,否则访问不到binding对象(binding对象要在activity的onCreate()方法调用完后才得到创建)。懒加载的好处是等到需要访问该对象时才去执行初始化的内容,并且只会执行一次。

val dotsArray: Array<ImageView> by lazy {
    arrayOf(
        binding.dot1, binding.dot2, binding.dot3, binding.dot4, binding.dot5,
        binding.dot6, binding.dot7, binding.dot8, binding.dot9
    )
}
如何判断当前触摸点是否在圆点区域内

首先我们应该知道,一个圆形的控件看起来是圆形的,实际上他是一个矩形。从图中可以很清晰地看到,每一个控件,不管是圆点还是线段,它实际上的面积都是一个矩形。
在这里插入图片描述
我们需要获取当前圆点所在的矩形区域,获取当前触摸点的区域,判断当前触摸点的区域是否在圆点所在区域之中。

获取当前圆点所在的矩形区域

这里我们可以创建一个Rect类的实例来表示该圆点所在的矩形区域。让我们来看看源码中Rect类的构造方法。
Rect类一共有四个属性,分别是left、top、right和bottom,分别对应着矩形区域的左边界、上边界、右边界和下边界。

    /**
     * Create a new rectangle with the specified coordinates. Note: no range
     * checking is performed, so the caller must ensure that left <= right and
     * top <= bottom.
     *
     * @param left   The X coordinate of the left side of the rectangle
     * @param top    The Y coordinate of the top of the rectangle
     * @param right  The X coordinate of the right side of the rectangle
     * @param bottom The Y coordinate of the bottom of the rectangle
     */
    public Rect(int left, int top, int right, int bottom) {
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
    }

我们再来看看将圆点视图转化为对应的Rect的代码应该怎么写。结合代码和图示,应该就很清晰了。这里要注意,在安卓中,Y轴的正方向是向下的,所以Rect对象的bottom这一属性应该是比top属性要更大的(数值上)。

//将某个视图坐标转化为Rect
private fun convert(dot: ImageView): Rect {
    return Rect(
        dot.x.toInt(),
        dot.y.toInt(),
        (dot.x + dot.width).toInt(),
        (dot.y + dot.height).toInt()
    )
}

在这里插入图片描述
为什么圆点的左上角的横坐标对应的是dot.x,纵坐标对应的是dot.y呢,我们继续来看看源码。

    /**
     * The visual x position of this view, in pixels. This is equivalent to the
     * {@link #setTranslationX(float) translationX} property plus the current
     * {@link #getLeft() left} property.
     *
     * @return The visual x position of this view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "drawing")
    public float getX() {
        return mLeft + getTranslationX();
    }

我们可以看到,这里getX()方法返回的其实是mLeft和getTranslationX()的和,那么这两个值又是什么呢,让我们进一步跟进。

    /**
     * The distance in pixels from the left edge of this view's parent
     * to the left edge of this view.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "layout")
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    protected int mLeft;

这里我们可以看到,mLeft其实就是当前控件距离父容器左侧的距离,如下图。
在这里插入图片描述
那getTranslationX()返回的又是什么呢?

    /**
     * The horizontal location of this view relative to its {@link #getLeft() left} position.
     * This position is post-layout, in addition to wherever the object's
     * layout placed it.
     *
     * @return The horizontal position of this view relative to its left position, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "drawing")
    @InspectableProperty
    public float getTranslationX() {
        return mRenderNode.getTranslationX();
    }

我们可以看到,getTranslationX()返回的其实是该控件相较于其左侧的距离(这里的左侧其实就是上面提到的mLeft),所以getTranslationX()的返回值其实是0。
综上所述,getX()方法返回的其实就是该控件与父容器左侧的距离mLeft,getY()方法也同理,这里不做过多赘述。

获取触摸点的坐标

通过上一步,我们已经得到了每个圆点对应的矩形区域的对象。***这里有一个细节需要注意,这个矩形对象中的坐标的值都是相对于其父容器而言的,所以我们在计算触摸点坐标时,也应该相对于圆点的父容器而言。***因为我们后续要使用Rect对象中的contains()方法来判断一个点是否在矩形区域内,所以就必须确保这个点和Rect对象的是基于统一标准的。
那么我们应该如何计算出触摸点相对于圆点父容器的位置呢?这里给出计算公式:触摸点相对于圆点父容器的高度 = 触摸点相对于屏幕的高度 - 手机顶部statusBar的高度(手机的刘海高度)- dotsContainer的高度
在这里插入图片描述
知道了公式,只需要获取所需要的数值往里面代入即可。我们先来看看如何获取当前触摸点相对于屏幕的高度。
在MotionEvent类中有一个geRawY()方法,返回的是触摸点相对于屏幕的位置。在MotionEvent中还有一个getY()方法,返回的是触摸点相对于父容器的位置,看上去这就是我们想要的,但是由于我们的触摸点不同,其对应的父容器也不同。

       /**
     * Returns the original raw Y coordinate of this event.  For touch
     * events on the screen, this is the original location of the event
     * on the screen, before it had been adjusted for the containing window
     * and views.
     *
     * @see #getY(int)
     * @see #AXIS_Y
     */
    public final float getRawY() {
        return nativeGetRawAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
    }

在这里插入图片描述
dotsContainer的高度可以用View的getY()方法来获取,这个方法在上文“将圆点转化为Rect”部分已详细分析过,此处不做过多赘述。

这里我们着重看一下,如何计算statusBar的高度。
这里给出计算公式:statusBar的高度 = 屏幕高度 - 可绘制区域高度 - 底部avigationBar的高度
在这里插入图片描述
这里我们通过windowManager类中的getCurrentWindowMetrics()方法来获取当前窗口,再通过getBounds()方法来获取该窗口的坐标,最后通过getHeight()方法获取当前窗口的高度。获取到当前窗口后,我们再通过id获取到可绘制区域。最后通过navigationBar的id来获取其高度。

fun getBarHeight(activity: Activity): Int {
    var screenHeight = 0
    screenHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        activity.windowManager.currentWindowMetrics.bounds.height()
    } else {
        val mMetrics = DisplayMetrics()
        activity.windowManager.defaultDisplay.getMetrics(mMetrics)
        mMetrics.heightPixels
    }
    //获取绘制区域尺寸
    val rect = Rect()
    val content = activity.window.findViewById<ViewGroup>(Window.ID_ANDROID_CONTENT)
    content.getDrawingRect(rect)
    //获取底部navigationBar的高度
    val resourceId = activity.resources.getIdentifier("navigation_bar_height", "dimen", "android")
    val navBarHeight = activity.resources.getDimensionPixelSize(resourceId)
    //bar的高度
    return screenHeight - rect.height() - navBarHeight
}

到这里,如何将圆点转化为对应的Rect,如何获取触摸点相对于父容器的坐标、如何判断当前触摸点是否在某个圆点的范围内 这三个步骤我们就都已经讲完了。附上完整代码

//将某个视图坐标转化为Rect
private fun convert(dot: ImageView): Rect {
    return Rect(
        dot.x.toInt(),
        dot.y.toInt(),
        (dot.x + dot.width).toInt(),
        (dot.y + dot.height).toInt()
    )
}

//判断触摸点是否在某个dot内
fun checkTouchView(event: MotionEvent): View? {
    //获取触摸点在容器中的坐标
    val point = getTouchPoint(event)
    //遍历保存9个点的数组
    dotsArray.forEach {
        //获取当前视图的Rect
        val dotRect = convert(it)
        //判断触点是否在矩形区域内
        val result = dotRect.contains(point)
        if (result) {
            return it
        }
    }
    return null
}

//计算触摸点相对于父容器的坐标
fun getTouchPoint(event: MotionEvent): Point {
    //相对于屏幕的高度 - 顶部Bar的高度 - 容器和内容区域顶部的高度
    val y = event.rawY - getBarHeight(activity) - binding.dotsContainer.y
    val touchPoint = Point()
    touchPoint.x = event.x.toInt()
    touchPoint.y = y.toInt()
    return touchPoint
}

写在最后

至此,这篇博客就算写完了。这次的内容比起上一次的开发计算器,在难度上有了不小的提升,包括如何存储密码及判断密码、如何对触摸事件做出响应、如何计算statusBar高度、如何判断触摸点是否在圆点范围内等,需要读者好好消化。
完整项目代码:gestureUnlock
其他文章:【Android入门01】教你从0开发一个计算器软件——say No to “Hello world”,say Hi to Calculator

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值