自定義九宮格

首先來介紹一下這個自定義View:

(1)這個自定義View的名稱叫做 LockView ,繼承自View類;
(2)這個自定義View實現了應用中常見的九宮格手勢解鎖功能,可以用於保證應用安全;
(3)用戶可以自定義控件在不同狀態下顯示的顏色、什麼情況算解鎖成功、解鎖成功或失敗回調的方法等。
  接下來介紹一下在這個自定義View中用到的技術點:

(1)自定義屬性;
(2)在 onMeasure() 方法中對控件進行測量,保證九宮格顯示在屏幕的中央;
(3)在 onDraw() 方法中根據用戶的手勢繪製圓圈和連線;
(4)在 onTouchEvent() 方法中接收用戶的觸摸事件並進行相應的處理;
(5)將判斷解鎖是否成功以及解鎖成功或失敗的情況下回調的方法抽取成接口,供用戶自定義。
  下面是這個自定義View—— LockView 的實現代碼:

  自定義View類 LockView.java 中的代碼:

package com.example.a04_lockview.lockview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import com.example.a04_lockview.CircleRect;
import com.example.a04_lockview.R;

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

/**
 * Created by yujie on 2017/12/1.
 */

public class LockView extends View{
    // 狀態常量
    private static final int STATE_NORMAL = 0x001; // 默認狀態
    private static final int STATE_SELECT = 0x002; // 選中狀態
    private static final int STATE_CORRECT = 0x003; // 正確狀態
    private static final int STATE_WRONG = 0x004; // 錯誤狀態
    // 自定義屬性
    private int normalColor = Color.GRAY; // 默認顯示的顏色
    private int selectColor = Color.YELLOW; // 選中時顯示的顏色
    private int correctColor = Color.GREEN; // 正確時顯示的顏色
    private int wrongColor = Color.RED; // 錯誤時顯示的顏色
    private int lineWidth = -1; // 連線的寬度
    // 寬高相關
    private int width; // 父佈局分配給這個View的寬度
    private int height; // 父佈局分配給這個View的高度
    private int rectRadius; // 每個小圓圈的寬度(直徑)
    // 元素相關
    private List<CircleRect> rectList; // 存儲所有圓圈對象的列表
    private List<CircleRect> pathList; // 存儲用戶繪製的連線上的所有圓圈對象
    // 繪製相關
    private Canvas mCanvas; // 用於繪製元素的畫布
    private Bitmap mBitmap; // 用戶繪製元素的Bitmap
    private Path mPath; // 用戶繪製的線條
    private Path tmpPath; // 記錄用戶以前繪製過的線條
    private Paint circlePaint; // 用戶繪製圓圈的畫筆
    private Paint pathPaint; // 用戶繪製連線的畫筆
    // 觸摸相關
    private int startX; // 上一個節點的X座標
    private int startY; // 上一個節點的Y座標
    private boolean isUnlocking; // 是否正在解鎖(手指落下時是否剛好在一個節點上)
    // 結果相關
    private OnUnlockListener listener;

    public LockView(Context context) {
        this(context, null);
    }

    public LockView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 初始化一些對象(List等)
        rectList = new ArrayList<>();
        pathList = new ArrayList<>();
        // 獲取自定義屬性
        TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LockView, defStyleAttr, 0);
        int count = array.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = array.getIndex(i);
            switch (attr) {
                case R.styleable.LockView_normalColor:
                    normalColor = array.getColor(attr, Color.GRAY);
                    break;
                case R.styleable.LockView_selectColor:
                    selectColor = array.getColor(attr, Color.YELLOW);
                    break;
                case R.styleable.LockView_correctColor:
                    correctColor = array.getColor(attr, Color.GREEN);
                    break;
                case R.styleable.LockView_wrongColor:
                    wrongColor = array.getColor(attr, Color.RED);
                    break;
                case R.styleable.LockView_lineWidth:
                    lineWidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, context.getResources().getDisplayMetrics()));
                    break;
            }
        }
        if (lineWidth == -1) {
            lineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, context.getResources().getDisplayMetrics());
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 獲取到控件的寬高屬性值
        width = getMeasuredWidth();
        height = getMeasuredHeight();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 初始化繪製相關的元素
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);
        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);
        circlePaint.setDither(true);
        mPath = new Path();
        tmpPath = new Path();
        pathPaint = new Paint();
        pathPaint.setDither(true);
        pathPaint.setAntiAlias(true);
        pathPaint.setStyle(Paint.Style.STROKE);
        pathPaint.setStrokeCap(Paint.Cap.ROUND);
        pathPaint.setStrokeJoin(Paint.Join.ROUND);
        pathPaint.setStrokeWidth(lineWidth);
        // 初始化一些寬高屬性
        int horizontalSpacing;
        int verticalSpacing;
        if (width <= height) {
            horizontalSpacing = 0;
            verticalSpacing = (height - width) / 2;
            rectRadius = width / 14;
        } else {
            horizontalSpacing = (width - height) / 2;
            verticalSpacing = 0;
            rectRadius = height / 14;
        }
        // 初始化所有CircleRect對象
        for (int i = 1; i <= 9; i++) {
            int x = ((i - 1) % 3 * 2 + 1) * rectRadius * 2 + horizontalSpacing + getPaddingLeft() + rectRadius;
            int y = ((i - 1) / 3 * 2 + 1) * rectRadius * 2 + verticalSpacing + getPaddingTop() + rectRadius;
            CircleRect rect = new CircleRect(i, x, y, STATE_NORMAL);
            rectList.add(rect);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, 0, 0, null);
        for (int i = 0; i < rectList.size(); i++) {
            drawCircle(rectList.get(i), rectList.get(i).getState());
        }
        canvas.drawPath(mPath, pathPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int currX = (int) event.getX();
        int currY = (int) event.getY();
        CircleRect rect = getOuterRect(currX, currY);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 保證手指按下後所有元素都是初始狀態
                this.reset();
                // 判斷手指落點是否在某個圓圈中,如果是則設置該圓圈爲選中狀態
                if (rect != null) {
                    rect.setState(STATE_SELECT);
                    startX = rect.getX();
                    startY = rect.getY();
                    tmpPath.moveTo(startX, startY);
                    pathList.add(rect);
                    isUnlocking = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (isUnlocking) {
                    mPath.reset();
                    mPath.addPath(tmpPath);
                    mPath.moveTo(startX, startY);
                    mPath.lineTo(currX, currY);
                    if (rect != null) {
                        rect.setState(STATE_SELECT);
                        startX = rect.getX();
                        startY = rect.getY();
                        tmpPath.lineTo(startX, startY);
                        pathList.add(rect);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                isUnlocking = false;
                if (pathList.size() > 0) {
                    mPath.reset();
                    mPath.addPath(tmpPath);
                    StringBuilder result = new StringBuilder();
                    for (int i = 0; i < pathList.size(); i++) {
                        result.append(pathList.get(i).getCode());
                    }
                    if (listener.isUnlockSuccess(result.toString())) {
                        listener.onSuccess();
                        setWholePathState(STATE_CORRECT);
                    } else {
                        listener.onFailure();
                        setWholePathState(STATE_WRONG);
                    }
                }
                break;
        }
        invalidate();
        return true;
    }

    /**
     * 根據狀態(解鎖成功/失敗)改變整條路徑上所有元素的顏色
     *
     * @param state 狀態(解鎖成功/失敗)
     */
    private void setWholePathState(int state) {
        pathPaint.setColor(getColorByState(state));
        for (CircleRect rect : pathList) {
            rect.setState(state);
        }
    }

    /**
     * 通過狀態得到應顯示的顏色
     *
     * @param state 狀態
     * @return 給定狀態下應該顯示的顏色
     */
    private int getColorByState(int state) {
        int color = normalColor;
        switch (state) {
            case STATE_NORMAL:
                color = normalColor;
                break;
            case STATE_SELECT:
                color = selectColor;
                break;
            case STATE_CORRECT:
                color = correctColor;
                break;
            case STATE_WRONG:
                color = wrongColor;
                break;
        }
        return color;
    }

    /**
     * 根據參數中提供的圓圈參數繪製圓圈
     *
     * @param rect  存儲圓圈所有參數的CircleRect對象
     * @param state 圓圈的當前狀態
     */
    private void drawCircle(CircleRect rect, int state) {
        circlePaint.setColor(getColorByState(state));
        mCanvas.drawCircle(rect.getX(), rect.getY(), rectRadius, circlePaint);
    }

    /**
     * 判斷參數中的x、y座標對應的點是否在某個圓圈內,如果在則返回這個圓圈,否則返回null
     *
     * @param x 給定的點的X座標
     * @param y 給定的點的Y座標
     * @return 給定點所在的圓圈對象,如果不在任何一個圓圈內則返回null
     */
    private CircleRect getOuterRect(int x, int y) {
        for (int i = 0; i < rectList.size(); i++) {
            CircleRect rect = rectList.get(i);
            if ((x - rect.getX()) * (x - rect.getX()) + (y - rect.getY()) * (y - rect.getY()) <= rectRadius * rectRadius) {
                if (rect.getState() != STATE_SELECT) {
                    return rect;
                }
            }
        }
        return null;
    }

    /**
     * 解鎖,手指擡起後回調的藉口
     */
    public interface OnUnlockListener {
        // 由用戶來判斷解鎖是否成功
        boolean isUnlockSuccess(String result);

        // 當解鎖成功時回調的方法
        void onSuccess();

        // 當解鎖失敗時回調的方法
        void onFailure();
    }

    /**
     * 爲當前View設置結果監聽器
     */
    public void setOnUnlockListener(OnUnlockListener listener) {
        this.listener = listener;
    }

    /**
     * 重置所有元素的狀態到初始狀態
     */
    public void reset() {
        setWholePathState(STATE_NORMAL);
        pathPaint.setColor(selectColor);
        mPath.reset();
        tmpPath.reset();
        pathList = new ArrayList<>();
    }
}
自定義屬性文件 res/values/attr.xml 中的代碼:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="normalColor" format="color" /> <!-- 正常狀態下圓圈的顏色 -->
    <attr name="selectColor" format="color" /> <!-- 選中狀態下圓圈的顏色 -->
    <attr name="correctColor" format="color" /> <!-- 正確狀態下圓圈的顏色 -->
    <attr name="wrongColor" format="color" /> <!-- 錯誤狀態下圓圈的顏色 -->
    <attr name="lineWidth" format="dimension" /> <!-- 連線的寬度 -->

    <declare-styleable name="LockView">
        <attr name="normalColor" />
        <attr name="selectColor" />
        <attr name="correctColor" />
        <attr name="wrongColor" />
        <attr name="lineWidth" />
    </declare-styleable>
</resources>
存儲每個圓圈屬性的實體類 CircleRect.java 中的代碼:
package com.example.a04_lockview;

/**
 * Created by yujie on 2017/12/1.
 */

public class CircleRect {
    // 圓圈所代表的數字(1~9)
    private int code;
    // 圓心的X座標
    private int x;
    // 圓心的Y座標
    private int y;
    // 圓圈的當前狀態
    private int state;

    public CircleRect() {
    }

    public CircleRect(int code, int x, int y, int state) {
        this.code = code;
        this.x = x;
        this.y = y;
        this.state = state;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }
}
主界面佈局文件 activity_main.xml 中的代碼:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.a04_lockview.MainActivity">

    <com.example.a04_lockview.lockview.LockView
        android:id="@+id/lock_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:correctColor="#00FF00"
        app:lineWidth="5.0dip"
        app:normalColor="#888888"
        app:selectColor="#FFFF00"
        app:wrongColor="#FF0000" />

</RelativeLayout>
主界面JAVA文件 MainActivity.java 中的代碼:
package com.example.a04_lockview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.Toast;

import com.example.a04_lockview.lockview.LockView;

public class MainActivity extends AppCompatActivity {

    private LockView lockView; // 自定義九宮格手勢解鎖控件

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 通過 ID 找到控件
        lockView = findViewById(R.id.lock_view);
        // 初始化事件
        initEvents();
    }

    /**
     * 初始化事件
     */
    private void initEvents() {
        // 爲LockView設置監聽器
        lockView.setOnUnlockListener(new LockView.OnUnlockListener() {
            // 設置在什麼情況下視爲解鎖成功
            @Override
            public boolean isUnlockSuccess(String result) {
                Log.i("result++++++++++", result);
                return "77441155336699".equals(result);
            }

            // 當解鎖成功時回調的方法
            @Override
            public void onSuccess() {
                Toast.makeText(MainActivity.this, "Unlock Success!", Toast.LENGTH_SHORT).show();
            }

            // 當解鎖失敗時回調的方法
            @Override
            public void onFailure() {
                Toast.makeText(MainActivity.this, "Unlock Failed!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章