本节内容
1.demo布局分析
2.界面布局
3.布局9个点
4.数组保存圆点对象并设置tag
5.获取触摸点相对于容器的坐标
6.点亮点
7.获取密码
8.添加线
9.给线添加tag值并保存所有tag
10.点亮线
11.封装SharePreferencr
12.实现密码设置和解锁
13.限制可点击区域
14.添加第三方库支持圆形头像
15.从相册提取图片
一、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.
进行完上面的操作后,差不多是下图的效果
三、布局九个点
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"
val sharedPreferences =
mContext?.getSharedPreferences(FILE_NAME,Context.MODE_PRIVATE)
val edit = sharedPreferences?.edit()
-
写入数据的时候需要一个key,所以我们在前面再添加一个key
private val KEY = "passwordKey"
edit?.putString(KEY,pwd)
edit.apply {
}
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)
}
}
}