教你编写一个手势解锁控件
今日科技快讯
5月16日,在联想、华为双方两次公开态度表示,联想对华为主导的5G方案投了赞成票之后,今天柳传志也忍不住联合杨元庆等发了“5G投票事件”的声明,称:有人给联想扣”卖国”的帽子,这事不能忍。而任正非也表示,联想在5G标准的投票过程中的做法没有任何问题,并对联想对华为的支持表示感谢。
作者简介
本篇来自 烧饼正努力 的投稿,分享了一个自定义锁屏控件,一起来看看!希望大家喜欢。
烧饼正努力 的博客地址:
https://www.jianshu.com/u/a89c1b1e40af
前言
最近学习了一些自定义控件的知识,想着趁热多做些练习来巩固,上周自定义了一个等级进度条,是一个自定义View,这周就换一个类型,做一个自定义的ViewGroup。这周自定义ViewGroup的是一个锁屏控件,效果如下:
正文
效果分析
仔细分析效果图发现,锁屏控件需要绘制的有三个部分,分别是:
图案点,图案点有四种状态,分别是默认、选中、正确和错误
图案点之间的连线
连线会根据1中点的状态改变发生颜色上的变化
悬空线段
就是图案点和悬空点之间的线段
整体思路
自定义一个LockScreenView来表示图案点,LockScreenView有四种状态
自定义一个LockScreenViewGroup,在onMeasure中获取到宽度以后(根据宽度算图案点之间的间距),动态地将LockScreenView添加进来
在LockScreenViewGroup的onTouchEvent中消耗触摸事件,根据触摸点的轨迹来更新LockScreenView、图案点连线和悬空线段
实现
自定义LockScreenView
由于没有和这个自定义View比较类似的原生控件,因此自定义的时候直接继承自View。首先,需要的属性通过构造函数传入:
private int smallRadius; // LockScreenView小圈的半径 private int bigRadius; // LockScreenView中大圆圈的半径 private int normalColor; // LockScreenView中默认的颜色 private int rightColor; // LockScreenView中图形码正确时的颜色 private int wrongColor; // LockScreenView中图形码错误时的颜色
public LockScreenView(Context context, int normalColor, int smallRadius, int bigRadius, int rightColor, int wrongColor)
View的状态用一个枚举类型来表示
enum State { // 四种状态,分别是正常状态、选中状态、结果正确状态、结果错误状态 STATE_NORMAL, STATE_CHOOSED, STATE_RESULT_RIGHT, STATE_RESULT_WRONG}
View的状态通过暴露一个方法给LockScreenViewGroup来进行设置。在onDraw方法中判断类型,进行绘制:
@Overrideprotected void onDraw(Canvas canvas) { switch(mCurrentState) { case STATE_NORMAL: // break; case STATE_CHOOSED: // break; case STATE_RESULT_RIGHT: // break; case STATE_RESULT_WRONG: // break; }}
这里在选中时用属性动画做了一个放大效果,在下次恢复正常的时候要将大小恢复回去:
private void zoomOut() { ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "scaleX", 1, 1.2f); animatorX.setDuration(50); ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "scaleY", 1, 1.2f); animatorY.setDuration(50); AnimatorSet set = new AnimatorSet(); set.playTogether(animatorX, animatorY); set.start(); needZoomIn = true; }
private void zoomIn() { ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "scaleX", 1, 1f); animatorX.setDuration(0); ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "scaleY", 1, 1f); animatorY.setDuration(0); AnimatorSet set = new AnimatorSet(); set.playTogether(animatorX, animatorY); set.start(); needZoomIn = false; }
在LockScreenViewGroup中,我将LockScreenView的宽高设置为wrap_content,因此需要在onMeasure方法做一些特殊的处理,至于为什么要做特殊处理,在上一篇博文《等级进度条》中已经提到过了。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) { widthSize = (int) Math.round(bigRadius*2); } if (heightMode == MeasureSpec.AT_MOST) { heightSize = (int) Math.round(bigRadius*2); } setMeasuredDimension(widthSize, heightSize);}
自定义LockScreenViewGroup
为了方便确定子View的位置,LockScreenViewGroup继承自RelativeLayout。在xml中赋予了如下属性:
<declare-styleable name="LockScreenViewGroup"> <attr name="itemCount" format="integer"/> <attr name="smallRadius" format="dimension"/> <attr name="bigRadius" format="dimension"/> <attr name="normalColor" format="color"/> <attr name="rightColor" format="color"/> <attr name="wrongColor" format="color"/></declare-styleable>
其中itemCount表示一行有几个LockScreenView,其它属性都已经提到过了。在构造函数中解析xml中的自定义属性:
public LockScreenViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);
// 从xml中获取自定义属性 TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.LockScreenViewGroup); itemCount = array.getInt(R.styleable.LockScreenViewGroup_itemCount, 3); smallRadius = (int) array.getDimension(R.styleable.LockScreenViewGroup_smallRadius, 20); bigRadius = (int) array.getDimension(R.styleable.LockScreenViewGroup_bigRadius, 2); normalColor = array.getInt(R.styleable.LockScreenViewGroup_normalColor, 0xffffff); rightColor = array.getColor(R.styleable.LockScreenViewGroup_rightColor, 0x00ff00); wrongColor = array.getColor(R.styleable.LockScreenViewGroup_wrongColor, 0x0000ff);
array.recycle();
在onMeasure方法中,获取到LockScreenViewGroup的宽以后,算出LockScreenView之间的间隙,并动态地将LockScreenView添加进来(每个LockScreenView添加进来的时候,设置id作为唯一标识,后面在判断图案是否正确时会用到):
// 动态添加LockScreenView if (lockScreenViews == null) { lockScreenViews = new LockScreenView[itemCount * itemCount]; for (int i = 0; i < itemCount * itemCount; i++) { lockScreenViews[i] = new LockScreenView(getContext(), normalColor, smallRadius, bigRadius, rightColor, wrongColor); lockScreenViews[i].setId(i + 1); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT ); // 这里不能通过lockScreenViews[i].getMeasuredWidth()来获取宽高,因为这时它的宽高还没有测量出来 int marginWidth = (getMeasuredWidth() - bigRadius * 2 * itemCount) / (itemCount + 1);
// 除了第一行以外,其它的View都在在某个LockScreenView的下面 if (i >= itemCount) { params.addRule(BELOW, lockScreenViews[i - itemCount].getId()); }
// 除了第一列以外,其它的View都在某个LockScreenView的右边 if (i % itemCount != 0) { params.addRule(RIGHT_OF, lockScreenViews[i - 1].getId()); }
// 为LockScreenView设置margin int left = marginWidth; int top = marginWidth; int bottom = 0; int right = 0; params.setMargins(left, top, right, bottom); lockScreenViews[i].setmCurrentState(LockScreenView.State.STATE_NORMAL); addView(lockScreenViews[i], params); } }
这里有两个地方需要注意一下:
LockScreenView的宽不能用getMeasuredWidth方法来获取,因为这里只是把LockScreenView创建了出来,还没有对它进行测量,故通过getMeasuredWidth方法只能得到0,这里直接把LockScreenView中大圆的直径当作它的宽(因为这里动态添加的时候用了wrap_content, 并且没有设padding)
重写onMeasure方法的时候不能把super.onMeasure方法删掉,因为这里面会进行子View宽高的测量,删了子View就画不出来了
触摸事件的消耗在onTouchEvent中处理(在这个案例中也可以在dispatchTouchEvent方法中处理,因为子View的状态由LockScreenViewGroup告诉它了,子View不需要处理触摸事件)。在onTouchEvent方法中对Down、Move、Up三种不同的触摸状态分别做了处理。
首先,在Down状态时,需要对之前的状态做一些重置:
private void resetView() { if (mCurrentViews.size() > 0) { mCurrentViews.clear(); } if (!mCurrentPath.isEmpty()) { mCurrentPath.reset(); }
// 重置LockScreenView的状态 for (int i = 0; i < itemCount * itemCount; i++) { lockScreenViews[i].setmCurrentState(LockScreenView.State.STATE_NORMAL); }
skyStartX = -1; skyStartY = -1; }
其中,mCurrentViews用来保存当前选中的LockScreenView的id,mCurrentPath用来保存图像点间线段的路径,skyStartX、skyStartY分别是悬空线段起始的x和y。
在Move状态时,判断是否在LockScreenView区域,如果在某个LockScreenView区域且这个LockScreenView之前没有被选中,则将这个LockScreenView设置为选中状态。另外在onMove中还做了图案点间线段路径和悬空线段起点和终点(mTempX、mTempY)的更新,悬空线段的起点就是上一个被选中的LockScreenView的中心点。
case MotionEvent.ACTION_MOVE: mPaint.setColor(normalColor); LockScreenView view = findLockScreenView(x, y); if (view != null) { int id = view.getId(); // 当前LockScreenView不在选中列表中时,将其添加到列表中,并设置其状态为选中 if (!mCurrentViews.contains(id)) { mCurrentViews.add(id); view.setmCurrentState(LockScreenView.State.STATE_CHOOSED); skyStartX = (view.getLeft() + view.getRight()) / 2; skyStartY = (view.getTop() + view.getBottom()) / 2;
// path中线段的添加 if (mCurrentViews.size() == 1) { mCurrentPath.moveTo(skyStartX, skyStartY); } else { mCurrentPath.lineTo(skyStartX, skyStartY); } } } // 悬空线段末端的更新 mTempX = x; mTempY = y; break;
在Up状态时,根据答案的正确与否,对LockScreenView设置不同的状态,并且对悬空线段起始点进行重置。
case MotionEvent.ACTION_UP: // 根据图案正确与否,对LockScreenView设置不同的状态 if (checkAnswer()) { setmCurrentViewsState(LockScreenView.State.STATE_RESULT_RIGHT); mPaint.setColor(rightColor); } else { setmCurrentViewsState(LockScreenView.State.STATE_RESULT_WRONG); mPaint.setColor(wrongColor); } // 抬起手指后对悬空线段的起始点进行重置 skyStartX = -1; skyStartY = -1;
在onTouchEvent方法最后会调用invalidate方法对视图进行重绘,这时会调用dispatchDraw方法进行子View的绘制。
在dispatchDraw方法中进行图像点间的线段路径以及悬空线段的绘制:
@Override protected void dispatchDraw(Canvas canvas) { // 进行子View的绘制 super.dispatchDraw(canvas);
// path线段的绘制 if (!mCurrentPath.isEmpty()) { canvas.drawPath(mCurrentPath, mPaint); }
// 悬空线段的绘制 if (skyStartX != -1) { canvas.drawLine(skyStartX, skyStartY, mTempX, mTempY, mPaint); } }
这里要注意,在重写dispatchDraw方法时,不能把super.dispatchDraw方法删掉,因为这里会绘制LockScreenViewGroup的子View(即,LockScreenView们),如果删了,动态添加的LockScreenView就会显示不出来(重写的时候不小心删了,排查好久才发现是这里的问题,都是泪orz)
总结
文章到这里就结束了。最后,奉上源码地址:
https://github.com/shonnybing/LockScreenView
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
教你编写一个手势解锁控件相关推荐
- 自制一个九宫格解锁控件
前两天从网上学习了下如何自定义一个九宫格解锁的控件,于是自己根据逻辑写了一遍,自定义控件的代码如下: public class LockedView extends View {private boo ...
- Android Jelly Bean滑动解锁控件实现
锁屏界面实现: 主要研究Jelly Bean中的解锁界面实现. 界面源码位置: frameworks\base\Policy\src\com\android\internal\policy\impl\ ...
- 手把手教你编写一个上位机
关注+星标公众号,不错过精彩内容 转自 | 嵌入式大杂烩 嵌入式开发,基本都会用到有一些上位机工具,比如串口助手就是最常用的工具之一. 那么,今天分享有一篇由ZhengN整理的用Qt写的简单上位机教程 ...
- C语言: 编写一个程序解鸡兔同笼问题:已知鸡兔总数为a, 鸡兔腿总数为b, 计算出鸡兔各多少只
题目: 编写一个程序解鸡兔同笼问题:已知鸡兔总数为a, 鸡兔腿总数为b, 计算出鸡兔各多少只 题目分析:直接使用顺序结构就行 1.设鸡有x只,兔子有y只,因此x+y=a:即, x=a ...
- 教你编写一个机器学习代码也能使用的单元测试
摘要: 想不想节省重新训练数据的时间?想不想让你的研究成果有个质的飞跃?来看看这些单元测试,助你一臂之力. 注:这篇文章自从发布出来,就受到读者的好评和关注,因此,我编写了一个机器学习测试库,请点击链 ...
- 自定义手势解锁锁控件
一.控件的使用 模仿市面上app的手势解锁功能,实现的小控件,将控件封装到了一个UIView上 二.核心原理技术 1.触摸事件 (1)UIView的触摸三个触摸响应事件:开始.移动.结束 (2)CGR ...
- 使用DELPHI编写一个小的控件
好久没有来了,今天终于有时间了,可以来写写关于控件开发的文章了. 今天我以我4年前写的一个小控件为例写一下如何开发控件的方法. 这个控件实现后的效果可见附件1. ...
- 手把手教你写一个手势密码解锁View(GesturePasswordView)
相信大家在很多的app肯定看到过手势密码解锁View,但是大家有没有想过怎么实现这样一个View,哈,接下来,小编手把手教大家教写一个GesturePasswordView. 先看一张效果图 要实现这 ...
- 自定义九宫格解锁控件
前言 九宫格手势解锁已经是非常常见的手机解锁方式,支付宝等一些软件也都曾经使用过,感觉还是很高大上的.在github已经有很好的开源控件,大家可以去自己搜索,我自己写了一个自定义的九宫格控件,作为练习 ...
最新文章
- python3 tkinter电子书_python3 tkinter实现添加图片和文本
- ipad如何连接电脑_超能办公课堂丨电脑如何使用无线网络连接方式安装驱动程序...
- 操作系统实验——简易FAT16文件系统的实现
- anjularjs 路由
- 【数据分析】数据缺失影响模型效果?是时候需要missingno工具包来帮你了!
- IDEA创建java文件失败,但是new选项中有java class选项,设置中file and Code Templates中有对应模板
- 实录 | DSTC 8“基于Schema的对话状态追踪”竞赛冠军方案解读
- Android 一s个相对完整的自动升级功能实现代码
- 考研复习安排——第一阶段末
- 把ueditor的 p 标签 改成a标签_每周一签·35 | 云标签使用常见问题(QA)
- FUCKED-BUG之python子进程的键盘中断
- zircon ddk快速入门
- python通过正则匹配指定字符开头与结束提取中间内容
- 被华策、欢瑞等甩下,唐人影视往日荣光已成回忆?
- 大数据助力运营商创新转型 中国信息通信大数据大会圆满召开
- SpringBoot开发常用技术这些你知道吗???
- 盘点 2021年度在线协同设计软件
- Xcode一键发布到AppStore
- 腾讯视频下载视频QLV格式转为MP4格式
- 天美服务器维护,王者荣耀亚瑟皮肤特效丢失什么时候会修复?新皮肤特效消失天美紧急修复...