Android开发(9)——九宫格解锁页面

本节内容

1.demo布局分析

2.界面布局

3.布局9个点

4.数组保存圆点对象并设置tag

5.获取触摸点相对于容器的坐标

6.点亮点

7.获取密码

8.添加线

9.给线添加tag值并保存所有tag

10.点亮线

11.封装SharePreferencr

12.实现密码设置和解锁

13.限制可点击区域

14.添加第三方库支持圆形头像

15.从相册提取图片

一、demo布局分析
QQ图片20210318100842.png
  • 这个demo最后的效果差不多如上图所示。
在开始写代码之前,我们先把这次可能会用到的知识梳理一下。
1.最上方的图像是一张图片,点击这个图片,就可以去相册选择一张照片作为头像,而且这个头像必须是圆形的。
2.“指尖上的工程师”就是一个id,一个简单的textView。
3.再下面是一个文本,这个文本的内容会随着我们的操作而改变。
4.最下面的九个点,是由九张图片构成的。当鼠标移动到点上的时候,这个点会变成红色,实际上只不过是另外一张图片覆盖在了上面而已。
5.因为设置图案是在这九个点上设置的,那么我们就需要限制一下滑动区域在九个点所在的矩形区域内。
6.这个布局整体可以分为四部分,图片,文本,文本和九个点。我们把这九个点视作一个整体,那么这就是一个ViewGroup。内部就用ConstraintLayout来布局。
7.在判断某个点是不是在矩形框内部时,我们就要获取这个点的x,y坐标。但是直接获取的是相对屏幕的坐标,而我们想要的是相对于外部容器的坐标。所以我们就需要将其转换一下。那么我们只需要将矩形边框到屏幕的距离算出来即可。
8.点亮点的原理:其实所有的红点我们都已经加进去了,只不过我们将其隐藏起来了,当我要点亮它的时候,只要把它暴露出来即可。
二、界面布局
1.首先添加三个横向的guideline,将它分为四个部分。
2.然后在最上方部分拖拽一张图片进来。
3.第二部分添加一个TextView,这是用户的id,可以写死。
4.第三部分添加一个提示的TextView。
所有的约束都根据guideline来。
5.最后一部分是一个容器,所以我们直接拖拽一个ConstraintLayout进来。把这个容器设置为正方形,只需要在右侧的布局中,点击一下红方框框起来的部分即可, 让它长宽比为1:1.
image.png
进行完上面的操作后,差不多是下图的效果
image.png
三、布局九个点
1.拖拽一个ImageView到容器中,然后设置一下id为dot1,长宽都为60dp,然后复制两个出来。删掉一个约束,调正一下,把它们chain在一起。
2.布局好上面三个点之后,复制两个出来,再重新调整一下布局。
3.九个点都弄好一后,再选择第一列三个点,把它们也chain一下,这样横向和纵向距离都一样了。这九个点的id就是dot(1,2,...9)
4.接着我们再添加一张新的图片,就是被点亮的图片,并设置id为sDot。然后和前面一样,再布局一下这九个点。
5.整好这九个点之后,我们要把这点亮的几个点隐藏起来。那么就需要在右侧搜索一下它的visibility属性,将其设置为invisable。
四、数组保存圆点对象并设置tag
1.首先给每隔点亮的红色图片设置一下tag,分别为1-9。
2.用数组保存九个圆点的对象 ,用于滑动过程中进行遍历
 private val dots:Array<ImageView>by lazy {
    arrayOf(sDot1,sDot2,sDot3,sDot4,sDot5,sDot6,sDot7,sDot8,sDot9)
    }
  • 因为dots的初始化必须在onCreate方法里面,只有当调用了onCreate方法之后,才能开始初始化。但是这样很麻烦,所以我们最好使用懒加载。当你要用的时候才开始加载。
五、获取触摸点相对于容器的坐标
1.要计算触摸点相对于容器的坐标,先要计算bar的高度,然后再计算容器的y值和x值,最后用触摸点的y减去容器的y和bar,就得到了触摸点相对于容器的y坐标。同理,x也一样。
2.先计算bar的高度。因为懒加载只会被加载一次,所以使用懒加载比较方便。如果写成一个函数,那么在移动的时候,这个方法就会被不断的调用,这就很麻烦。
private val barHeight:Int by lazy {
        //1.获取屏幕的尺寸
        val display = DisplayMetrics()
        windowManager.defaultDisplay.getMetrics(display)
        //2.获取操作区域的尺寸
        val drawingRect = Rect()
        window.findViewById<ViewGroup>(Window.ID_ANDROID_CONTENT).getDrawingRect(drawingRect)

         display.heightPixels - drawingRect.height()
    }
3.将触摸点的坐标转化为相对于屏幕的坐标
private fun convertTouchLocationToContainer(event: MotionEvent):Point{
        return   Point().apply {
               //x坐标 = 触摸点的x  - 容器的x
               x  = (event.x - mContainer.x).toInt()
               //y坐标 = 触摸点的y - 容器的y-状态栏高度
               y  = (event.y - mContainer.y-barHeight).toInt()
           }
    }
六、点亮点
1.获取当前这个触摸点所在的视图
private fun findViewContainersPoint(point: Point):ImageView?{
        //遍历所有的点是否包含这个point
        for(dotView in dots){
            //判断这个视图是否包含point
            getRectForView(dotView).also {
                if(it.contains(point.x,point.y)){
                    return dotView
                }
            }
        }
        return null
    }
2.获取视图对应的Rect
  private fun getRectForView(v:View)= Rect(v.left,v.top,v.right,v.bottom)
3.在onTouchEvent方法里面,将触摸点的坐标转化到mContainer上
 val location = convertTouchLocationToContainer(event!!)
4.再判断是否在操作区域内,如果不在操作区域内就直接返回 不执行下面的操作了
if(!((location.x>=0&&location.x<=mContainer.width)&&(location.y>=0&&location.y<=mContainer.height))){
           return true
       }
5.判断这个点是否在视图中,如果在的话,就将其点亮,即不再将它隐藏。
when(event?.action){
            MotionEvent.ACTION_DOWN ->{
                 findViewContainersPoint(location).also {
                    if(it!=null){
                      it.visibility = View.VISABLE
                    }
                 }
        }
七、获取密码
1.点亮点之后,当我们的手指离开的时候,那么点亮的这些点应该消失。所以我们需要添加一个容器,当点亮了点的时候,我们就把这个点放进来,等手指离开之后,再全部释放。
  • 所以我们用一个数组来保存当前被点亮的视图
  private val allSelectedViews = mutableListOf<ImageView>()
2.当某一个点被点亮的时候,就把这个点添加到数组中
 allSelectedViews.add(it)
3.当手指拿起来的时候,我们又要清空一下数据。写一个方法,进行还原的操作。
private fun reset(){
         //遍历保存点亮点的数组
        for(item in allSelectedViews){
            item.visibility = View.INVISIBLE
        }
        //清空
        allSelectedViews.clear()
        lastSelectedView = null
        password.clear()
    }
4.点亮点之后,我们需要记住密码,也就是这些图案的tag值。先用一个变量来记录滑动过程中的轨迹
   private val password = StringBuilder()
5.当点亮这个点的时候,就把tag值追加进这个数组
 password.append(it.tag)
6.因为ACTION_DOWN 和ACTION_MOVE点亮点之后又要记录密码,这个操作是一样的,所以我们可以另外写一个方法。既可以点亮点,又可以记录密码。
 private fun highLightDot(v:ImageView){
        //点亮这个点
        v.visibility = View.VISIBLE
        allSelectedViews.add(v)
        password.append(v.tag)
        //当前点亮的点就是下一个点亮点的上一个点
        lastSelectedView = v
    }
7.因为一个圆点的区域很大,当我们的手指划过这个区域的时候,ACTION_MOVE会被调用多次,所以记录的密码会有很多个重复的数字。为了避免出现这种情况,我们要修改一下规则,只有当这个点没有被点亮并且它不为空时,我们才需要将其点亮。
 if (v != null&&v.visibility==View.INVISIBLE) 
八、添加线
1.点亮了点之后,我们要让这两个点之间的线也点亮一下。
2.所以我们要先去添加线,有横线、竖线和斜线。横线和竖线的添加比较简单,让它和两个点对齐即可。注意:这几根线都是我们事先准备好的图片,并不是另外添加的线。
3.因为斜线两个顶点的中心是在圆点的中心,所以添加完之后会有一部分凸在圆点上,所以我们需要拖动这几根斜线的id,把它们放在圆点id的上方,这样就不会有凸出来的一截。
九、给线添加tag值并保存所有tag
1.在点亮线的时候,会有很多的规则,如果按照规则来点亮线,就会很复杂。所以我们给这些线保存一下tag,这样我们到时候就可以遍历数组,去看看有没有这根线。
  • 给线设置tag值的时候,我们也有自己的规则。因为前面我们给每个点都设置过了tag值,所以我们给线设置的tag值为:小tag*10+大tag。比如:点1和点2中间的线的tag为12,点1和点5中间的线的tag为15。
  • 如何判断两个点之间有没有线呢,我们只需要按照上面的规则,计算一下这两个点中间线的tag,然后再遍历一下保存所有线的tag的数组,如果在这个数组中找到了,那么就有这条线,那么这条线就可以被点亮,否则就没有这条线。
2.所以我们给所有的线都设置一下tag值,并且让它们都隐藏一下,即设置visibility为Invisable。
3.创建一个数组保存所有线的tag值
private val allLineTags = arrayOf(
        12,23,45,56,78,89,
        14,25,36,47,58,69,
        24,35,57,68, 15,26,48,59)
十、点亮线
1.在点亮线的时候,会不断刷新两个点。因为不断滑动的过程中,两个点在不断发生变化,所以需要点亮的那根线也在发生变化。那么我们就需要记录一下每次最后一个被点亮的点。
 private var lastSelectedView:ImageView? = null
2.点亮点之后,当前点亮的点就是下一个点亮点的上一个点。(以下代码写在highLightDot方法中)
 lastSelectedView = v
3.在点亮视图时,我们要判断点亮的这个点是不是第一个点,如果是第一个点,只需要点亮并且保存即可
   private fun highLightView(v: ImageView?) {
        if (v != null&&v.visibility==View.INVISIBLE) {
            //判断这个点是不是第一个点
            if(lastSelectedView==null){
                //第一个点 只需要点亮  并且保存
               highLightDot(v)
            }else{}
}
4.如果在滑动过程中已经点亮过其他点了,那么就需要获取上一点和这个点的线的tag值
       else{
             val previous:Int = (lastSelectedView?.tag as String).toInt()
             val current :Int =(v.tag as String ).toInt()
5.然后计算这两个点之间的tag值
val lineTag = if(previous>current) current*10+previous else
                    previous*10+current
6.然后判断是否有这条线
if(allLineTags.contains(lineTag)){
                    // 点亮这个点
                    highLightDot(v)
                    //点亮这条线
                    mContainer.findViewWithTag<ImageView>(lineTag.toString()).apply {
                        visibility = View.VISIBLE
                       allSelectedViews.add(this)
                    }
                }
十一、封装SharedPreference
1.在Android中,很多应用都需要存储一些参数,例如在天气app中,在某一次使用时用户添加了若干个城市,当用户下一次点开时也希望之前设置的城市会保存在手机中,以方便直接获取信息。这个时候就需要用到 SharedPreference 类的辅助,利用它存储一些键值对(Key-Value)参数。
2.我们这里就使用SharedPreference来完成密码的存取。
3.我们创建一个类,取名为SharedPreferenceUtil,采用单例设计模式(私有化构造方法,创建一个对象,给外部提供一个静态方法并返回创建的对象)
class SharedPreferenceUtil private constructor(){
    private val FILE_NAME = "password"
    private val KEY = "passwordKey"
      companion object{
          private var instance: SharedPreferenceUtil? = null
          private var mContext:Context?=null
          fun getInstance(context: Context) :SharedPreferenceUtil{
              mContext= context
              if(instance==null){
                 synchronized(this){
                     instance= SharedPreferenceUtil()
                 }
              }
              return instance!!
          }
      }
}
  • 以上就是单例,我们还可以添加一些方法,比如保存密码,获取密码。
4.在保存密码时,需要一个context对象,所以我们在上面的方法中创建了一个mContext对象。在调用getSharedPreferences方法时,它需要我们提供一个名字,所以我们在前面也创建了一个名字对象。
 private val FILE_NAME = "password"
  • 然后我们就可以获取preference对象了
val sharedPreferences = 
mContext?.getSharedPreferences(FILE_NAME,Context.MODE_PRIVATE)
  • 获取edit对象 ->写数据
  val edit = sharedPreferences?.edit()
  • 写入数据的时候需要一个key,所以我们在前面再添加一个key
private val KEY = "passwordKey"
  • 然后把数据写入
  edit?.putString(KEY,pwd)
  • 最后提交数据
 edit.apply {

        }
  • 完整的savePassword方法如下
 fun savePassword(pwd:String){
        //获取preference对象
      val sharedPreferences = mContext?.getSharedPreferences(FILE_NAME,Context.MODE_PRIVATE)
        //获取edit对象  ->写数据
        val edit = sharedPreferences?.edit()
        //写入数据
        edit?.putString(KEY,pwd)
        //提交
        edit.apply {

        }
    }
5.获取密码
fun getPassword():String?{
        //获取preference对象
        val sharedPreferences = mContext?.getSharedPreferences(FILE_NAME,Context.MODE_PRIVATE)
         return  sharedPreferences?.getString(KEY,null)
    }
6.在MainActivity里面,可以直接调用这个类获取密码。在onCreate方法里面获取
SharedPreferenceUtil.getInstance(this).getPassword().also {
            if(it==null){
             mAiert.text = "请设置密码图案"
            }else{
                mAiert.text = "请解锁密码图案"
                orgPassword = it
            }
        }
  • mAiert是布局的时候,那个TextView的id,随着密码的不同,它显示的文字也不同。 orgPassword为原始密码。第一次打开这个手机的时候,显示的是请设置密码图案,其他时候打开都是请设置。
十二、实现密码设置和解锁
1.定义一个记录原始密码的变量,和第一次设置的密码
    //记录原始密码
    private var orgPassword: String? = null
    //记录第一次设置的密码
    private var firstPassword : String?= null
2.在触摸事件中,当手指拿起来的时候,需要判断是不是第一次这样滑动,也就是判断有没有原始密码。如果是的话,再判断是不是设置密码的第一次。
 MotionEvent.ACTION_UP -> {
                //判断是不是第一次
                if(orgPassword==null){
                    //是不是设置密码的第一次
                  if(firstPassword==null){
                      //记录第一次的密码
                      firstPassword = password.toString()
                      //提示输入第二次
                      mAiert.text = "请确认密码图案"
                  }else{
                       //确认密码
                      comparePassword(firstPassword!!,password.toString())
                  }
                }else{
                    //确认密码
                    comparePassword(firstPassword!!,password.toString())
                }
                 reset()
            }
3.判断两次密码是否相同,可以写一个方法出来
private fun comparePassword(first:String,second:String){
        //确认密码
        if (first== second){
            //两次密码一致
            mAiert.text = "设置密码成功"
            //保存密码
            SharedPreferenceUtil.getInstance(this).savePassword(first)
        }else{
            mAiert.text = "两次密码不一样,请重新设置密码"
            firstPassword = null
        }
    }
  • 原始密码:其实就是你上一次设置的密码。如果这是你新买来的手机,那么它就没有原始密码,这个时候你设置了密码,这个密码就成了原始密码。
  • 第一次设置的密码:在你设置密码的时候,需要设置两遍,只有第一遍和第二遍设置的密码一样时,你才算设置密码成功。如果不一样,就要把第一次设置的密码清空,方便下一次进行判断。
十三、限制可点击区域
1.有时候不在九个点之内点击,提示的文字可能也会发生改变,所以我们需要限制一下点击区域。只有在这个区域内点击,提示的文字才会改变。
2.之前我们已经获取了触摸点相对于容器的坐标,所以根据这个约束一下点击范围即可。
if(!((location.x>=0&&location.x<=mContainer.width)&&(location.y>=0&&location.y<=mContainer.height))){
           //如果不在操作区域内就直接返回 不执行下面的操作了
           return true
       }
十四、添加第三方库支持圆形头像
1.一般的头像都是支持圆角的,所以我们要添加一个第三方库来支持圆角。
2.打开build.gradle这一页的代码,在dependencies括号里面的最后一行加上以下代码,然后同步一下。
implementation 'de.hdodenhof:circleimageview:3.1.0'
3.为了让其更美观,可以让头像后方的背景都为黑色。所以拖动一个View过来,放在头像后面。然后设置一下背景颜色为黑色,并调整一下,把这个view放在头像下面。
4.然后在code里面,找到头像对应的那段代码,然后把<ImageView改为
<de.hdodenhof.circleimageview.CircleImageView
然后重新设置一下src
android:src="@drawable/touxiang"
还可以设置一下边框的颜色
app:civ_border_color="@android:color/background_light"
5.如果是用模拟器运行的,那么我们需要先在相册里面保存几张图片。直接在模拟器里面点击Google,然后搜索图片,最后下载几张图片下来,为后面更换头像做准备。
十五、从相册提取图片
1.给头像图片添加点击事件
 mHeader.setOnClickListener(){
            //从相册里面获取一张图片
          Intent().apply {
              action = Intent.ACTION_PICK
              type = "image/*"
              startActivityForResult(this,REQUEST_IMAGE_CODE)
          }
        }
2.给图片或视频设置请求码
    private val REQUEST_IMAGE_CODE = 123
    private val REQUEST_VEDIO_CODE = 124
3.实现onActivityResult方法。
 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when(requestCode){
            REQUEST_IMAGE_CODE -> {
                //图片
                //判断用户是否取消操作
                if(resultCode!=Activity.RESULT_CANCELED){
                    //获取图片
                  val uri=  data?.data
                  uri?.let {
                      //对io操作尽量使用use
                      contentResolver.openInputStream(uri).use {
                          //Bitmap
                          BitmapFactory.decodeStream(it).also {image->
                              //显示图片
                              mHeader.setImageBitmap(image)
                              //把图片缓存起来
                              val file = File(filesDir,"header.jpg")
                              FileOutputStream(file).also { fos->
                                  //将图片缓存到fos对应的文件中
                                  image.compress(Bitmap.CompressFormat.JPEG,50,fos)
                              }

                          }
                      }
                  }
                }
            }
            REQUEST_VEDIO_CODE ->{
                //视频
            }
        }
    }
4.获取头像
 File(filesDir,"header.jpg").also {
            if(it.exists()){
                BitmapFactory.decodeFile(it.path).also {image->
                    mHeader.setImageBitmap(image)
                }
            }
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容