在此之前,一直在想九宮格的實現方法,經過一個上午的初步研究終於完成了一個簡單的N*N的宮格解鎖元件,程式碼略顯粗糙,僅僅做到簡單的實現,介面等後期在做優化,純粹是學習的目的,在演算法上有點缺陷,如果有錯誤或者更好的方法,歡迎提出,相互學習。先來看一下預覽圖
九宮格效果展示
N=3 手指抬起
N=4 手指沒有抬起
其他的廢話不多說了,直接開始吧…..
實現步驟
- 設定宣告屬性attrs.xml檔案
- 建立SeniorPoint.java檔案
- 建立View並重寫其中的幾個重要方法
- 設定觸控事件,並進行資料處理
- 設定回撥函式,在Activity裡面呼叫
設定宣告屬性
很簡單的xml內容,在res資料夾裡面新建檔案attrs.xml,將下面的內容寫入即可。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Lock">
<!--圓的半徑-->
<attr name="circleRadius" format="dimension"></attr>
<!--圓的顏色-->
<attr name="circleColor" format="color"></attr>
<!--圓的線寬-->
<attr name="circlrWidth" format="dimension"></attr>
<!--線的寬度-->
<attr name="LineWidth" format="dimension"></attr>
<!--線的顏色-->
<attr name="LineColor" format="color"></attr>
<!--鬆開手之後線的顏色-->
<attr name="LineColorAfterLeave" format="color"></attr>
<!--圓圈的個數-->
<attr name="circlrNumber" format="integer"></attr>
</declare-styleable>
</resources>
建立SeniorPoint.java檔案
SeniorPoint.java是一個Bean,裡面儲存著以圓心點的參考資訊,程式碼如下:
package cn.example.tao.newview;
import android.graphics.Point;
/**
* Created by Tao on 2017/2/3.
*/
public class SeniorPoint extends Point {
private boolean isSelect=false;
public SeniorPoint(int x, int y, boolean isSelect) {
super(x, y);
this.isSelect = isSelect;
}
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
}
建立View並重寫其中的幾個重要方法
至於怎麼自定義View這裡不在過多的贅述,可以看一下我的文章,裡面寫了怎麼自定義一個齒輪的View http://www.jianshu.com/p/104a9d7eeefd ,建立java檔案Lock.java,繼承View元件
獲取在Activity佈局中的設定屬性值
在Activity的佈局中的設定如下,具體每個屬性的意義,請結合attrs.xml檔案分析:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@mipmap/sky"
>
<cn.example.tao.newview.widget.Lock
android:id="@+id/lock"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
tools:circleColor="#EAEAEA"
tools:circlrWidth="2dp"
tools:circleRadius="30dp"
tools:LineColor="#EAEAEA"
tools:LineWidth="3dp"
tools:LineColorAfterLeave="#77E6D8"
tools:circlrNumber="3"
/>
</LinearLayout>
首先獲得我們在xml檔案中設定的屬性值,程式碼如下:
public Lock(Context context, AttributeSet attrs) throws Exception {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Lock);
//繪製圓的半徑,預設值30dp
circleRadius = typedArray.getDimension(R.styleable.Lock_circleRadius, dp2px(30));
//繪製圓形的顏色,預設白色
circleColor = typedArray.getColor(R.styleable.Lock_circleColor, Color.WHITE);
//繪製圓形的寬度,預設3dp
circleWidth = typedArray.getDimension(R.styleable.Lock_circlrWidth, dp2px(3));
//折線的顏色
lineColor = typedArray.getColor(R.styleable.Lock_LineColor, Color.GRAY);
//折線的寬度
lineWidth = typedArray.getDimension(R.styleable.Lock_LineWidth, dp2px(1));
//連線完成後的線的顏色
lineColorAfterLeaver = typedArray.getColor(R.styleable.Lock_LineColorAfterLeave, Color.argb(255, 92, 186, 167));
//每行圓的數目,有事N*N,所以也是每列的數目,當然也可以根據次設定行數和列數不同的樣式
circlrNumber=typedArray.getInt(R.styleable.Lock_circlrNumber,3);
typedArray.recycle();
//設定圓的數量為0或者負數的時候異常丟擲
if (circlrNumber<1)
throw new Exception("圓的數量不能為0或負數");
//用字串的形式儲存點的位置,比如01代表第0行1列,當然可以在回撥函式根據自己的需要設計
password=new StringBuffer();
mPaint = new Paint();
mPaint.setStrokeWidth(dp2px(5));
//初始化儲存圓心位置的二維陣列
location = new SeniorPoint[circlrNumber][circlrNumber];
}
private float dp2px(int i) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, i, getResources().getDisplayMetrics());
}
重寫測量方法
在此直接寫程式碼,不解釋了
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getValueByComplete(widthMeasureSpec);
int height = getValueByComplete(heightMeasureSpec);
setMeasuredDimension(width, width);
}
public int getValueByComplete(int value) {
int size = MeasureSpec.getSize(value);
int mode = MeasureSpec.getMode(value);
int resultValue = 0;
if (mode == MeasureSpec.EXACTLY) {
resultValue = size;
} else {
resultValue = (int) mPaint.descent();
if (mode == MeasureSpec.AT_MOST)
resultValue = size;
}
return resultValue;
}
重寫繪製方法
這裡體現是對介面的繪製,主要是繪製圓和線,具體解釋參考註釋,在看一下這個懶到家的模型圖,以N=3為引數畫的,主要是注意一些點的設定
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//設定繪製圓形的時候,圓心的移動步長
int setp_x = getWidth() / circlrNumber;
int setp_y = getHeight() / circlrNumber;
//設定第一個圓的位置,後面的圓形的繪製都是相對於第一各院的圓心的位置進行移動,移動的單位也就是setp_x和setp_y
int mPaint_x = getWidth() / (2*circlrNumber), mPaint_y = getHeight() / (2*circlrNumber);
//設定繪製圓形的畫筆資訊
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(circleWidth);
mPaint.setColor(circleColor);
//迴圈,開始繪製圓形
for (int i = 0; i < circlrNumber; i++)
for (int j = 0; j < circlrNumber; j++) {
//此處開始繪儲存圓心位置資訊,設定為沒有選中
if (location[i][j] == null)
location[i][j] = new SeniorPoint(mPaint_x + j * setp_x, mPaint_y + i * setp_y, false);
//開始繪製圓形,圓心座標(mPaint_x + i * setp_x, mPaint_y + j * setp_y)
canvas.drawCircle(mPaint_x + i * setp_x, mPaint_y + j * setp_y, circleRadius, mPaint);
}
//使用arrayList儲存被選中的點的資訊
if (arrayList != null && arrayList.size() > 0) {
//如果存在被選中的點,則開始進行連線操作
//重新設定畫筆的引數資訊
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(lineWidth);
//如果現在的點的位置為(0,0)那麼說明,手已經抬起來了,將這顯得顏色更改為設定顏色,否則的話使用另外的顏色
if (nowPoint != null && nowPoint.x == 0 && nowPoint.y == 0)
mPaint.setColor(lineColorAfterLeaver);
else mPaint.setColor(lineColor);
//進行折線的繪製工作
for (int i = 0; i < arrayList.size(); i++) {
canvas.drawPoint(arrayList.get(i).x, arrayList.get(i).y, mPaint);
if (i != 0) {
canvas.drawLine(arrayList.get(i - 1).x, arrayList.get(i - 1).y, arrayList.get(i).x, arrayList.get(i).y, mPaint);
}
}
//如果手沒有抬起,繼續跟隨手的位置來移動
if ((nowPoint != null && nowPoint.x != 0 && nowPoint.y != 0)) {
canvas.drawLine(arrayList.get(arrayList.size() - 1).x, arrayList.get(arrayList.size() - 1).y, nowPoint.x, nowPoint.y, mPaint);
}
}
}
設定觸控事件,並進行資料處理
@Override
public boolean onTouchEvent(MotionEvent event) {
// return super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//每次重新按下之前,要清除arratList中的列表儲存的資訊
arrayList.clear();
case MotionEvent.ACTION_MOVE:
//判斷當前的手指的位置有沒有在院內,如果在院內返回這個圓的的圓心並設定改圓為選中狀態,否則返回null
SeniorPoint select = checkLocation(event.getX(), event.getY());
//配置點前手指的位置
if (nowPoint == null)
nowPoint = new SeniorPoint((int) event.getX(), (int) event.getY(), false);
else nowPoint.set((int) event.getX(), (int) event.getY());
if (select != null)
select.setSelect(true);
//重繪
invalidate();
break;
case MotionEvent.ACTION_UP:
//手指從螢幕離開後,將當前點的座標設定為(0,0)
nowPoint.set(0, 0);
//離開後,讀取已經選中的位置資訊,返回給回撥函式
//這裡僅僅返回來的座標點的位置,需要處理下才行
password.delete(0,password.length());
for (int i = 0; i < arrayList.size(); i++) {
password.append("第"+i+"個點的座標 X:" + arrayList.get(i).x + " Y:" + arrayList.get(i).y + "
");
}
if (onFinsh != null)
onFinsh.leaver(password.toString());
//重繪
invalidate();
break;
}
return super.onTouchEvent(event);
}
public SeniorPoint checkLocation(float x, float y) {
//此處迴圈檢測九個點的位置,此處程式碼使用演算法優化,沒有必要迴圈判斷位置
//後面有時間會專門寫一個文章來分析下,追求更快的方法
double radio = dp2px(30);
for (int i = 0; i < circlrNumber; i++)
for (int j = 0; j < circlrNumber; j++) {
double l = getLong(x, y, location[i][j]);
if (l <= radio) {
//如果已選中的列表的長度為0 或者長度不是0,但是也不能和arryList表中上一個值完全一樣,這樣才能新增
if (arrayList.size() ==0 || (arrayList.size()>=1 && (location[i][j].x != arrayList.get(arrayList.size() - 1).x || location[i][j].y != arrayList.get(arrayList.size() - 1).y)))
arrayList.add(location[i][j]);
return location[i][j];
}
}
return null;
}
public double getLong(float x, float y, SeniorPoint point) {
//返回點前手指的點到圓形的位置
double s = Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2);
return Math.sqrt(s);
}
設定回撥函式,在Activity裡面呼叫
手抬起的時候,應該將選擇的結果返回給Activity,在Activity中檢查是否解鎖成功,然後進行相應的處理.
首先定義介面,並在Lock.java檔案中定義
private OnFinsh onFinsh;
//set方法
public void setOnFinsh(OnFinsh onFinsh) {
this.onFinsh = onFinsh;
}
//當手指抬起的時候,呼叫其leaver()方法,將結果回撥到Activity中
//在上面的OnTouch事件中呼叫
if (onFinsh != null)
onFinsh.leaver(password.toString());
public interface OnFinsh {
void leaver(String password);
}
在Activity中這樣使用
package cn.example.tao.newview;
import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import cn.example.tao.newview.widget.Lock;
public class MainActivity extends FragmentActivity {
//宣告變數
private Lock lock;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
//繫結變數,並設定回撥函式
lock= (Lock) findViewById(R.id.lock);
lock.setOnFinsh(new Lock.OnFinsh() {
@Override
public void leaver(String password) {
Log.e("PassWord",password);
}
});
}
}
列印的結果如下:
N=4的選中10個點
後記
當然也可以設定一下屬性:
- 是否可見,設定為布林型資料,如果true則繪製直線,否則不繪製直線。
- 增加選中一個點之後即呼叫的回撥函式
- 設定型別為填充圓或者點或者圖片
……
目前還存在的問題:
- 當手指處於某一點的時候,判斷這個位置是不是在某一圓內,這裡為了簡單,使用了迴圈判斷的方法,但是顯然這種效率是很慢的,所以我想了下面的過程,不知是否合適:
1、將view介面想象分割成N*N的介面
2、首先大致判斷手指的位置是不是在某個方格內,如果在,那麼找到這個方格內的那個圓
3、通過一些邏輯計算得到這個圓的圓心位置
本部落格內容一致同步到本人的部落格站點:http://www.zhoutaotao.xyz 歡迎訪問留言交流