1. 程式人生 > >android自定義控制元件手勢密碼

android自定義控制元件手勢密碼

現在很多app都用到一種安全機制,手勢密碼,特別是銀行相關的app,雖然他也並不是那麼安全,但是就是喜歡用。今天來看一個簡單而炫酷的手勢密碼鎖,廢話不多說,上圖上程式碼。

這裡寫圖片描述

看圖說話,想怎麼定義就怎麼定義,使用起來就是這麼任性。。。

這裡寫圖片描述

箭頭可以隨手指任意旋轉,這就是我要的效果

這裡寫圖片描述

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="count" format="integer" />
    <attr name="outerNorColor" format="color"
/>
<attr name="innerNorColor" format="color" /> <attr name="outerPressColor" format="color" /> <attr name="innerPressColor" format="color" /> <attr name="outerUpColor" format="color" /> <attr name="innerUpColor" format="color" /> <attr name="mlineColor"
format="color" />
<declare-styleable name="LockViewGroup"> <attr name="count" /> <attr name="outerNorColor" /> <attr name="innerNorColor" /> <attr name="outerPressColor" /> <attr name="innerPressColor" /> <attr name
="outerUpColor" />
<attr name="innerUpColor" /> <attr name="mlineColor" /> </declare-styleable> </resources>

將用到的屬性定義出來,供使用者自己選擇定義。

package com.example.apple.Custom;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by apple on 17/9/16.
 */

public class LockItem extends View {

    final String TAG = this.getClass().getSimpleName();

    private Mode states = Mode.NOR;
    private int width, hight;
    private int outerCircleWidth = 2, outerCircleRadius, innerCircleRadius, centerXY;

    //paint
    private Paint mouerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);//去鋸齒
    private Paint minnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    //color
    private int outerNorColor, innerNorColor;
    private int outerPressColor, innerPressColor;
    private int outerUpColor, innerUpColor;

    //Arrow
    private int mArrowline;
    private Path mArrowPath = new Path();
    private int angle = -1000;//角度,這裡值給大點,不然可能有問題

    enum Mode {NOR, PRESS, UP}

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

    public LockItem(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LockItem(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public LockItem(Context context, int outerNorColor, int innerNorColor, int outerPressColor, int innerPressColor, int outerUpColor, int innerUpColor) {
        this(context);
        this.outerNorColor = outerNorColor;
        this.innerNorColor = innerNorColor;
        this.outerPressColor = outerPressColor;
        this.innerPressColor = innerPressColor;
        this.outerUpColor = outerUpColor;
        this.innerUpColor = innerUpColor;
    }

    public int getCenterXY() {
        return centerXY;
    }


    private void init() {
        mouerCirclePaint.setColor(outerNorColor);
        mouerCirclePaint.setStyle(Paint.Style.STROKE);//空心畫筆
        mouerCirclePaint.setStrokeWidth(outerCircleWidth);
        minnerCirclePaint.setStyle(Paint.Style.FILL);//實心
        minnerCirclePaint.setColor(innerNorColor);
        mArrowPath.reset();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wsize = MeasureSpec.getSize(widthMeasureSpec);
        int hsize = MeasureSpec.getSize(heightMeasureSpec);
        width = hight = Math.min(wsize, hsize);
        centerXY = width / 2;
        outerCircleRadius = (width - 2) / 2;
        innerCircleRadius = width / 6;
        setMeasuredDimension(width, hight);
        mArrowPath.reset();
        mArrowline = (int) (width * 1.0 / 2 * 0.3);
        //指引三角形箭頭
        mArrowPath.moveTo(width / 2 - mArrowline, centerXY - innerCircleRadius - 4);
        mArrowPath.lineTo(width / 2 + mArrowline, centerXY - innerCircleRadius - 4);
        mArrowPath.lineTo(width / 2, (outerCircleRadius - innerCircleRadius) / 3);
        mArrowPath.close();
    }

    public LockItem setMode(Mode mode) {
        states = mode;
        return this;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (states == Mode.NOR) {
            mouerCirclePaint.setColor(outerNorColor);
            minnerCirclePaint.setColor(innerNorColor);
        } else if (states == Mode.PRESS) {
            mouerCirclePaint.setColor(outerPressColor);
            minnerCirclePaint.setColor(innerPressColor);
        } else if (states == Mode.UP) {
            mouerCirclePaint.setColor(outerUpColor);
            minnerCirclePaint.setColor(innerUpColor);
        }

        canvas.drawCircle(centerXY, centerXY, outerCircleRadius, mouerCirclePaint);
        canvas.drawCircle(centerXY, centerXY, innerCircleRadius, minnerCirclePaint);
        if (angle != -1000) {
            canvas.rotate(angle, centerXY, centerXY);
            canvas.drawPath(mArrowPath, mouerCirclePaint);
        }
    }

    public LockItem setAngle(int angle) {
        this.angle = angle;
        return this;
    }


}

LockItem裡面定義了列舉狀態,根據使用者的手指觸控事件修改狀態,來控制view的繪製顏色。裡面還定義了一個三角箭頭,看效果圖能看出效果,根據手勢去控制箭頭的方向,這個是難點,箭頭看起來簡單,控制起來卻不是那麼簡單,哎,又少了幾根頭髮。。。

package com.example.apple.Custom;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Toast;

import com.example.apple.pullzoom.R;

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

/**
 * Created by apple on 17/9/16.
 */

public class LockViewGroup extends ViewGroup {

    final String TAG = this.getClass().getSimpleName();

    //密碼元素的寬高
    private int lockItemWidth;
    //元素的個數
    private int count = 3;
    //裝item容器
    private LockItem[] lockItems;
    //每一個item的間隔
    private int spacingWidth;
    private int width, hight;
    private List<Integer> selectLockViews = new ArrayList<>();
    private Integer[] pwd = new Integer[]{1, 4, 7, 8, 9};

    private int donwx, downy, scaledTouchSlop;
    //color
    private int outerNorColor = Color.RED, innerNorColor = Color.RED;
    private int outerPressColor = Color.BLUE, innerPressColor = Color.BLUE;
    private int outerUpColor = Color.YELLOW, innerUpColor = Color.YELLOW;
    private int mlineColor = Color.BLACK;

    private Paint mlinePait = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Path mlinePath = new Path();

    private int tempx, tempy;
    private LockItem lastView;
    private int pathCenterX, pathCenterY, lastPointX, lastPointY;

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

    public LockViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        mlinePait.setColor(mlineColor);
        mlinePait.setStrokeWidth(30);
        mlinePait.setAlpha(150);
        mlinePait.setStyle(Paint.Style.STROKE);
        mlinePait.setStrokeCap(Paint.Cap.ROUND);
        mlinePait.setStrokeJoin(Paint.Join.ROUND);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.LockViewGroup,defStyleAttr,0);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
        int arr = typedArray.getIndex(i);
            switch (arr) {
                case R.styleable.LockViewGroup_count:
                    count = typedArray.getInteger(arr, 3);
                    break;
                case R.styleable.LockViewGroup_outerNorColor:
                    outerNorColor = typedArray.getColor(arr, Color.RED);
                    break;
                case R.styleable.LockViewGroup_innerNorColor:
                    innerNorColor = typedArray.getColor(arr, Color.RED);
                    break;
                case R.styleable.LockViewGroup_outerPressColor:
                    outerPressColor = typedArray.getColor(arr, Color.BLUE);

                    break;
                case R.styleable.LockViewGroup_innerPressColor:
                    innerPressColor = typedArray.getColor(i, Color.BLUE);
                    break;
                case R.styleable.LockViewGroup_outerUpColor:
                    outerUpColor = typedArray.getColor(arr, Color.YELLOW);
                    break;
                case R.styleable.LockViewGroup_innerUpColor:
                    mlineColor = typedArray.getColor(arr, Color.YELLOW);
                    break;
                case R.styleable.LockViewGroup_mlineColor:
                    innerUpColor = typedArray.getColor(arr, Color.YELLOW);
                    break;


            }

        }

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wsize = MeasureSpec.getSize(widthMeasureSpec);
        int hsize = MeasureSpec.getSize(heightMeasureSpec);
        width = hight = Math.min(wsize, hsize);
        lockItemWidth = (2 * width) / (3 * count + 1);// width / (count + (count + 1) / 2);間隔是item寬度的一半
        spacingWidth = lockItemWidth / 2;
        if (lockItems == null) {
            lockItems = new LockItem[count * count];
            removeAllViews();
            for (int i = 0, j = count * count; i < j; i++) {
                LockItem lockItem = new LockItem(getContext(), outerNorColor, innerNorColor, outerPressColor, innerPressColor, outerUpColor, innerUpColor);
                ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(lockItemWidth, lockItemWidth);
                lockItem.setLayoutParams(layoutParams);
                lockItem.setTag(j - i);
                lockItems[i] = lockItem;
                addView(lockItem);
                measureChild(lockItem, lockItemWidth, lockItemWidth);
            }
        }
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int mChildCount = getChildCount();
        long num = (long) Math.sqrt(mChildCount);
        for (int i = 0; i < num; i++) {
            for (int j = 0; j < num; j++) {
                View childAt = getChildAt(--mChildCount);
                childAt.layout(
                        spacingWidth * (j + 1) + j * lockItemWidth,
                        spacingWidth * (i + 1) + i * lockItemWidth,
                        lockItemWidth * (j + 1) + spacingWidth * (j + 1),
                        lockItemWidth * (i + 1) + spacingWidth * (i + 1));
            }
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.drawPath(mlinePath, mlinePait);
        if (pathCenterX > 0 && pathCenterY > 0) {
            canvas.drawLine(pathCenterX, pathCenterY, lastPointX, lastPointY, mlinePait);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        tempx = (int) event.getX();
        tempy = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                reset(tempx, tempy);
                break;
            case MotionEvent.ACTION_MOVE:
                int dy = downy - tempy;
                int dx = donwx - tempx;
                if (Math.abs(dy) > scaledTouchSlop || Math.abs(dx) > scaledTouchSlop) {

                    donwx = tempx;
                    downy = tempy;
                    LockItem child = getChildByXY(tempx, tempy);
                    if (child != null) {
                        if (!selectLockViews.contains(child.getTag())) {
                            selectLockViews.add((Integer) child.getTag());
                            child.setMode(LockItem.Mode.PRESS);
                            pathCenterX = (child.getLeft() + child.getRight()) / 2;
                            pathCenterY = (child.getTop() + child.getBottom()) / 2;
                            if (selectLockViews.size() == 1) {
                                mlinePath.moveTo(pathCenterX, pathCenterY);
                            } else {
                                mlinePath.lineTo(pathCenterX, pathCenterY);
                            }
                            //箭頭直接直接指向某一個小圓的圓心
                            changeArraw(child.getLeft() + child.getCenterXY(), child.getTop() + child.getCenterXY());
                            lastView = child;
                            child.invalidate();
                        }
                    }
                }
                //箭頭隨著手指改變方向
                changeArraw(tempx, tempy);
                lastPointX = tempx;
                lastPointY = tempy;
                break;
            case MotionEvent.ACTION_UP:
                lastView = null;
                pathCenterX = lastPointX;
                pathCenterY = lastPointY;
                if (selectLockViews.size() == 0) return true;
                if (selectLockViews.size() < 4) {
                    Toast.makeText(getContext(), "密碼不能少於4個", Toast.LENGTH_LONG).show();
                    reset(tempx, tempy);
                    return true;
                }
                if (checkPwd()) {
                    Toast.makeText(getContext(), "親,密碼正確", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getContext(), "密碼錯誤", Toast.LENGTH_LONG).show();
                }
                changeMode();
                this.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        reset(tempx, tempy);
                    }
                }, 200);
                break;
        }
        invalidate();
        return true;
    }

    private void changeArraw(int x, int y) {
        if (lastView != null) {
            double v = getAngle(lastView.getLeft() + lastView.getCenterXY(), lastView.getTop() + lastView.getCenterXY(), x, y);
            lastView.setAngle((int) v);
            lastView.invalidate();
        }
    }

    /**
     * up的時候改變狀態,更新view,使用者可以根據自己的需要增加幾個狀態,如:密碼錯誤和密碼正確的顏色樣式
     */
    private void changeMode() {
        if (lockItems == null) return;
        for (int i = 0; i < lockItems.length; i++) {
            lockItems[i].setMode(LockItem.Mode.UP).invalidate();

            if (lockItems[i].getTag() == selectLockViews.get(selectLockViews.size() - 1)) {
                //去掉最後一個箭頭
                lockItems[i].setAngle(-1000).invalidate();
            }
        }
    }

    /**
     *檢查密碼是否正確
     * @return
     */
    private boolean checkPwd() {
        if (selectLockViews.size() != pwd.length) return false;
        for (int i = 0; i < pwd.length; i++) {
            if (selectLockViews.get(i) != pwd[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * 重置,將各種狀態復原。
     * @param x
     * @param y
     */
    private void reset(int x, int y) {
        selectLockViews.clear();
        mlinePath.reset();
        donwx = x;
        downy = y;
        pathCenterX = lastPointX = 0;
        pathCenterY = lastPointY = 0;
        for (int i = 0; i < lockItems.length; i++) {
            lockItems[i].setMode(LockItem.Mode.NOR).setAngle(-1000).invalidate();
        }
        invalidate();
    }

    /**
     * 通過手指的座標去檢查當前點在哪一個子view上面
     * @param x
     * @param y
     * @return
     */
    private LockItem getChildByXY(int x, int y) {
        if (lockItems == null) return null;
        for (int i = 0; i < lockItems.length; i++) {
            LockItem lockItem = lockItems[i];
            if (positionInView(lockItem, x, y)) {
                return lockItem;
            }
        }
        return null;
    }

    /**
     * 通過view的邊界檢查x。y是否在內部
     * @param lockItem
     * @param x
     * @param y
     * @return
     */
    private boolean positionInView(LockItem lockItem, int x, int y) {
        if (x > lockItem.getLeft() && x < lockItem.getRight() && y > lockItem.getTop() && y < lockItem.getBottom()) {
            return true;
        }
        return false;
    }

    /**
     * 這個就是根據兩個點座標計算角度,通知箭頭的方向。
     * @param px1
     * @param py1
     * @param px2
     * @param py2
     * @return
     */
    double getAngle(int px1, int py1, int px2, int py2) {
        //兩點的x、y值
        int x = px2 - px1;
        int y = py2 - py1;
        double hypotenuse = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        //斜邊長度
        double sin = x / hypotenuse;
        double radian = Math.asin(sin);
        //求出弧度
        double angle = 180 / (Math.PI / radian);
        //用弧度算出角度
        if (y > 0) {
            angle = 180 - angle;
        }
        return angle;
    }
}

這裡面也一樣遵循了自定義控制元件的三部曲,onMeasure,onLayout,draw。onLayout有很多種方式可以控制子view的位置,這裡採用的最簡單,最易懂的n*n的乘法口訣模式,裡面沒有太多的複雜邏輯,都是一些細節的東西,我覺得最麻煩的就是計算角度了,數學是體育老師教的,哎。。。

<?xml version="1.0" encoding="utf-8"?>
<com.example.apple.Custom.LockViewGroup 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"
    app:count="5"
    android:background="@android:color/darker_gray"
    android:layout_height="match_parent">

</com.example.apple.Custom.LockViewGroup>

使用很簡單的,拿來就直接可以用,我就不放demo工程了,裡面的程式碼貼上就能跑,喲。。又十二點了,洗洗睡了。。。