Android进阶之自定义View实战(二)九宫格手势解锁实现

一.引言

在上篇博客Android进阶之自定义View实战(一)仿iOS UISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法。作为自定义View的入门篇,仅仅介绍了Canvas的基本使用方法,而对用户交互层面仅仅处理了单击事件接口,在实际的业务中,常常涉及到手势操作,本篇博客以九宫格手势解锁View为例,来说明自定义View如何根据需求处理用户的手势操作。虽然九宫格手势解锁自定义View网上资料有很多,实现原理大同小异,但这里我只是根据自己觉得最优的思路来实现它,目的是让更多的初学者能看清我的思想,更快的掌握它的套路。话不多说,先看效果图,本人纯种工科男,颜色大家看看就好~_~!(ps:as的录屏效果有一半屏幕是花的,所以上图片将就一下。。);

1.手指滑动状态

2.手指释放后,校验失败

3.手指释放后,校验成功

二.案例分析

根据上面的三张图,可以看出手势锁有下面几个要素:

1.九宫格阵列状态,每个格子有三种状态:空闲、击中、校验失败、校验成功,其中击中状态是在手指移动过程中产生,后面的校验状态是手指释放后产生。格子状态的改变都在View的触摸事件里处理。

2.九宫格的绘制元素,每个格子有半透明大圆、深色小圆、三角;在移动过程中的手指路径,包括两个:格子与格子之间的连线和路径的”尾巴”:动态探测线。

3.触摸事件所需要处理的逻辑主要有每个格子的状态处理和路径规划。

1>根据手指的位置,确定击中的节点,在down和move事件改变它的状态;

2>在move事件中,探测击中的节点,如果探测到则绘制连线,否则绘制探测线。

3>在up或者cancel事件中,取消探测线,并根据击中的节点校验密码,更新状态,计算三角的方向,重绘节点。

三.范例代码

1.通过第二节的分析,格子(Block)是这个View的基本构成单元,包含状态、位置、大小半径、三角等元素,触摸事件的处理都是真的Block的处理。Block代码:

package com.star.gesturelock;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;

/**
 * 一个阵列的基本组成单元
 * Created by kakaixcm on 16/6/7.
 */
public class Block {
    float mCenterPointX;//圆心x
    float mCenterPointY;//圆心y
    float mBigRadius;//大圆半径
    float mLittleRadius;//小圆半径
    BlockSate mState = BlockSate.IDLE;//默认空闲

    int mId;//索引

    //空闲状态颜色
    int mIdleBigCircleColor = Color.parseColor("#110000ff");
    int mIdleLittleCircleColor = Color.parseColor("#0000ff");

    //选中状态颜色
    int mHittedBigCircleColor = Color.parseColor("#1100ff00");
    int mHittedLittleCircleColor = Color.parseColor("#00ff00");

    //密码通过的颜色
    int mSuccessBigCircleColor = Color.parseColor("#1100ff00");
    int mSuccessdLittleCircleColor = Color.parseColor("#00ff00");

    //密码错误时的颜色
    int mErroBigCircleColor = Color.parseColor("#11ff0000");
    int mErroLittleCircleColor = Color.parseColor("#ff0000");

    //三角
    Path mArrow = new Path();
    //三角指向角度,水平向右为0度,顺时针方向为正
    double mArrowAngle;

    public void setArrowAngle(double angle){
        mArrowAngle = angle;
    }

    public void drawArrow(Canvas canvas, Paint paint){
        //没有松手,则不画三角
        if(mState != BlockSate.SUCCESS && mState != BlockSate.ERRO){
            return;
        }

        float arrowLen = (mBigRadius - mLittleRadius)*0.5f;
        float arrowLeftX = mCenterPointX + mLittleRadius + (mBigRadius - mLittleRadius - arrowLen)/2;
        float arrowRightX = arrowLeftX + arrowLen;
        float topY = mCenterPointY - arrowLen;
        float bottomY = mCenterPointY + arrowLen;
        mArrow.moveTo(arrowRightX, mCenterPointY);
        mArrow.lineTo(arrowLeftX, topY);
        mArrow.lineTo(arrowLeftX, bottomY);
        mArrow.close();

        canvas.save();
        canvas.rotate((float) mArrowAngle, mCenterPointX, mCenterPointY);
        canvas.drawPath(mArrow, paint);
        canvas.restore();
    }
    public enum BlockSate {
        IDLE,//空闲
        HITTED,//手指触摸
        ERRO,//密码错误
        SUCCESS;//密码正确
    }

}

说明:drawArrow实现根据指向下一节点的方向绘制三角,三角形采用Path实现,三角形的方向调整则通过canvas.rotate方法实现。每个节点的指引角度是在手势释放后,根据击中的节点列表来依次计算。

2.GestureLockView实现:

package com.star.gesturelock;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by kakaxicm on 16/6/7.
 */
public class GestureLockView extends View {
    private float mSize;//w=h

    private final float MBIGRADIUSFRACTION = 40/300.0f;
    private final float MLITTLERADIUSFRACTION = 15/300.0f;

    private float mLittleRadius;//小圆半径
    private float mBigRadius;//大圆半径

    private List<Block> mBaseBlocks = new ArrayList<>();
    private List<Integer> mSelectedIds = new ArrayList<>();

    private Paint mBigCirclePaint;
    private Paint mSmallCirclePaint;
    private Paint mLinePaint;//滑动过程中的折线和指引线paint

    private Path mPath;//滑动过程中的折线
    private float mNodeLineX;//折线的节点位置
    private float mNodeLineY;
    private float mLineTmpX;//指引线的终点
    private float mLineTmpY;

    String mAnswer = "012543678";//预设密码

    /**
     * 手势锁回调
     */
    public interface OnGestureLockListener{
        void onBlockHitted(int index);//block被触摸到
        void onGestureLockSuccess(String password);
        void onGestureLockFail();
    }

    private OnGestureLockListener mGestureLockListener;

    public void setmGestureLockListener(OnGestureLockListener listener){
        mGestureLockListener = listener;
    }

    public GestureLockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setBackgroundColor(Color.GRAY);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
        int resultWidth = wSize;
        int resultHeight = hSize;
        Resources r = Resources.getSystem();
        //lp = wrapcontent时 指定默认值
        if(wMode == MeasureSpec.AT_MOST){
            resultWidth =  (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, r.getDisplayMetrics());
        }
        if(hMode == MeasureSpec.AT_MOST){
            resultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, r.getDisplayMetrics());
        }
        int size = resultWidth>resultHeight?resultHeight:resultWidth;
        setMeasuredDimension(size, size);
        initParams();
    }

    /**
     * 绘制涉及参量的初始化操作
     */
    private void initParams(){
        mSize = getMeasuredWidth();
        mBigRadius = MBIGRADIUSFRACTION*mSize;
        mLittleRadius = MLITTLERADIUSFRACTION*mSize;

        mBigCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mSmallCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setStrokeCap(Paint.Cap.ROUND);
        mLinePaint.setStrokeJoin(Paint.Join.ROUND);
        mLinePaint.setStrokeWidth(mLittleRadius*2);
        mLinePaint.setColor(Color.parseColor("#4400ff00"));

        mPath = new Path();
        //blocks初始化
        if(mBaseBlocks.size() == 0){
            for(int i = 0; i < 3; i++){
                for(int j = 0; j < 3; j++){
                    //构建3*3 block
                    Block block = new Block();
                    float centerX = mSize*(1+j*2)/6;
                    float centerY = mSize*(1+i*2)/6;
                    block.mCenterPointX = centerX;
                    block.mCenterPointY = centerY;
                    block.mBigRadius = mBigRadius;
                    block.mLittleRadius = mLittleRadius;
                    block.mId = i*3+j;
                    mBaseBlocks.add(block);
                }
            }
        }

    }

    /**
     * 绘制blocks、折线、指引线
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
       for(int i = 0; i < mBaseBlocks.size(); i++){
           Block block = mBaseBlocks.get(i);
           drawBlock(canvas, block);
       }
        //绘制折线和指引线
        canvas.drawPath(mPath, mLinePaint);
        if(mSelectedIds.size()>0){
            canvas.drawLine(mNodeLineX, mNodeLineY, mLineTmpX, mLineTmpY, mLinePaint);
        }

    }

    /**
     * 绘制基本单元
     * 1.大、小圆
     * 2.三角指示
     * @param canvas
     * @param block
     */
    private void drawBlock(Canvas canvas, Block block){
        if(block.mState == Block.BlockSate.IDLE){
            mBigCirclePaint.setColor(block.mIdleBigCircleColor);
            mSmallCirclePaint.setColor(block.mIdleLittleCircleColor);
        }else if(block.mState == Block.BlockSate.HITTED){
            mBigCirclePaint.setColor(block.mHittedBigCircleColor);
            mSmallCirclePaint.setColor(block.mHittedLittleCircleColor);
        }else if(block.mState == Block.BlockSate.SUCCESS){
            mBigCirclePaint.setColor(block.mSuccessBigCircleColor);
            mSmallCirclePaint.setColor(block.mSuccessdLittleCircleColor);
        }else if(block.mState == Block.BlockSate.ERRO){
            mBigCirclePaint.setColor(block.mErroBigCircleColor);
            mSmallCirclePaint.setColor(block.mErroLittleCircleColor);
        }

        canvas.drawCircle(block.mCenterPointX,block.mCenterPointY, block.mBigRadius, mBigCirclePaint);
        canvas.drawCircle(block.mCenterPointX,block.mCenterPointY, block.mLittleRadius, mSmallCirclePaint);

        //画三角指示符
        if(mSelectedIds.size() > 0){
            if(block.mId != mSelectedIds.get(mSelectedIds.size()-1)){//最后一个不画三角
                block.drawArrow(canvas,mSmallCirclePaint);
            }
        }
    }

    /**
     * 核心代码,控制手势监听的逻辑
     * step1:ACTION_DOWN 做复位操作
     * setp2:ACTION_MOVE 监测手指滑到哪个block,同时更新block状态、指引线及折线
     * step3:ACTION_UP 校验密码、更新选中的block状态、设置选中的block三角角度
     * srep4:前三步都会更改绘制涉及的参数,需要重绘操作
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                reset();
                break;
            case MotionEvent.ACTION_MOVE:
                float x = event.getX();
                float y = event.getY();
                Block block = checkHitBlock(x, y);
                //探测未选中的block
                if(block != null && !mSelectedIds.contains(block.mId)){//探测到
                    if(mGestureLockListener != null){
                        mGestureLockListener.onBlockHitted(block.mId);
                    }
                    //手指触摸到block,作以下处理:
                    //1.block状态处理
                    //2.path的节点设置为block的中心
                    //3.指引线的终点设为节点位置
                    block.mState = Block.BlockSate.HITTED;
                    mSelectedIds.add(block.mId);
                    mNodeLineX = block.mCenterPointX;
                    mNodeLineY = block.mCenterPointY;//折线变为block的圆心

                    if(mSelectedIds.size() == 1){//手指第一次选中block
                        mPath.moveTo(mNodeLineX, mNodeLineY);
                    }else{
                        mPath.lineTo(mNodeLineX, mNodeLineY);
                    }
                    mLineTmpX = mNodeLineX;
                    mLineTmpY = mNodeLineY;
                }else{//未探测到
                    //手指未触摸到block,则只需要设置指引线终点即可
                    mLineTmpX = x;
                    mLineTmpY = y;
                }

                break;
            case MotionEvent.ACTION_UP:
                //选中的block 改为error/success状态
                changeReleaseBlockState();
                //折线处理,终点回退到节点,实现取消指引线的效果
                mLineTmpX = mNodeLineX;
                mLineTmpY = mNodeLineY;
                //三角角度设置
                configBlockArrowAngles();
                break;
            default:
                break;
        }
        invalidate();
        return true;
    }

    /**
     * 手指松开时,根据选中的block,设置三角的角度
     */
    private void configBlockArrowAngles(){
        for(int i = 0; i < mSelectedIds.size()-1; i++){
            int index = mSelectedIds.get(i);
            int nextIndex = mSelectedIds.get(i+1);
            Block curBlock = mBaseBlocks.get(index);
            Block nextBlock = mBaseBlocks.get(nextIndex);

            float offsetX = nextBlock.mCenterPointX - curBlock.mCenterPointX;
            float offsetY = nextBlock.mCenterPointY - curBlock.mCenterPointY;
            double angle = Math.toDegrees(Math.atan2(offsetY,offsetX));
            curBlock.setArrowAngle(angle);
            Log.e("ANGLES",angle+"");
        }
    }

    /**
     * 松手时,检测结果,修改选中的block状态
     */
    private void changeReleaseBlockState(){
        StringBuilder sb = new StringBuilder();
        for(int i = 0;i < mSelectedIds.size();i++){
            sb.append(mSelectedIds.get(i));
        }

        boolean isSuccess = TextUtils.equals(mAnswer, sb.toString());

        if(mGestureLockListener != null){
            if(isSuccess){
                mGestureLockListener.onGestureLockSuccess(mAnswer);
            }else {
                mGestureLockListener.onGestureLockFail();
            }
        }
        //设置选中的block的状态
        for(int i = 0; i < mBaseBlocks.size(); i++){
            Block block = mBaseBlocks.get(i);
            if(mSelectedIds.contains(block.mId)){
                if(isSuccess){
                    block.mState = Block.BlockSate.SUCCESS;
                }else{
                    block.mState = Block.BlockSate.ERRO;
                }
            }
        }
    }

    /**
     * 复位所有block、path、指引线状态
     */
    private void reset(){
        mPath.reset();
        mSelectedIds.clear();
        for(int i = 0; i < mBaseBlocks.size(); i++){
            Block block = mBaseBlocks.get(i);
            block.mState = Block.BlockSate.IDLE;
            block.mArrowAngle = 0;
        }

        mLineTmpX = 0;
        mLineTmpY = 0;
        mNodeLineX = 0;
        mNodeLineY = 0;
    }

    /**
     * 检测位置落在哪个block上
     * @param x
     * @param y
     * @return
     */
    private Block checkHitBlock(float x, float y){
        for(int i = 0; i < mBaseBlocks.size(); i++){
            Block block = mBaseBlocks.get(i);
            float startX = block.mCenterPointX - block.mBigRadius;
            float endX = block.mCenterPointX + block.mBigRadius;
            float startY = block.mCenterPointY - block.mBigRadius;
            float endY = block.mCenterPointY + block.mBigRadius;

            if(x >= startX && x <=endX && y >= startY && y <= endY){
                return block;
            }
        }
        return null;
    }
}

说明:

1>initParams方法初始化了各个画笔、连接点。核心是初始化3*3的Block阵列。

2>核心逻辑处理逻辑在onTouchEvent中:

ACTION_DOWN方法复位整个View的状态;

ACTION_MOVE做探测击中的节点,如果探测到,则更新连线节点、探测线的起点以及节点状态(Hitted),如果未探测到,则只需更新探测线的终点;

ACTION_UP校验密码并更新所有被选中的节点的状态和三角箭头方向、取消探测线绘制。上面的操作均改变了绘制涉及的参量,所以都唤起View重绘。

3> configBlockArrowAngles方法根据两个节点的位置计算三角箭头的方向;changeReleaseBlockState()主要做密码校验用;checkHitBlock做从手指位置到节点的探测;drawBlock主要实现不同状态下的节点和三角器绘制;onDraw方法调用每个节点的绘制和折线、探测线的绘制。

用例:

  GestureLockView lockView = (GestureLockView) findViewById(R.id.gesturelock);
        lockView.setmGestureLockListener(new GestureLockView.OnGestureLockListener() {
            @Override
            public void onBlockHitted(int index) {
                Log.e("GestureLockView",index+"");
            }

            @Override
            public void onGestureLockSuccess(String password) {
                Log.e("GestureLockView",password);
            }

            @Override
            public void onGestureLockFail() {
                Log.e("GestureLockView","erro");
            }
        });

希望大家通过这篇博客的分享,能在自定义View过程中对基本的触摸事件的处理如鱼得水!如有问题和更好的实现方案,希望大家指正。

时间: 2024-08-01 10:46:24

Android进阶之自定义View实战(二)九宫格手势解锁实现的相关文章

【Android自定义View实战】之超简单SearchView

[Android自定义View实战]之超简单SearchView 在Android开发中我们经常会用到搜索框,而系统提供的又不尽完美.所以自定义一个比较简单的SearchView. 效果图 实现代码 package cn.bluemobi.dylan.searchview; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; import android.ut

【Android自定义View实战】之仿百度加载动画,一种优雅的Loading方式

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53470872 本文出自[DylanAndroid的博客] Android自定义View实战之仿百度加载动画一种优雅的Loading方式 第一个仿百度加载动画用ObjectAnimator属性动画操作ImageView的属性方法实现 第二个仿百度加载动画第二种实现方式用ValueAnimator原生的ondraw方法实现 第三个扔球动画-水平旋转动画 第四个扔球动画-垂直旋转动

Android进阶:自定义视频播放器开发(下)

上一篇文章我们主要讲了视频播放器开发之前需要准备的一个知识,TextureView,用于对图像流的处理.这篇文章开始构建一个基础的视频播放器. 一.准备工作 在之前的文章已经说过了,播放器也是一个view,我们要在这个view上播放视频流.所以我们要自定义一个简单的viewgroup,比如继承FrameLayout.还出就是布局简单,其他控件可以往上面添加.大家见过的视频播放器的控制器都是放在视频的上方的.这样就是用FrameLayout布局是最好的. class SmallVideoPlaye

自定义view(二)

这里是自定义view(二),上一篇关于自定义view的一些基本知识,比如说自定义view的步骤.会涉及到哪些函数以及如何实现自定义属性,同时实现了一个很基础的自定义控件,一个自定义的计时器,需要看的人可以点击这个链接:http://www.cnblogs.com/YaoJianXun/p/5806926.html. 这次讲的是如何通过坐标系的变化实现一些更复杂的自定义view绘制,上一次博客我们实现了一个类似于计时器的环形控件,这次我们在那个基础上再做一次改动,通过坐标系的变动实现下面的效果:

【android自定义控件】自定义View属性

1.自定义View的属性 2.在View的构造方法中获得我们自定义的属性 3.重写onMesure 4.重写onDraw 3这个步骤不是必须,当然了大部分情况下还是需要重写的. 1.自定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式. <?xml version="1.0" encoding="utf-8"?> <resources> <attr name=&

Android自定义View(二)

前言 魅族手机的闹钟应用中有个倒计时,这个控件还是蛮有趣的.左边是魅族闹钟,右边是我们最终实现的效果,虽然有些细节还需优化,不过基本上已经达到了想要的效果,我们先来就来看看如何实现吧. 分析 确定宽高 对一个Android自定义控件来说,一般都经过三个步骤 onLayout() onMeasure() onDraw() onLayout明确子控件在父控件中的位置(本控件不需要重写),onMeasure是确定控件的大小(宽.高),而onDraw是我们重点关注的方法,我们需要在这个方法中写入显示Vi

【Android自定义View实战】之仿QQ运动步数圆弧及动画,Dylan计步中的控件StepArcView

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/52936609[DylanAndroid的csdn博客] 在之前的Android超精准计步器开发-Dylan计步中的首页用到了一个自定义控件,和QQ运动的界面有点类似,还有动画效果,下面就来讲一下这个View是如何绘制的. 1.先看效果图 2.效果图分析 功能说明:黄色的代表用户设置的总计划锻炼步数,红色的代表用户当前所走的步数. 初步分析:完全自定义View重写onDraw(

Android自定义View(二、深入解析自定义属性)

转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51468648 本文出自:[openXu的博客] 目录: 为什么要自定义属性 怎样自定义属性 属性值的类型format 类中获取属性值 Attributeset和TypedArray以及declare-styleable ??在上一篇博客<Android自定义View(一.初体验)>中我们体验了自定义控件的基本流程: 继承View,覆盖构造方法 自定义属性 重写onMeasure方法测量宽

Android 高手进阶之自定义View,自定义属性(带进度的圆形进度条)

转载请注明地址:http://blog.csdn.net/xiaanming/article/details/10298163 很多的时候,系统自带的View满足不了我们功能的需求,那么我们就需要自己来自定义一个能满足我们需求的View,自定义View我们需要先继承View,添加类的构造方法,重写父类View的一些方法,例如onDraw,为了我们自定义的View在一个项目中能够重用,有时候我们需要自定义其属性,举个很简单的例子,我在项目中的多个界面使用我自定义的View,每个界面该自定义View