Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android進階之自定義View實戰(二)九宮格手勢解鎖實現

Android進階之自定義View實戰(二)九宮格手勢解鎖實現

編輯:關於Android編程

一.引言

本篇博客以九宮格手勢解鎖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 mBaseBlocks = new ArrayList<>();
    private List 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過程中對基本的觸摸事件的處理如魚得水!如有問題和更好的實現方案,希望大家指正。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved