教你编写一个手势解锁控件
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方法中判断类型,进行绘制:
@Override
protected 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方法做一些特殊的处理,至于为什么要做特殊处理,在上一篇博文《等级进度条》中已经提到过了。
@Override
protected 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
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
-
晴天yh 2018-05-19 15:36:22 #1楼哇,这就是微信公众号的文章啊,正需要
- 上一页
- 1
- 下一页
FingerGestures 3 1<em>手势</em>插件 最新完整版
FingerGestures是一个Unity中简化鼠标<em>手势</em>操作的扩展,它能很方便的监听到Unity中的各种<em>手势</em>事件:...
下载
2018年05月07日 00:00
移动端<em>手势</em>插件
jGestures是一个jq移动端<em>手势</em>插件,jGestures插件允许你如同原生的jQuery事件一样监听以下事件...
下载
2018年05月08日 00:00
Android<em>手势</em>锁屏<em>控件</em>(附例子)
举报人: 被举报人: windworst 举报的资源分: 3 *类型: *详细原因: 取 消 提 交 Android<em>手势</em>锁屏<em>...
下载
2018年05月06日 00:00
FingerGestures手势插件的使用
手势插件(V3.1)的下载地址: http://pan.baidu.com/s/1hrOUXqW 在移动端单手指按下/抬起/单手指拖动旋转视角/双手指拉大放小/双手指同时按下平移等等,在其它插件...
手把手教你写一个手势密码解锁View(GesturePasswordView)
相信大家在很多的app肯定看到过手势密码解锁View,但是大家有没有想过怎么实现这样一个View,哈,接下来,小编手把手教大家教写一个GesturePasswordView。 先看一张效果图 ...
纯js写的日期和时间选择控件
2012年12月15日 27KB 下载
Android九宫格手势密码解锁控件源码及其资源
2015年09月23日 35KB 下载
归档
- 2018年6月1篇
- 2018年5月21篇
- 2018年4月26篇
- 2018年3月23篇
- 2018年2月14篇
- 2018年1月22篇
- 2017年12月18篇
- 2017年11月16篇
- 2017年10月16篇
- 2017年9月21篇
- 2017年8月22篇
- 2017年7月21篇
- 2017年6月22篇
- 2017年5月21篇
- 2017年4月18篇
- 2017年3月23篇
- 2017年2月19篇
- 2017年1月17篇
- 2016年12月21篇
- 2016年11月22篇
- 2016年10月18篇
- 2016年9月16篇
- 2016年8月23篇
- 2016年7月21篇
- 2016年6月14篇
- 2016年5月16篇
- 2016年4月1篇
- 2016年3月1篇
热门文章
- 教你免费且快速地搭建个人网站
阅读量:8406
- 国产 Android 权限申请最佳适配方案——permissions4m
阅读量:3512
- 自定义View之王者荣耀等级进度条
阅读量:3261
- 实现一个类似QQ的社交聊天工具
阅读量:2310
- 讲给Android程序员看的前端和后台教程
阅读量:2300
最新评论
- 高手不得不知的List细节
myy629464:归纳总结是知识积累的好方法之一。感谢博主。
- Java数据结构学习,从源码角度彻...
myy629464:玉笙弹尽断人肠,只缘一梦枕黄粱。
- Android 7.0关于HTTP...
kuailyanghui:你好,按照你说的步骤,在wifi走代理时都是可以抓包的。 但是当把wifi的代理去掉后,这样的ap...
- Android Realm详解
jw2268136570:写的很好
- 教你编写一个手势解锁控件
weixin_39272004:哇,这就是微信公众号的文章啊,正需要
0
收藏
评论