Android之圖案鎖:

參考了:http://blog.csdn.net/liusiqian0209/article/details/50372448並在此基礎上做了進一步的完善

如下圖:


首先我們考慮在哪裏完成點和線的繪圖

通常我們想到的是寫一個自定義的View(即繼承自View類),添加onTouchEvent進行控制,同時覆寫onDraw()方法,完成繪製。不過我這裏沒有采用這種方式,考慮到onTouchEvent只能接收在View之上的觸摸事件,從上面第一張圖中可以看出,如果文字和自定義View平鋪擺放的話,那麼當手指滑動到文字上面的時候,已經超出了自定義View的範圍,因此無法響應觸摸事件。雖說有一種補救方式,就是讓其他控件和自定義View疊在一起,即擺放在一個FrameLayout裏面,不過幀佈局對控件位置的控制不像RelativeLayout這樣靈活,因此我的實現方式是自定義RelativeLayout,並且在dispatchDraw()方法裏,完成點和線的繪製。dispatchDraw()會在佈局繪製子控件時調用,具體的可以參考谷歌官方文檔。 
  首先需要有一個類來記錄九個圓點的基本信息。我們可以視爲這九個圓是分佈於3*3的方格子裏面,其中每一個圓位於方格子的中心,在繪製這些圓時,有以下基本信息是要知道的: 
1、這些方格子的位置(左上角的X,Y座標) 
2、方格子的邊長有多大? 
3、方格子的邊到圓的邊有多大的間隔? 
4、圓心的位置(圓心X,Y座標) 
5、圓的半徑是多少? 
6、這個圓當前應該顯示什麼顏色?(即圓點的狀態) 
7、由於我們不可能記錄圖案整體,而是記錄連接點的順序,那麼這個圓所表示的密碼值是多少? 
  不過上面這7個值是相互依賴的,比如我知道了1和2,就能知道4;知道了2和3,就能知道5。因此,在定義這些值的時候,應當讓用戶提供充分但不衝突的信息(比如我這裏從外部獲取的是1、2、3、6、7,而4和5是算出來的)。我在實現的時候,把定義下來就再也用不到的信息寫在了一個類裏面,把繪製點時還需要獲取的信息寫在了另一個類裏面,並且這個類提供了一些外部調用的方法(實際上這兩個類合二爲一是完全合理的),代碼如下


package xiangcuntiandi.mylock1;

/**
 * Créé par liusiqian 15/12/17.
 */
public class PatternPoint extends PatternPointBase
{
    protected static final int MIN_SIDE = 20;        //最小邊長
    protected static final int MIN_PADDING = 4;        //最小間隔
    protected static final int MIN_RADIUS = 6;        //最小半徑

    protected int left, top, side, padding;     //side:邊長

    public PatternPoint(int left, int top, int side, int padding, String tag)
    {
        this.left = left;
        this.top = top;
        this.tag = tag;

        if (side < MIN_SIDE)
        {
            side = MIN_SIDE;
        }
        this.side = side;

        if (padding < MIN_PADDING)
        {
            padding = MIN_PADDING;
        }

        radius = side / 2 - padding;
        if (radius < MIN_RADIUS)
        {
            radius = MIN_RADIUS;
            padding = side / 2 - radius;
        }
        this.padding = padding;
        centerX = left + side / 2;
        centerY = top + side / 2;
        status = STATE_NORMAL;
    }
}
package xiangcuntiandi.mylock1;

/**
 * Créé par liusiqian 15/12/18.
 */
public abstract class PatternPointBase
{
    protected int centerX;     //圓心X
    protected int centerY;     //圓心Y
    protected int radius;      //半徑
    protected String tag;      //密碼標籤

    public int status;         //狀態

    public static final int STATE_NORMAL = 0;       //正常
    public static final int STATE_SELECTED = 1;     //選中
    public static final int STATE_ERROR = 2;        //錯誤

    public int getCenterX()
    {
        return centerX;
    }

    public int getCenterY()
    {
        return centerY;
    }

    public boolean isPointArea(double x, double y)
    {
        double len = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
        return radius > len;
    }

    public String getTag()
    {
        return tag;
    }

    public int getRadius()
    {
        return radius;
    }
}


 可以看到,在基類裏面定義了圓點的狀態常量。此外還提供了一個方法叫做isPointArea(),這個方法用於判斷對於給定的一個點,它是否在這個圓之內。我們在進行連線時,如果經過了一個點,則需要把它連接起來,這時需要用到這個函數。 
  接下來是這個擴展的RelativeLayout,這裏先給出整個類的代碼,然後再逐步解釋。

package xiangcuntiandi.mylock1;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

import java.util.ArrayList;

/**
 * Créé par liusiqian 15/12/17.
 */
public class PatternLockLayout extends RelativeLayout
{
    public PatternLockLayout(Context context)
    {
        super(context);
    }

    public PatternLockLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

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

    private boolean hasinit;                //初始化是否完成
    private PatternPoint[] points = new PatternPoint[9];        //九個圓圈對象
    private int width, height, side;                        //佈局可用寬,佈局可用高,小方格子的邊長
    private int sidePadding, topBottomPadding;      //側邊和上下邊預留空間

    private boolean startLine;      //是否開始連線
    private boolean errorMode;      //連線是否使用表示錯誤的顏色
    private boolean drawEnd;        //是否已經擡手
    private boolean resetFinished;  //重置是否已經完成(是否可以進行下一次連線)
    private float moveX, moveY;     //手指位置
    private ArrayList<PatternPoint> selectedPoints = new ArrayList<>();     //所有已經選中的點

    private static final int PAINT_COLOR_NORMAL = 0xffcccccc;
    private static final int PAINT_COLOR_SELECTED = 0xff00dd00;
    private static final int PAINT_COLOR_ERROR = 0xffdd0000;

    private Handler mHandler;

    @Override
    protected void dispatchDraw(Canvas canvas)
    {
        super.dispatchDraw(canvas);
        if (!hasinit)
        {
            //暫時寫死,後面通過XML設置
            sidePadding = 40;
            topBottomPadding = 40;
            initPoints();
            resetFinished = true;
        }

        this.paint1 = new Paint();
        this.paint1.setAntiAlias(true); //消除鋸齒
        this.paint1.setStyle(Paint.Style.STROKE);
        drawCircle(canvas);
        drawLine(canvas);
    }

    Paint paint1;
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        moveX = event.getX();
        moveY = event.getY();

        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
            {
                int index = whichPointArea();
                if (-1 != index && resetFinished)
                {
                    addSelectedPoint(index);
                    startLine = true;
                }
            }
            break;
            case MotionEvent.ACTION_MOVE:
            {
                if (startLine && resetFinished)
                {
                    int index = whichPointArea();
                    if (-1 != index && points[index].status == PatternPointBase.STATE_NORMAL)
                    {
                        //查看是否有中間插入點
                        insertPointIfNeeds(index);
                        //增加此點到隊列中
                        addSelectedPoint(index);
                    }
                }
            }
            break;
            case MotionEvent.ACTION_UP:
            {
                if (startLine && resetFinished)
                {
                    resetFinished = false;
                    int delay = processFinish();
                    mHandler.postDelayed(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            reset();
                        }
                    }, delay);
                }
            }
            break;
        }

        invalidate();

        return true;
    }


    public void setAllSelectedPointsError()
    {
        errorMode = true;
        for (PatternPoint point : selectedPoints)
        {
            point.status = PatternPointBase.STATE_ERROR;
        }
        invalidate();
    }

    private void reset()
    {
        for (PatternPoint point : points)
        {
            point.status = PatternPointBase.STATE_NORMAL;
        }
        selectedPoints.clear();
        startLine = false;
        errorMode = false;
        drawEnd = false;
        if (listener != null)
        {
            listener.onReset();
        }
        resetFinished = true;
        invalidate();
    }

    //返回值爲reset延遲的毫秒數
    private int processFinish()
    {
        drawEnd = true;
        if (selectedPoints.size() < 2)
        {
            return 0;
        }
        else            //長度過短、密碼錯誤的判斷留給外面
        {
            int size = selectedPoints.size();
            StringBuilder sbPassword = new StringBuilder();
            for (int i = 0; i < size; i++)
            {
                sbPassword.append(selectedPoints.get(i).tag);
            }
            if (listener != null)
            {
                listener.onFinish(sbPassword.toString(), size);
            }
            return 1000;
        }
    }

    public interface OnPatternStateListener
    {
        void onFinish(String password, int sizeOfPoints);

        void onReset();
    }

    private OnPatternStateListener listener;

    public void setOnPatternStateListener(OnPatternStateListener listener)
    {
        this.listener = listener;
    }

    private void insertPointIfNeeds(int curIndex)
    {
        final int[][] middleNumMatrix = new int[][]{{-1, -1, 1, -1, -1, -1, 3, -1, 4}, {-1, -1, -1, -1, -1, -1, -1, 4, -1}, {1, -1, -1, -1, -1, -1, 4, -1, 5}, {-1, -1, -1, -1, -1, 4, -1, -1, -1}, {-1, -1, -1, -1, -1, -1, -1, -1, -1}, {-1, -1, -1, 4, -1, -1, -1, -1, -1}, {3, -1, 4, -1, -1, -1, -1, -1, 7}, {-1, 4, -1, -1, -1, -1, -1, -1, -1}, {4, -1, 5, -1, -1, -1, 7, -1, -1}};

        int selectedSize = selectedPoints.size();
        if (selectedSize > 0)
        {
            int lastIndex = Integer.parseInt(selectedPoints.get(selectedSize - 1).tag) - 1;
            int middleIndex = middleNumMatrix[lastIndex][curIndex];
            if (middleIndex != -1 && (points[middleIndex].status == PatternPointBase.STATE_NORMAL) && (points[curIndex].status == PatternPointBase.STATE_NORMAL))
            {
                addSelectedPoint(middleIndex);
            }

        }
    }

    private void addSelectedPoint(int index)
    {
        selectedPoints.add(points[index]);
        points[index].status = PatternPointBase.STATE_SELECTED;
    }

    /**
     * 點的區域大小
     * @return
     */
    private int whichPointArea()
    {
        for (int i = 0; i < 9; i++)
        {
            if (points[i].isPointArea(moveX+10, moveY+10))
            {
                return i;
            }
        }
        return -1;
    }

    /**
     * 畫線
     * @param canvas
     */
    private void drawLine(Canvas canvas)
    {
        Paint paint = getCirclePaint(errorMode ? PatternPoint.STATE_ERROR : PatternPoint.STATE_SELECTED);
        paint.setStrokeWidth(15);

        for (int i = 0; i < selectedPoints.size(); i++)
        {
            if (i != selectedPoints.size() - 1)      //連接線
            {
                PatternPoint first = selectedPoints.get(i);
                PatternPoint second = selectedPoints.get(i + 1);
                canvas.drawLine(first.getCenterX(), first.getCenterY(),
                        second.getCenterX(), second.getCenterY(), paint);
            }
            else if (!drawEnd)                        //自由線,擡手之後就不用畫了
            {
                PatternPoint last = selectedPoints.get(i);
                canvas.drawLine(last.getCenterX(), last.getCenterY(),
                        moveX, moveY, paint);
            }
        }
    }

    /**
     * 畫圓
     * @param canvas
     */
    private void drawCircle(Canvas canvas)
    {
        for (int i = 0; i < 9; i++)
    {
        //圓的中心點
        PatternPoint point = points[i];
        Paint paint = getCirclePaint(point.status);
        //畫內圓
        canvas.drawCircle(point.getCenterX(), point.getCenterY(), points[i].getRadius(), paint);
        //畫外圓環
        canvas.drawCircle(point.getCenterX(), point.getCenterY(), points[i].getRadius()+20,paint1);
    }
    }

    /**
     * 初始化點
     */
    private void initPoints()
    {
        width = getWidth() - getPaddingLeft() - getPaddingRight() - sidePadding * 2;
        height = getHeight() - getPaddingTop() - getPaddingBottom() - topBottomPadding * 2;

        //使用時暫定強制豎屏
        int left, top;
        left = getPaddingLeft() + sidePadding;
        top = height + getPaddingTop() + topBottomPadding - width;
        side = width / 3;

        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                int leftX = left + j * side;
                int topY = top + i * side;
                int index = i * 3 + j;
                points[index] = new PatternPoint(leftX, topY, side, side / 3, String.valueOf(index + 1));
            }
        }

        mHandler = new Handler();

        hasinit = true;
    }

    /**
     * 得到畫筆的顏色
     * @param state
     * @return
     */
    private Paint getCirclePaint(int state)
    {
        Paint paint = new Paint();
        switch (state)
        {
            case PatternPoint.STATE_NORMAL:
                paint.setColor(PAINT_COLOR_NORMAL);
                break;
            case PatternPoint.STATE_SELECTED:
                paint.setColor(PAINT_COLOR_SELECTED);
                break;
            case PatternPoint.STATE_ERROR:
                paint.setColor(PAINT_COLOR_ERROR);
                break;
            default:
                paint.setColor(PAINT_COLOR_NORMAL);
        }
        return paint;
    }
}

首先先繪製佈局中的其他控件,它們與圖案鎖沒有任何關係。接下來分爲3步: 
  1、初始化。參見initPoints()方法,其作用爲創建九個PatternPoint對象,並確定每一個圓的位置和密碼。我們之前說視爲這九個圓位於3*3的方格子中,不過這3*3的方格子不一定要緊貼着佈局的邊界,因此定義了兩個變量sidePadding和topBottomPadding,用於記錄方格子與佈局邊界之間的距離。不過我這裏圖省事兒直接將這兩個值寫死了,實際上最妥當的方案是在attrs.xml中定義這兩個屬性,然後在佈局xml中定義這兩個屬性的值,最後在源文件中獲取這兩個屬性,並且將它們的值賦值給變量。此外需要注意的是,初始化代碼只需執行一次就夠了,而dispatchDraw()會反覆調用,因此需要一個控制變量記錄初始化是否完畢。

  2、畫圓。這個比較簡單,根據不同圓當前處於的狀態進行繪製即可。參見drawCircle()和getCirclePaint()方法。

  3、畫線。這是最複雜的一部分,實現部分在drawLine()方法中,首先我們需要知道要畫的是哪個顏色的線。從上面的效果展示可知,線的顏色一共分爲兩種:正在連線時和連線正確時是同一種顏色,另外就是連線錯誤時的顏色。這裏需要使用一個變量記錄當前是否處於連線錯誤狀態,並且根據這個變量的值去獲取不同的畫筆(Paint對象)。 
  前面說過,連線分爲兩部分,一部分是點和點之間的連線(我們稱之爲連接線),另一部分是最後一個點和當前手指的位置的連線(我們稱之爲自由線)。無論是連接線還是自由線,都需要知道我之前所有連接過的點的順序,因此需要一個ArrayList來記錄它。在繪製自由線的時候,需要知道當前手指的位置(X,Y座標),這兩個值是在onTouchEvent()中獲取的,因此需要兩個類變量記錄它。此外,當我的手擡起來之後,表示我的一次連線已經結束了,這時是不需要繪製自由線的,因此這裏要額外加一個判斷。


  接下來分析一下觸摸事件,它的設計思路大致如下: 
  1、在按下時,如果我手指的位置正處於某個點中,那麼一次連線開始,並且把這個點加入到選中點的List中,作爲第一個點。 
  2、在移動時,如果我已經開始連線,那麼需要明確的是我的選中列中至少已經有一個點了(至少會有一個起始點)。此時需要判斷是否經過了某一個點,並且這個點是還沒有進入選中列中的點。在滿足這些條件之後,進行下面判斷: 
   a)查看我上一個連接的點和這次經過的點中間是否需要插入點(比如上一個點是左上角的點,這裏經過的點是右上角的點,並且正上方的點還沒有進入選中列,此時,應當將正上方的點加入到選中列中,並且在右上角這個點之前插入) 
   b)增加這個經過的點到選中列中。 
  3、在擡起時,如果我已經開始連線,表明我這次連線結束了。這時如果存在連接線而不是僅僅有自由線(即選中列中的點至少有兩個),則去計算這個圖案對應的密碼,提供給外部進行密碼長度和密碼正誤的判斷。既然說到要給外部進行回調,因此需要提供一個接口。 
  4、在每當發生觸摸事件之後,都重新繪製連線。


  下面強調幾個特殊的方法。 
  1、insertPointIfNeeds(),這個方法用於上面說的觸摸事件中2a這個步驟,判斷兩個點中間是否需要插入額外的一個點到選中隊列中。我在程序裏把9個點從左到右,從上到下分別標爲1-9。那麼1和3中需要插入2,4和6中需要插入5等等這些判斷,我通過一個常量矩陣進行獲取,這樣就避免了大片的if,else。矩陣中的值表示需要插入點的index值,-1表示沒有。當然有這樣的點不一定就表示需要插入到選中列中,還需要滿足當前經過的點和中間插入的點之前都沒有在選中列中的條件。 
  2、setAllSelectedPointsError(),這個方法提供給外部Activity調用,當用戶判斷出圖案密碼太短或者圖案密碼錯誤時,將所有選中列中的點的狀態設爲錯誤狀態,同時,將連線的顏色設爲錯誤時連線的顏色。注意設置完成之後需要重繪。 
  3、processFinish(),這個方法主要說一下返回值,從程序中可以看出,它的返回值是一個時間值。因爲當用戶連線完成之後,無論其連線正確與否,都需要將這個連線圖案保持一段時間,而並不是瞬間就恢復到初始狀態。 
  4、reset()方法和resetFinished變量,reset()的作用是將所有記錄狀態的值都恢復到初始化完成的狀態,隨後將resetFinished置爲true。而在resetFinished爲false時,按下、移動、擡起這些觸摸事件都是不起作用的。之前說過,當用戶連線完成之後,需要保持圖案一定時間,而這段時間之內,是不允許用戶進行連線的,resetFinished變量的作用就是控制這個部分。reset()方法中,當所有變量都重置之後,又給外部提供了一個回調方法,它的作用是告訴Activity已經重置完成,如果Activity中有關於密碼正誤判斷的顯示,則可在這個回調中進行重置。

在activity中界面代碼如下:

public class LockActivity extends Activity implements OnPatternStateListener{
	
	 private TextView tvInfo;
	    private PatternLockLayout lockLayout;

	    @Override
	    protected void onCreate(Bundle savedInstanceState)
	    {
	        super.onCreate(savedInstanceState);
	        setContentView(R.layout.lock);
	        tvInfo = (TextView) findViewById(R.id.txt_patternlock_info);
	        tvInfo.setText("請繪製圖案密碼");
	        lockLayout = (PatternLockLayout) findViewById(R.id.layout_lock);
	        lockLayout.setOnPatternStateListener(this);
	        
	        TextView tView=(TextView) findViewById(R.id.txt_patternlock);
	        
	        tView.setOnClickListener(new OnClickListener() {
				
				@Override
				public void onClick(View v) {
					// TODO Auto-generated method stub
					   MySPUtils.clear(getApplicationContext());
	   				   MyApplication.getInstance().exit();
	   				// 極光推送消息註冊別名 調用 Handler 來異步設置別名
	   				   Intent intent = new Intent(LockActivity.this, MainActivity.class);
	   				   startActivity(intent);
	   				   finish();
	   				   SpUtils.remove(getApplicationContext(), Constant.userId+"");
				}
			});
	        
	    }
	    int anInt=0;
	    String psw;

//這段代碼完整的實現了qq和支付寶屏幕鎖的功能

同一個手機不同的賬號可以設置不同的密碼:並且數據持久化:
	    @Override
	    public void onFinish(String password, int sizeOfPoints)
	    {
                       if (TextUtils.isEmpty(SpUtils.getString(getApplicationContext(),Constant.userId+"",""))){
	   	           if(sizeOfPoints<4)
	   	           {
	   	               tvInfo.setText("請連接至少4個點");
	   	               lockLayout.setAllSelectedPointsError();
	   	           }else {
	   	               anInt++;
	   	               tvInfo.setText("請在輸入一遍密碼");
	   	               psw=SpUtils.getString(getApplicationContext(),"pass","");
	   	               if (TextUtils.isEmpty(psw)){
	   	                   SpUtils.putString(getApplicationContext(),"pass",password);
	   	               }else {
	   	                   if (password.equals(psw)&&anInt==2){
	   	                       anInt=0;
	   	                       tvInfo.setText("密碼設置成功");
	   	                       SpUtils.putString(getApplicationContext(),Constant.userId+"",password);
	   	                       finish();
	   	                   }else if (anInt==2){
	   	                       anInt=0;
	   	                       tvInfo.setText("密碼不一致");
	   	                       SpUtils.remove(getApplicationContext(),"pass");
	   	                   }
	   	               }
	   	           }

	   	           return;
	   	       }else if (!TextUtils.isEmpty(SpUtils.getString(getApplicationContext(),Constant.userId+"",""))){
	   	           if(sizeOfPoints<4)
	   	           {
	   	               tvInfo.setText("請連接至少4個點");
	   	               lockLayout.setAllSelectedPointsError();
	   	           }
	   	           else if( !password.equals(SpUtils.getString(getApplicationContext(),Constant.userId+"","")) )
	   	           {
	   	               tvInfo.setText("圖案密碼錯誤");
	   	               ante++;
	   	               if (ante==5) {
	   	            	   ante=0;
	   	            	  lockLayout.setAllSelectedPointsError();
		   	               MySPUtils.clear(getApplicationContext());
		   				   MyApplication.getInstance().exit();
		   				// 極光推送消息註冊別名 調用 Handler 來異步設置別名
		   				   Intent intent = new Intent(this, MainActivity.class);
		   				   startActivity(intent);
		   				 finish();
		   				   SpUtils.remove(getApplicationContext(), Constant.userId+"");
		   				  
					   } 
	   	           }
	   	           else
	   	           {
	   	               tvInfo.setText("圖案正確");
	   	               finish();
	   	           }

	   	       }

	    }
	    int ante=0;

	    @Override
	    public void onReset()
	    {
	        tvInfo.setText("請繪製圖案密碼");
	    }
	    
	    @Override
	    public void onBackPressed() {
	    	// TODO Auto-generated method stub
	    	super.onBackPressed();
	    	 Intent intent = new Intent(this, MainActivity.class);
			 startActivity(intent);
	    	
	    }




發佈了62 篇原創文章 · 獲贊 6 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章