Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Hot Article Preview | Click Title to Read

Big news, Google finally takes action, moving closer to iOS? Android 9.0 will prohibit developers from using unofficial APIs

Incredible! Complete source code for 74 apps!

The truth learned after 10 years of work: I wish I had known earlier that positions matter more than now

Source: http://www.apkbus.com/blog-914188-76660.html

Introduction

Recently, project requirements: to add gesture password and fingerprint verification in the project. Coincidentally, I have been practicing custom views lately, so I referred to some online resources and my understanding to achieve the following effect.

International practice: Without a picture, you say a JB (trophy)

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

This GIF is really garbage, those who are interested can check the demo, I will post the link later.

1. Analyze the effect diagram:

All custom views are created by analyzing the effect diagram, breaking it down into modules, implementing each module, and finally assembling them into a whole. Let’s analyze the gesture password effect diagram below.

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

From the above image, we can analyze the View as follows:

(1) The total size of the View can be determined by the width of the large gesture View plus the height of the small gesture View. We can assign layout size through the onMeasure method using setMeasuredDimension(width, width + minHeight), where minHeight can be assigned based on actual conditions.

(2) The large gesture View can determine its square size by comparing width and height in the onMeasure method.

(3) The small gesture View is similar to the large gesture View, and the position of the prompt text is also easy to confirm.

(4) Once we have the width and height of the large gesture View, we can easily determine the coordinates and sizes of each gesture point.

(5) Everyone must have done operations to print various shapes using tags when learning Java. The gesture View is equivalent to a simple 3×3 matrix. Knowing the size and coordinates makes it easy to draw.

(6) The small View can also be implemented similarly. It should be noted that during the first registration of the gesture password, there is a small View, while there is none during authentication. We can set it to be hidden in onDraw based on the state.

2. After analyzing, let’s implement it step by step:

1. The template should be generic and customizable, so we need to define some attrs.

Based on the effect diagram analysis, I defined the attrs as follows. Here, I used images for the gesture points (images can make the gesture points cooler).

<declare-styleable name="SecurityCenter">
        <!-- Selected state of gesture point-->
        <attr name="selectedBitmap" format="reference"/>
        <!-- Unselected state of gesture point-->
        <attr name="unselectedBitmap" format="reference"/>
        <!-- Selected state of small gesture point-->
        <attr name="selectedBitmapSmall" format="reference"/>
        <!-- Unselected state of small gesture point-->
        <attr name="unselectedBitmapSmall" format="reference"/>
        <!-- Interception time for re-verification after failure-->
        <attr name="waitTime" format="integer"/>
        <!-- Maximum number of verification failures-->
        <attr name="maxFailCounts" format="integer"/>
        <!-- Minimum number of points to connect when drawing-->
        <attr name="minPoint" format="integer"/>
        <!-- Font color-->
        <attr name="paintColor" format="color"/>
        <!-- Font size-->
        <attr name="paintTextSize" format="dimension"/>
    </declare-styleable>

2. In the View, receive the assigned values. I used bitmap for the point images, and if no specific definition is provided, these attributes will all have default values. The code is as follows:

 public ChaosGestureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        
     super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.SecurityCenter);
        Drawable dw_selected = ta.getDrawable(R.styleable.SecurityCenter_selectedBitmap);
        Drawable dw_unSeclect = ta.getDrawable(R.styleable.SecurityCenter_unselectedBitmap);
        Drawable dw_selected_small = ta.getDrawable(R.styleable.SecurityCenter_selectedBitmapSmall);
        Drawable dw_unSeclect_small = ta.getDrawable(R.styleable.SecurityCenter_unselectedBitmapSmall);        
         if (dw_selected!=null){
            selectedBitmap = ((BitmapDrawable) dw_selected).getBitmap();
        }else{
            selectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected);
        }        
         if (dw_unSeclect!=null){
            unSelectedBitmap = ((BitmapDrawable) dw_unSeclect).getBitmap();
        }else{
            unSelectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected);
        }        
         if (dw_selected_small!=null){
            selectedBitmapSmall = ((BitmapDrawable) dw_selected_small).getBitmap();
        }else{
            selectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected_small);
        }        
         if (dw_unSeclect_small!=null){
            unSelectedBitmapSmall= ((BitmapDrawable) dw_unSeclect_small).getBitmap();
        }else{
            unSelectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected_new);
        }        
         //Waiting time, default 30s
        waitTime = ta.getInteger(R.styleable.SecurityCenter_waitTime,30);        
         //Attempt count, default 5
        tempCount = ta.getInteger(R.styleable.SecurityCenter_maxFailCounts,5);        
         //Minimum number of points set, default 4
        minPointNums = ta.getInteger(R.styleable.SecurityCenter_minPoint,4);        
        //Set the color of the paint
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);        
         //Color of the paint
        int color = ta.getColor(R.styleable.SecurityCenter_paintColor, context.getResources().getColor(R.color.black));
        mPaint.setColor(color);        
         //Font size
        float textsize = ta.getDimension(R.styleable.SecurityCenter_paintTextSize, 40);
        mPaint.setTextSize(textsize);        
         //Avoid errors when recreated
        ta.recycle();

        initView(context);
    }

3. In onMeasure, measure the layout size of the gesture View, which is easy to understand based on the initial analysis.

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);        
         int widthSize = MeasureSpec.getSize(widthMeasureSpec);        
         int widthMode = MeasureSpec.getMode(widthMeasureSpec);        
         int heightSize = MeasureSpec.getSize(heightMeasureSpec);        
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);        
         //width is the unit width of the large View
        int width = Math.min(widthSize, heightSize);        
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            width = heightSize;
        } else if (heightMode == MeasureSpec.UNSPECIFIED) {
            width = widthSize;
        }        
        //The height of the large gesture View is a square with side length of width, panelHeight is reserved for the small gesture View
        setMeasuredDimension(width, width + panelHeight);
    }

4. Through the onSizeChange method, we can obtain the size of the large gesture password points and small gesture password points based on the value of mLineHeight (3*1 row height), and then set the size of the gesture points through Bitmap.createScaledBitmap method. Details can be seen in the code comments below:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        
         super.onSizeChanged(w, h, oldw, oldh);
        mPanelWidth = Math.min(w, h);        
         //The width of the large gesture point is 0.6 times the unit width, making it look better and not too full
        pieceWidth = (int) (mLineHeight * 0.6f);        
         //The width of the small gesture point is similar
        pieceWidthSmall = (int) (mLineHeight * 0.15f);        
         //Draw the corresponding gesture point size
        selectedBitmap = Bitmap.createScaledBitmap(selectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
        unSelectedBitmap = Bitmap.createScaledBitmap(unSelectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
        selectedBitmapSmall = Bitmap.createScaledBitmap(selectedBitmapSmall, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
        unSelectedBitmapSmall = Bitmap.createScaledBitmap(unSelectedBitmapSmall, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
    }

5. We know that GestureView generally has two states: one is the registration state (which includes the small View), and the other is the authentication state (which does not include the small View). Therefore, we need to define two states to distinguish usage conditions.

 //Gesture initialization registration state
    public static final int STATE_REGISTER = 101;    
     //Gesture confirmation usage state
    public static final int STATE_LOGIN = 100;    
     //Set a parameter to record whether it is in the initialization stage or the usage stage, default is confirmation state
    private int stateFlag = STATE_LOGIN;

When we successfully register, we need to save the gesture state, which we store in SharedPreferences.

 //Save state after success
    private boolean saveState() {
        SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("state", stateFlag);        
        return edit.commit();
    }

Similarly, before initialization, we need to obtain the state to determine what state the current View belongs to, so that we can determine whether to draw the small View in onDraw.

 //Get the current View state from SP, default is initialization state
    private int getState() {
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);        
        return mSharedPreference.getInt("state", STATE_REGISTER);
    }

6. Based on the state, draw the gesture password points, connection lines, and prompt text.

(1) Draw 9 unselected large gesture View points using the canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint) method. Here we need to pay attention to the position calculation, as in the Android screen coordinate system, the upper left position is (0,0), and positive is to the right and down.

for (int i = 0; i < 3; i++) {            
    for (int j = 0; j < 3; j++) {
                canvas.drawBitmap(unSelectedBitmap, (float) (mLineHeight * (j + 0.5) - pieceWidth / 2), (float) (mLineHeight * (i + 0.5) - pieceWidth / 2 + panelHeight), mPaint);
            }
        }

It may sound abstract, but it is actually simple once you understand the coordinates. Draw a picture to assist your understanding; the small View is reserved for height (panelHeight):

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

(2) You may be curious about how the gesture connection line is stored and verified! Good question! I also thought about this for a long time. Some wheels use a two-dimensional array to achieve this, and through this two-dimensional array, I thought of using a Bean. I used a Bean to store the X and Y coordinates of the corresponding points and added each point instance to a List<>, thus completing the storage of all gesture drawing points.

The Bean code is as follows:

//Define Bean to store gesture coordinates
    public class GestureBean {        
        private int x;        
        private int y;        
        @Override
        public String toString() {            
            return "GestureBean{" +                    
                "x=" + x +                    
                ", y=" + y +                    
                '}';
        }        
        public GestureBean(int x, int y) {            
            this.x = x;            
            this.y = y;
        }        
        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;
        }        
        @Override
        public boolean equals(Object o) {            
            return ((GestureBean) o).getX() == x &amp;&amp; ((GestureBean) o).getY() == y;
        }
    }

(3) Draw connection lines and selected points: The connection lines are drawn through onTouchEvent and onDraw. In onTouchEvent, the points traversed by the finger will all be stored in the listDatas collection, and then through the invalidate(); method notify onDraw, based on the number of new points in listDatas, to draw the selected points and the connection lines between the points. Here is only the code in onDraw; the logic in onTouchEvent will be detailed later.

 //Used to judge the state
        GestureBean firstGestrue = null;
        GestureBean currGestrue = null;        
         if (!listDatas.isEmpty()) {

            firstGestrue = listDatas.get(0);           
             //Draw connection line
            for (int i = 1; i &lt; listDatas.size(); i++) {
                currGestrue = listDatas.get(i);
                canvas.drawLine((float) (mLineHeight * (firstGestrue.getX() + 0.5)), (float) (mLineHeight * (firstGestrue.getY() + 0.5) + panelHeight), (float) (mLineHeight * (currGestrue.getX() + 0.5)), (float) (mLineHeight * (currGestrue.getY() + 0.5) + panelHeight), mPaint);
                firstGestrue = currGestrue;
            }            
             //Last line
            lastGestrue = listDatas.get(listDatas.size() - 1);
            canvas.drawLine((float) (mLineHeight * (lastGestrue.getX() + 0.5)), (float) (mLineHeight * (lastGestrue.getY() + 0.5) + panelHeight), currX, currY, mPaint);            
             //Traverse the array and change the selected points' images
            for (GestureBean bean : listDatas) {
                canvas.drawBitmap(selectedBitmap, (float) (mLineHeight * (bean.getX() + 0.5) - pieceWidth / 2), (float) (mLineHeight * (bean.getY() + 0.5) + panelHeight - pieceWidth / 2), mPaint);
            }
        }

When the gesture is successfully registered, we need to save the gesture collection for the next verification, which we store in SharedPreference (note: this gesture View is only applicable for local interception, so storing in SharedPreferences is sufficient).

 //Store the x and y list into sp
    private boolean saveToSharedPrefference(List&lt;GestureBean&gt; data) {
        SharedPreferences sp = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();        
         //Store how many points
        edit.putInt("data_size", data.size()); /*sKey is an array*/
        //Coordinates of each point
        for (int i = 0; i &lt; data.size(); i++) {
            edit.remove("data_" + i);
            edit.putString("data_" + i, data.get(i).getX() + " " + data.get(i).getY());
        }        return edit.commit();
    }

To retrieve the stored collection: When we store and retrieve, we can first store the number of input points for easier judgment.

 //Read the previously saved List
    public List&lt;GestureBean&gt; loadSharedPrefferenceData() {
        List&lt;GestureBean&gt; list = new ArrayList&lt;&gt();
        SharedPreferences mSharedPreference = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);        
         //Get the number of points
        int size = mSharedPreference.getInt("data_size", 0);        
         //And the coordinates
        for (int i = 0; i &lt; size; i++) {
            String str = mSharedPreference.getString("data_" + i, "0 0");
            list.add(new GestureBean(Integer.parseInt(str.split(" ")[0]), Integer.parseInt(str.split(" ")[1])));
        }        return list;
    }

(4) Drawing the small image and text is straightforward. I will directly provide the code (I was interrupted during a meeting, and my thoughts were disrupted…).

 //If in initialization state
        if (stateFlag == STATE_REGISTER) {            
         //Draw the prompt point above  No prompt point needed
            drawTipsPoint(canvas);
        } else {            
         //Above is text, points are gone
            drawTipsText(canvas);
        }

It should be noted that the small View needs to retain the style of the first drawing when completing the first drawing. This is done by storing the comparison through a list, as shown in the following code.

 //Draw prompt points
    private void drawTipsPoint(Canvas canvas) {        
        //Width is half of the View width
        float widthMiddleX = mPanelWidth / 2;        
        //Determine the relevant coordinates and find the center point of the first point
        float firstX = widthMiddleX - pieceWidthSmall / 4 - pieceWidthSmall / 2 - pieceWidthSmall;        
        float firstY = panelHeight / 2 - pieceWidthSmall / 2 - pieceWidthSmall - pieceWidthSmall / 4 - 10;        
        //Draw points, as there are no selected points, draw 9 unselected points
        for (int i = 0; i &lt; 3; i++) {            
            for (int j = 0; j &lt; 3; j++) {
                canvas.drawBitmap(unSelectedBitmapSmall, (float) (firstX + j * (pieceWidthSmall * 1.25)), (float) (firstY + i * (pieceWidthSmall * 1.25)), mPaint);
            }
        }        
         //Before the second confirmation, the small gesture password shows the traces of the first drawing
        if (listDatasCopy != null &amp;&amp; !listDatasCopy.isEmpty()) {            
             for (GestureBean bean : listDatasCopy) {
                canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
            }
        }        
        //As the finger ActionMove changes the color of the selected point
        else if (listDatas != null &amp;&amp; !listDatas.isEmpty()) {            
             for (GestureBean bean : listDatas) {
                canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
            }
        }
        drawMessage(canvas, "Draw unlock pattern", mError);
    }

Effect diagram as follows:

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Drawing text is straightforward; just determine the general coordinates, and it’s easy to understand. I will directly provide the code:

    //Draw prompt text
    private void drawTipsText(Canvas canvas) {        
        float widthMiddleX = mPanelWidth / 2;
        mPaint.setStyle(Paint.Style.FILL);        
        int widthStr1 = (int) mPaint.measureText("Enter gesture to unlock");        
        float baseX = widthMiddleX - widthStr1 / 2;        
        float baseY = panelHeight / 2 + 50;
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();        
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;        
        float offY = fontTotalHeight / 2 - fontMetrics.bottom - 30;        
        float newY = baseY + offY;
        canvas.drawText("Enter gesture to unlock", baseX, newY, mPaint);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(10);
    }

7. Setting the gesture password definitely requires handling OnTouchEvent, and the logic here is key. I will analyze it in detail.

(1) Here we have encapsulated it quite well. I handled the case where verification exceeds the maximum attempts, which will intercept the gesture View. However, this may seem redundant for organizing ideas, so I will just paste the code for now; feel free to check the demo:

 if (mTimeOut) {            
     switch (event.getAction()) {                
         case MotionEvent.ACTION_DOWN:                    
             break;                
         case MotionEvent.ACTION_MOVE:                    
             break;                
         case MotionEvent.ACTION_UP:                    
             if (0 &lt; leftTime &amp;&amp; leftTime &lt;= 30) {
                        AlertUtil.t(mContext, "Maximum number of attempts reached, retry in " + leftTime + "s");
                    }                    
                     return true;
            }
        }

(2) First, we need to determine whether our OnTouch event is within the range of the large View. Since the coordinates are clearly specified from the start, this is easy to judge.

   if (event.getY() &gt;= ((mLineHeight * (0 + 0.5) - pieceWidth / 2 + panelHeight))){  
       //Get XY to determine which point the finger is at
            int x = (int) ((event.getY() - panelHeight) / mLineHeight);            
            int y = (int) (event.getX() / mLineHeight);    
                    
            //Current finger coordinates
            currX = event.getX();
            currY = event.getY();
}

(3) MotionEvent.ACTION_DOWN: When the finger is pressed down, we need to determine which large gesture point the pressed point falls within and add it to the List<bean>. Notify onDraw to redraw, as described earlier, to show that the first point is selected.

case MotionEvent.ACTION_DOWN:
                    lastGestrue = null;                    
                    if (currX &gt;= 0 &amp;&amp; currX &lt;= mPanelWidth &amp;&amp; currY &gt;= panelHeight &amp;&amp; currY &lt;= panelHeight + mPanelWidth) {                        
                    if (currY &lt;= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight &amp;&amp; currY &gt;= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &amp;&amp;
                                currX &lt;= (y + 0.5) * mLineHeight + pieceWidth / 2 &amp;&amp; currX &gt;= (y + 0.5) * mLineHeight - pieceWidth / 2) {                            
                            //Determine which point the current finger is in range of. If the point does not exist in listData, store it as the first point
                            if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
                            }
                        }
                    }                    
                    //Redraw once, the first point shows as selected
                    invalidate();                    
                    break;

(4) MotionEvent.ACTION_MOVE: When the finger slides on the View, it adds the coordinates of the point it slides to the List<bean>. Then notify to redraw.

 case MotionEvent.ACTION_MOVE:                    
             //Finger moves within the range of the large View
              if (currX &gt;= 0 &amp;&amp; currX &lt;= mPanelWidth &amp;&amp; currY &gt;= panelHeight &amp;&amp; currY &lt;= panelHeight + mPanelWidth) {                        
                    //Reduce the response range. Here it should be noted that x and currX are physically opposite
                    if (currY &lt;= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight &amp;&amp; currY &gt;= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &amp;&amp;
                                currX &lt;= (y + 0.5) * mLineHeight + pieceWidth / 2 &amp;&amp; currX &gt;= (y + 0.5) * mLineHeight - pieceWidth / 2) {                            
                          //If the point is not in listData, store it
                          if (!listDatas.contains(new GestureBean(y, x))) {
                                listDatas.add(new GestureBean(y, x));
                          }
                    }
               }                    
               //Redraw
               invalidate();                    
               break;

(5) MotionEvent.ACTION_UP: There are two situations: 1. In authentication state, it will compare with the previously retrieved listdatas from loadSharedPrefferenceData to determine if it is successful. 2. In registration state, it will compare the first listdatas to determine if the two verifications are consistent, thus handling the success and failure logic.

 case MotionEvent.ACTION_UP:                    
         if (lastGestrue != null) {
                        currX = (float) ((lastGestrue.getX() + 0.5) * mLineHeight);
                        currY = (float) ((lastGestrue.getY() + 0.5) * mLineHeight);
                    }                    
                    //If the View is in authentication state
                    if (stateFlag == STATE_LOGIN) {                        
                        //If they are the same, authentication is successful
                        if (listDatas.equals(loadSharedPrefferenceData())) {
                            mError = false;
                            postListener(true);
                            invalidate();
                            listDatas.clear();                            
                            return true;
                        } else {                            
                     if (--tempCount == 0) {//Maximum attempts reached
                                mError = true;
                                mTimeOut = true;
                                listDatas.clear();
                                Date date = new Date();
                                PreferenceCache.putGestureTime(date.getTime());
                                mTimerTask = new InnerTimerTask(handler);
                                mTimer.schedule(mTimerTask, 0, 1000);
                                invalidate();                                
                                return true;
                            }
                            mError = true;
                            AlertUtil.t(mContext, "Gesture error, you can input " + tempCount + " more times");
                            listDatas.clear();
                        }

                    }                    
                    //If the View is in registration state
                    else if (stateFlag == STATE_REGISTER) {                        
                        //First authentication state
                        if (listDatasCopy == null || listDatasCopy.isEmpty()) {                            
                             if (listDatas.size() &lt; minPointNums) {
                                listDatas.clear();
                                mError = true;
                                AlertUtil.t(mContext, "The number of points cannot be less than " + minPointNums + " points");
                                invalidate();                                
                                return true;
                            }
                            listDatasCopy.addAll(listDatas);
                            listDatas.clear();
                            mError = false;
                            AlertUtil.t(mContext, "Please draw again");
                        } else {                            
                            //If the two authentications are successful
                            if (listDatas.equals(listDatasCopy)) {
                                saveToSharedPrefference(listDatas);
                                mError = false;
                                stateFlag = STATE_LOGIN;
                                postListener(true);
                                saveState();
                            } else {
                                mError = true;
                                AlertUtil.t(mContext, "Inconsistency with the previous gesture drawing, please reset");
                            }
                            listDatas.clear();
                            invalidate();                            
                            return true;
                        }
                    }
                    invalidate();                    
                    break;

To this end, the logic of the gesture View has been roughly clarified, and the next step is to improve it.

3. Improve the View, set interface calls, failure countdown, and clear current SP cache when closing the View.

1. Observant friends will notice that in ACTION.UP there are postListener(true) and similar calls. This is the interface I defined to return the authentication status. (1) Define the interface, with three parameters: the state of the view, the stored List, and whether it was successful (registration or authentication).

 //Define the interface to pass the View state
    public interface GestureCallBack{        
         void gestureVerifySuccessListener(int stateFlag, List&lt;GestureBean&gt; data, boolean success);
    }

(2) Instantiate the interface; the current Activity must implement the interface.

 //The current Activity must implement the View interface
 try {
      estureCallBack = (GestureCallBack) mContext;
} catch (final ClassCastException e) {            
      throw new ClassCastException(mContext.toString() + " must implement GestureCallBack");
}

(3) Pass the interface real-time data in Action.UP.

  //Pass data to the interface
    private void postListener(boolean success) {        
         if (gestureCallBack != null) {
            gestureCallBack.gestureVerifySuccessListener(stateFlag, listDatas, success);
        }
    }

2. The failure countdown is implemented through Handler, Timer, and TimerTask (1) Define an inner TimerTask class to record the error countdown.

 //Define an inner TimerTask class for recording the error countdown
    static class InnerTimerTask extends TimerTask{
        Handler handler;     
        public InnerTimerTask(Handler handler) {            
         this.handler = handler;
        }        
        @Override
        public void run() {
            handler.sendMessage(handler.obtainMessage());
        }
    }

(2) Instantiate; the last error time is also stored in SharedPreference, waitTime is the defined timeout period. The code is as follows:

 mTimer = new Timer();        
         //Calculate the difference between the last failure time and the current time
        try {            
            long lastTime = PreferenceCache.getGestureTime();
            Date date = new Date();            
            if (lastTime !=0 &amp;&amp; (date.getTime()-lastTime)/1000&lt;waitTime){                
                //Failure time not reached, still in locked state
                mTimeOut = true;
                leftTime = (int)(waitTime-((date.getTime()-lastTime))/1000);
                mTimerTask = new InnerTimerTask(handler);
                mTimer.schedule(mTimerTask,0,1000);
            }else{
                mTimeOut = false;
                leftTime = waitTime;
            }

        }catch (RuntimeException e){
            e.printStackTrace();
        }

(3) Handler processes messages:

 //Receive TimerTask messages to notify UI
    private Handler handler = new Handler(){        
         @Override
        public void handleMessage(Message msg) {
           leftTime--;           
           if (leftTime == 0){               
                if (mTimer != null)
                   mTimerTask.cancel();
               mTimeOut = false;
               AlertUtil.t(mContext,"Please draw the unlock pattern");
               mError = false;
               invalidate();               
               //Restore timing information
               reSet();               
               return;
           }
           mError = true;
           invalidate();
        }
    };

3. Clear the gesture View cache for closing the View or changing the password.

    //Clear previously saved state for closing the View
    public boolean clearCache() {
        SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("state", STATE_REGISTER);
        stateFlag = STATE_REGISTER;
        invalidate();        return edit.commit();
    }    
    //To change the gesture password, clear the previous password
    public boolean clearCacheLogin() {
        SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
        SharedPreferences.Editor edit = sp.edit();
        edit.putInt("state", STATE_LOGIN);
        stateFlag = STATE_LOGIN;
        invalidate();        
         return edit.commit();
    }
         

4. Simple usage:

1. Taking setting the gesture password as an example: (1) XML layout

&lt;LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.chaos.chaossecuritycenter.activity.SettingPatternPswActivity"&gt;
    &lt;RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"&gt;
        &lt;TextView
            android:id="@+id/tv_setting_back"
            android:layout_marginLeft="10dp"
            android:textSize="16sp"
            android:drawableLeft="@mipmap/back"
            android:textColor="@color/bak_blue"
            android:gravity="center_vertical"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="Back"/&gt;
        &lt;TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Set Gesture Password"
            android:textColor="@color/black"
            android:textSize="20sp" /&gt;
    &lt;/RelativeLayout&gt;

    &lt;com.chaos.chaossecuritycenter.weight.ChaosGestureView
        android:id="@+id/gesture"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:layout_marginTop="40dp"
        android:layout_weight="1"
        app:selectedBitmap="@mipmap/icon_finger_selected"
        app:unselectedBitmap="@mipmap/icon_finger_unselected"
        app:selectedBitmapSmall="@mipmap/icon_finger_selected_small"
        app:unselectedBitmapSmall="@mipmap/icon_finger_unselected_new"
        app:waitTime="30"
        app:maxFailCounts="5"
        app:minPoint="4"
        app:paintColor="@color/bak_blue"
        app:paintTextSize="15sp"
       /&gt;
&lt;/LinearLayout&gt;

Layout preview:

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Java code (setting gesture password page):

public class SettingPatternPswActivity extends AppCompatActivity implements ChaosGestureView.GestureCallBack{    
    private TextView tv_back;    
    private ChaosGestureView gestureView;    
    private int jumpFlg;    
    private int flag;    
    @Override
    protected void onCreate(Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_setting_pattern_psw);
        jumpFlg = getIntent().getIntExtra("jumpFlg", 0);
        flag = getIntent().getIntExtra("flag", 0);
        initView();
    }    
    private void initView() {
        tv_back = (TextView) findViewById(R.id.tv_setting_back);
        gestureView = (ChaosGestureView) findViewById(R.id.gesture);
        gestureView.setGestureCallBack(this);        
        //Not calling this method will cause the program to directly enter gesture recognition instead of gesture setting on the second launch
        gestureView.clearCache();
        tv_back.setOnClickListener(new View.OnClickListener() {            
            @Override
            public void onClick(View view) {
                finish();
            }
        });
    }    
    @Override
    public void gestureVerifySuccessListener(int stateFlag, List&lt;ChaosGestureView.GestureBean&gt; data, boolean success) {        
        if (stateFlag == GestureView.STATE_LOGIN) {
            PreferenceCache.putGestureFlag(true);
            finish();
        }
    }    
    @Override
    public void onPointerCaptureChanged(boolean hasCapture) {

    }
}

5. Summary

This custom gesture password is designed based on some wheels and my understanding, mimicking Alipay’s gesture password design. Overall, I have analyzed it for you. This View still has many areas for improvement, and I will gradually improve it. If you have any advice or questions, please leave a comment below.

Demo:

1. Custom View for gesture password 2. Fingerprint verification (due to the security of Alipay’s imitation, I did it together with fingerprint using third-party)

Demo link: https://github.com/ChaosOctopus/ChaosSecurityCenter

If you find it useful, please give me a thumbs up or a star.

If you have good articles to share, feel free to submit them by sending me the article link directly.

Java and Android architecture

Scan the QR code below or click to receive advanced resources for Android, Python, AI, Java, etc.

After following, reply “Baidu”, “Ali”, “Tencent”, or “Resources” for surprises

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Public account:JANiubility

Welcome to join our Java and Android architecture circle, with nearly 1000 people joining for learning and communication. More learning resources are updated, and more exchanges lead to progress.

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Click below “Read the original text” to get more learning materials.

Advanced Android View: Implementing a Custom Gesture Password Similar to Alipay

Leave a Comment

×