今日科技快讯

5月16日,在联想、华为双方两次公开态度表示,联想对华为主导的5G方案投了赞成票之后,今天柳传志也忍不住联合杨元庆等发了“5G投票事件”的声明,称:有人给联想扣”卖国”的帽子,这事不能忍。而任正非也表示,联想在5G标准的投票过程中的做法没有任何问题,并对联想对华为的支持表示感谢。

作者简介

本篇来自 烧饼正努力 的投稿,分享了一个自定义锁屏控件,一起来看看!希望大家喜欢。

烧饼正努力 的博客地址:

https://www.jianshu.com/u/a89c1b1e40af

前言

最近学习了一些自定义控件的知识,想着趁热多做些练习来巩固,上周自定义了一个等级进度条,是一个自定义View,这周就换一个类型,做一个自定义的ViewGroup。这周自定义ViewGroup的是一个锁屏控件,效果如下:

正文

效果分析

仔细分析效果图发现,锁屏控件需要绘制的有三个部分,分别是:

  • 图案点,图案点有四种状态,分别是默认、选中、正确和错误

  • 图案点之间的连线

连线会根据1中点的状态改变发生颜色上的变化

  • 悬空线段

就是图案点和悬空点之间的线段

整体思路

  1. 自定义一个LockScreenView来表示图案点,LockScreenView有四种状态

  2. 自定义一个LockScreenViewGroup,在onMeasure中获取到宽度以后(根据宽度算图案点之间的间距),动态地将LockScreenView添加进来

  3. 在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);            }        }

这里有两个地方需要注意一下:

  1. LockScreenView的宽不能用getMeasuredWidth方法来获取,因为这里只是把LockScreenView创建了出来,还没有对它进行测量,故通过getMeasuredWidth方法只能得到0,这里直接把LockScreenView中大圆的直径当作它的宽(因为这里动态添加的时候用了wrap_content, 并且没有设padding)

  2. 重写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

欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

教你编写一个手势解锁控件相关推荐

  1. 自制一个九宫格解锁控件

    前两天从网上学习了下如何自定义一个九宫格解锁的控件,于是自己根据逻辑写了一遍,自定义控件的代码如下: public class LockedView extends View {private boo ...

  2. Android Jelly Bean滑动解锁控件实现

    锁屏界面实现: 主要研究Jelly Bean中的解锁界面实现. 界面源码位置: frameworks\base\Policy\src\com\android\internal\policy\impl\ ...

  3. 手把手教你编写一个上位机

    关注+星标公众号,不错过精彩内容 转自 | 嵌入式大杂烩 嵌入式开发,基本都会用到有一些上位机工具,比如串口助手就是最常用的工具之一. 那么,今天分享有一篇由ZhengN整理的用Qt写的简单上位机教程 ...

  4. C语言: 编写一个程序解鸡兔同笼问题:已知鸡兔总数为a, 鸡兔腿总数为b, 计算出鸡兔各多少只

    题目: 编写一个程序解鸡兔同笼问题:已知鸡兔总数为a, 鸡兔腿总数为b, 计算出鸡兔各多少只 题目分析:直接使用顺序结构就行         1.设鸡有x只,兔子有y只,因此x+y=a:即,  x=a ...

  5. 教你编写一个机器学习代码也能使用的单元测试

    摘要: 想不想节省重新训练数据的时间?想不想让你的研究成果有个质的飞跃?来看看这些单元测试,助你一臂之力. 注:这篇文章自从发布出来,就受到读者的好评和关注,因此,我编写了一个机器学习测试库,请点击链 ...

  6. 自定义手势解锁锁控件

    一.控件的使用 模仿市面上app的手势解锁功能,实现的小控件,将控件封装到了一个UIView上 二.核心原理技术 1.触摸事件 (1)UIView的触摸三个触摸响应事件:开始.移动.结束 (2)CGR ...

  7. 使用DELPHI编写一个小的控件

    好久没有来了,今天终于有时间了,可以来写写关于控件开发的文章了.           今天我以我4年前写的一个小控件为例写一下如何开发控件的方法.           这个控件实现后的效果可见附件1. ...

  8. 手把手教你写一个手势密码解锁View(GesturePasswordView)

    相信大家在很多的app肯定看到过手势密码解锁View,但是大家有没有想过怎么实现这样一个View,哈,接下来,小编手把手教大家教写一个GesturePasswordView. 先看一张效果图 要实现这 ...

  9. 自定义九宫格解锁控件

    前言 九宫格手势解锁已经是非常常见的手机解锁方式,支付宝等一些软件也都曾经使用过,感觉还是很高大上的.在github已经有很好的开源控件,大家可以去自己搜索,我自己写了一个自定义的九宫格控件,作为练习 ...

最新文章

  1. python3 tkinter电子书_python3 tkinter实现添加图片和文本
  2. ipad如何连接电脑_超能办公课堂丨电脑如何使用无线网络连接方式安装驱动程序...
  3. 操作系统实验——简易FAT16文件系统的实现
  4. anjularjs 路由
  5. 【数据分析】数据缺失影响模型效果?是时候需要missingno工具包来帮你了!
  6. IDEA创建java文件失败,但是new选项中有java class选项,设置中file and Code Templates中有对应模板
  7. 实录 | DSTC 8“基于Schema的对话状态追踪”竞赛冠军方案解读
  8. Android 一s个相对完整的自动升级功能实现代码
  9. 考研复习安排——第一阶段末
  10. 把ueditor的 p 标签 改成a标签_每周一签·35 | 云标签使用常见问题(QA)
  11. FUCKED-BUG之python子进程的键盘中断
  12. zircon ddk快速入门
  13. python通过正则匹配指定字符开头与结束提取中间内容
  14. 被华策、欢瑞等甩下,唐人影视往日荣光已成回忆?
  15. 大数据助力运营商创新转型 中国信息通信大数据大会圆满召开
  16. SpringBoot开发常用技术这些你知道吗???
  17. 盘点 2021年度在线协同设计软件
  18. Xcode一键发布到AppStore
  19. 腾讯视频下载视频QLV格式转为MP4格式
  20. 天美服务器维护,王者荣耀亚瑟皮肤特效丢失什么时候会修复?新皮肤特效消失天美紧急修复...

热门文章

  1. direct do造句 sb to_wish sb to do造句
  2. python http服务器
  3. AB Testing
  4. java输出GPA,简单的C GPA计算器问题
  5. 若依管理系统RuoYi-Vue(前后端分离版)项目启动教程
  6. 【ARM】IMX6UL串口通信
  7. 基于JAVA家装建材网计算机毕业设计源码+系统+mysql数据库+lw文档+部署
  8. 在ubuntu下使用Vim学习C++
  9. 195元爱奇艺会员只卖5元 揭秘背后黑色产业链
  10. 如何引流中老年粉?中老年人群怎么引流?中老年粉如何变现?