1. 程式人生 > >Android手勢密碼view筆記(一)

Android手勢密碼view筆記(一)

前言:不知不覺已經在這座陌生又熟悉的城市呆了一年多了,說不出什麼感覺,可是即使是自己感覺自己沒什麼變化,但是周圍的事物卻不斷的在變,不知道自己選擇的路未來如何,但是當下我還是會努力、努力、再努力的,加油,騷年!~說了一堆廢話,哈哈~! 好了,下面進入今天的主題。

剛接觸android的時候看到別人寫的手勢密碼view,然後當時就在想,我什麼時候才能寫出如此高階的東西?? 沒關係,不要怕哈,說出這樣話的人不是你技術不咋地而是你不願意花時間去研究它,其實也沒有那麼難哦(世上無難事,只怕有心人!),下面我們就一步一步實現一個手勢密碼view。

想必都看過手勢密碼view,但我們還是看看我們今天要實現的效果吧:

這裡寫圖片描述

上面是一個手勢view的提示view,下面是一個手勢view。

用法:

 <com.leo.library.view.GestureContentView
            android:id="@+id/id_gesture_pwd"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="10dp"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
app:column="3" app:row="3" app:padding="50dp" app:normalDrawable="@drawable/gesture_node_normal" app:selectedDrawable="@drawable/gesture_node_pressed" app:erroDrawable="@drawable/gesture_node_wrong" app:normalStrokeColor="#000"
app:erroStrokeColor="#ff0000" app:strokeWidth="4dp" />

app打頭的是自定義的一些屬性,

attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="IndicatorView">
        <!--預設狀態的drawable-->
        <attr name="normalDrawable" format="reference" />
        <!--被選中狀態的drawable-->
        <attr name="selectedDrawable" format="reference" />
        <!--錯誤狀態的drawabe-->
        <attr name="erroDrawable" format="reference" />
        <!--列數-->
        <attr name="column" format="integer" />
        <!--行數-->
        <attr name="row" format="integer" />
        <!--padding值,padding值越大點越小-->
        <attr name="padding" format="dimension" />
        <!--預設連線線顏色-->
        <attr name="normalStrokeColor" format="color" />
        <!--錯誤連線線顏色-->
        <attr name="erroStrokeColor" format="color" />
        <!--連線線size-->
        <attr name="strokeWidth" format="dimension" />
    </declare-styleable>
</resources>

MainActivity.java:

public class MainActivity extends AppCompatActivity implements IGesturePwdCallBack {
    private GestureContentView mGestureView;
    private IndicatorView indicatorView;
    private TextView tvIndicator;

    private int count=0;
    private String pwd;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGestureView= (GestureContentView) findViewById(R.id.id_gesture_pwd);
        indicatorView= (IndicatorView) findViewById(R.id.id_indicator_view);
        tvIndicator= (TextView) findViewById(R.id.id_indicator);
        mGestureView.setGesturePwdCallBack(this);
    }

    @Override
    public void callBack(List<Integer> pwds) {
        StringBuffer sbPwd=new StringBuffer();
        for (Integer pwd:pwds) {
            sbPwd.append(pwd);
        }
        tvIndicator.setText(sbPwd.toString());
        if(pwds!=null&&pwds.size()>0){
            indicatorView.setPwds(pwds);
        }
      if(count++==0){
            pwd=sbPwd.toString();
            Toast.makeText(this,"請再次繪製手勢密碼",Toast.LENGTH_SHORT).show();
            mGestureView.changePwdState(PointState.POINT_STATE_NORMAL,0);
        } else{
            count=0;
            if(pwd.equals(sbPwd.toString())){
                Toast.makeText(this,"密碼設定成功",Toast.LENGTH_SHORT).show();
            }else{
                Toast.makeText(this,"兩次密碼不一致,請重新繪製",Toast.LENGTH_SHORT).show();
                indicatorView.startAnimation(AnimationUtils.loadAnimation(this,R.anim.anim_shake));
                count=0;
                mGestureView.changePwdState(PointState.POINT_STATE_ERRO,0);
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mGestureView.changePwdState(PointState.POINT_STATE_NORMAL,0);
                    }
                },1000);
            }
        }
    }
}

看不懂也沒關係啊,我們先明確下我們要完成的目標,然後一步一步實現:

先實現下我們的指示器view,因為實現了指示器view也就相當於實現了一半的手勢密碼view了:
這裡寫圖片描述

實現思路:
1、我們需要知道指示器有多少行、多少列、預設顯示什麼、選中後顯示什麼?
2、然後根據傳入的密碼把對應的點顯示成選中狀態,沒有選中的點為預設狀態。

好了,知道我們的思路,首先自定義一個view叫IndicatorView繼承view,然後重寫三個構造方法:

public class IndicatorView extends View {
    public IndicatorView(Context context) {
        this(context, null);
    }

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

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

定義自定義屬性(在res/values下建立attrs.xml檔案):

1、我們需要傳入的預設顯示圖片:

 <!--預設狀態的drawable-->
        <attr name="normalDrawable" format="reference" />

2、我們需要拿到傳入的選中時圖片:

<!--被選中狀態的drawable-->
        <attr name="selectedDrawable" format="reference" />

其它的一些屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="IndicatorView">
        <!--預設狀態的drawable-->
        <attr name="normalDrawable" format="reference" />
        <!--被選中狀態的drawable-->
        <attr name="selectedDrawable" format="reference" />
        <!--列數-->
        <attr name="column" format="integer" />
        <!--行數-->
        <attr name="row" format="integer" />
            </declare-styleable>
</resources>

定義完屬性後,此時我們xml中就可以引用自定義view了:

 <com.leo.library.view.IndicatorView
        android:id="@+id/id_indicator_view"
        android:layout_marginTop="20dp"
        android:layout_width="85dp"
        android:layout_height="85dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        app:column="3"
        app:normalDrawable="@drawable/shape_white_indicator"
        app:padding="8dp"
        app:row="3"
        app:selectedDrawable="@drawable/shape_orange_indicator" />

注意:
中間的drawable檔案可以在github專案中找到,連結我會在文章最後給出。

有了自定義屬性,然後我們在帶三個引數的構造方法中獲取我們在佈局檔案傳入的自定義屬性:

private static final int NUMBER_ROW = 3;
    private static final int NUMBER_COLUMN = 3;
    private int DEFAULT_PADDING = dp2px(10);
    private final int DEFAULT_SIZE = dp2px(40);

    private Bitmap mNormalBitmap;
    private Bitmap mSelectedBitmap;
    private int mRow = NUMBER_ROW;
    private int mColumn = NUMBER_COLUMN;
 public IndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.IndicatorView, defStyleAttr, 0);
        mNormalBitmap = drawableToBitmap(a.getDrawable(R.styleable.IndicatorView_normalDrawable));
        mSelectedBitmap = drawableToBitmap(a.getDrawable(R.styleable.IndicatorView_selectedDrawable));
        if (a.hasValue(R.styleable.IndicatorView_row)) {
            mRow = a.getInt(R.styleable.IndicatorView_row, NUMBER_ROW);
        }
        if (a.hasValue(R.styleable.IndicatorView_column)) {
            mColumn = a.getInt(R.styleable.IndicatorView_row, NUMBER_COLUMN);
        }
        if (a.hasValue(R.styleable.IndicatorView_padding)) {
            DEFAULT_PADDING = a.getDimensionPixelSize(R.styleable.IndicatorView_padding, DEFAULT_PADDING);
        }
    }

好了,現在我們已經拿到了我們想要的東西了,接下來我們需要知道我的view要多大,相比小夥伴都知道接下來要幹什麼了吧?對~! 我們需要重寫下onMeasure方法,然後指定我們view的大小:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    }

那麼我們該以一個什麼樣的規則指定我們的view的大小呢?

1、當用戶自己指定了view的大小的話,我們就用使用者傳入的size,然後根據傳入的寬、高計算出我們的點的大小。

<com.leo.library.view.IndicatorView
        android:id="@+id/id_indicator_view"
        android:layout_marginTop="20dp"
        android:layout_width="85dp"
        android:layout_height="85dp"

2、如果使用者沒有指定view的大小,寬高都設定為wrap_content的話,我們需要根據使用者傳入的選中圖片跟沒選中圖片的大小計算view的大小:

 android:layout_width="wrap_content"
        android:layout_height="wrap_content"

好了,既然知道咋測量我們的view後,我們接下來就實現出來:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        float width = MeasureSpec.getSize(widthMeasureSpec);
        float height = MeasureSpec.getSize(heightMeasureSpec);
        float result=Math.min(width,height);
        height = getHeightValue(result, heightMode);
        width = getWidthValue(result, widthMode);
       }
 private float getHeightValue(float height, int heightMode) {
        //當size為確定的大小的話
        //每個點的高度等於(控制元件的高度-(行數+1)*padding值)/行數
        if (heightMode == MeasureSpec.EXACTLY) {
            mCellHeight = (height - (mRow + 1) * DEFAULT_PADDING) / mRow;
        } else {
            //高度不確定的話,我們就取選中的圖片跟未選中圖片中的高度的最小值
            mCellHeight = Math.min(mNormalBitmap.getHeight(), mSelectedBitmap.getHeight());
            //此時控制元件的高度=點的高度*行數+(行數+1)*預設padding值
            height = mCellHeight * mRow + (mRow + 1) * DEFAULT_PADDING;
        }
        return height;
    }

寬度計算方式也是一樣的話,只是行數換成了列數:

 private float getWidthValue(float width, int widthMode) {
        if (widthMode == MeasureSpec.EXACTLY) {
            mCellWidth = (width - (mColumn + 1) * DEFAULT_PADDING) / mColumn;
        } else {
            mCellWidth = Math.min(mNormalBitmap.getWidth(), mSelectedBitmap.getWidth());
            width = mCellWidth * mColumn + (mColumn + 1) * DEFAULT_PADDING;
        }
        return width;
    }

好了,現在是知道了點的高度跟寬度,然後控制元件的寬高自然也就知道了,但是如果我們傳入的選中的圖片跟未選擇的圖片大小不一樣咋辦呢?沒關係,接下來我們重新修改下圖片的size:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       .....
        height = getHeightValue(result, heightMode);
        width = getWidthValue(result, widthMode);
        setMeasuredDimension((int) width, (int) height);
        //重新修改圖片的size
        resizeBitmap(mCellWidth, mCellHeight);
    }

    private void resizeBitmap(float width, float height) {
        if (width > 0 && height > 0) {
            if (mNormalBitmap.getWidth() != width || mNormalBitmap.getHeight() !=height) {
                if (mNormalBitmap.getWidth() > 0 && mNormalBitmap.getHeight() > 0) {
                    mNormalBitmap = Bitmap.createScaledBitmap(mNormalBitmap, (int) width, (int) height, false);
                }
            }
            if (mSelectedBitmap.getWidth()!=width || mSelectedBitmap.getHeight() !=height) {
                if (mSelectedBitmap.getWidth() > 0 && mSelectedBitmap.getHeight() > 0) {
                    mSelectedBitmap = Bitmap.createScaledBitmap(mSelectedBitmap, (int) width, (int) height, false);
                }
            }
        }
    }

好了,圖片也拿到了,控制元件的寬高跟點的寬高都知道,所以接下來我們該進入我們的核心程式碼了(重寫onDraw方法,畫出我們的點):

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //遍歷行數
        for (int i = 0; i < mRow; i++) {
            //遍歷列數
            for (int j = 0; j < mColumn; j++) {
                float left = (j + 1) * DEFAULT_PADDING + j * mCellWidth;
                float top = (i + 1) * DEFAULT_PADDING + i * mCellHeight;
                //每個點代表的密碼值=點對應的行數值*列數+對應的列數
                //比如3*3的表格,然後第二排的第一個=1*3+0=3
                int num=i * mColumn + j;
                //此點是不是在傳入的密碼集合中?
                if (pwds!=null&&pwds.contains(num)) {
                    //這個點在傳入的密碼集合中的話就畫一個選中的bitmap
                    canvas.drawBitmap(mSelectedBitmap, left, top, null);
                } else {
                    canvas.drawBitmap(mNormalBitmap, left, top, null);
                }
            }
        }
    }

嗯嗯!!然後我們暴露一個方法,讓外界傳入需要現實的密碼集合:

 public void setPwds(List<Integer> pwds) {
        if(pwds!=null)this.pwds=pwds;
        if (Looper.myLooper() == Looper.getMainLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

好啦~~ 我們的指示器view就做完啦~~~ 是不是很簡單呢? 指示器view做完後,再想想手勢密碼view,是不是就只是差根據手勢改變,然後畫出我們的line呢?