微信号:guolin_blog

介绍:Android技术分享平台,在这里不仅可以学到各种Android相关的最新技术,还可以将你自己的技术总结分享给其他人,每周定期更新.

教你编写一个手势解锁控件

2018-05-17 08:00 烧饼正努力



今日科技快讯


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方法中判断类型,进行绘制:

@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);
           }
       }

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

  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


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

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

 
郭霖 更多文章 EventBus设计之禅 ConstraintLayout的崛起之路 从源码角度彻底搞懂ArrayList Android V1、V2签名包和快速集成美团多渠道打包 你知道String、StringBuffer和StringBuilder之间的区别吗?
猜您喜欢 黑客小发明!用一支“马克笔”打开酒店电子锁 深入贯彻闭包思想,全面理解JS闭包形成过程 百里金戈铁马,只愿君安天下! 达内集团获批教育部产学合作专业综合改革项目合作单位 互联网人平均年薪19万 四成程序员单身