【開源組件】自定義多宮格解鎖

2017年2月4日 下午15:30分

前言

在此之前岖妄,一直在想九宮格的實現(xiàn)方法,經(jīng)過一個上午的初步研究終于完成了一個簡單的N*N的宮格解鎖組件抬探,代碼略顯粗糙子巾,僅僅做到簡單的實現(xiàn),界面等后期在做優(yōu)化小压,純粹是學習的目的线梗,在算法上有點缺陷,如果有錯誤或者更好的方法怠益,歡迎提出仪搔,相互學習。先來看一下預(yù)覽圖

N=3 手指抬起

QQ拼音截圖未命名d.png

N=4 手指沒有抬起

QQ拼音截圖未命名.png

其他的廢話不多說了蜻牢,直接開始吧.....

實現(xiàn)步驟

  • 設(shè)置聲明屬性attrs.xml文件
  • 創(chuàng)建SeniorPoint.java文件
  • 創(chuàng)建View并重寫其中的幾個重要方法
  • 設(shè)置觸摸事件烤咧,并進行數(shù)據(jù)處理
  • 設(shè)置回調(diào)函數(shù),在Activity里面調(diào)用

1抢呆、設(shè)置聲明屬性

很簡單的xml內(nèi)容,在res文件夾里面新建文件attrs.xml煮嫌,將下面的內(nèi)容寫入即可。

<?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>
        <!--圓圈的個數(shù)-->
        <attr name="circlrNumber" format="integer"></attr>
    </declare-styleable>
</resources>

2抱虐、 創(chuàng)建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;
    }
}

3昌阿、創(chuàng)建View并重寫其中的幾個重要方法

至于怎么自定義View這里不在過多的贅述,可以看一下我的文章,里面寫了怎么自定義一個齒輪的View http://www.reibang.com/p/104a9d7eeefd ,創(chuàng)建java文件Lock.java,繼承View組件

獲取在Activity布局中的設(shè)置屬性值

在Activity的布局中的設(shè)置如下,具體每個屬性的意義懦冰,請結(jié)合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文件中設(shè)置的屬性值灶轰,代碼如下:

    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));
        //每行圓的數(shù)目,有事N*N内地,所以也是每列的數(shù)目伴澄,當然也可以根據(jù)次設(shè)置行數(shù)和列數(shù)不同的樣式
        circlrNumber=typedArray.getInt(R.styleable.Lock_circlrNumber,3);
        typedArray.recycle();
        //設(shè)置圓的數(shù)量為0或者負數(shù)的時候異常拋出
        if (circlrNumber<1)
            throw new Exception("圓的數(shù)量不能為0或負數(shù)");
        //用字符串的形式保存點的位置,比如01代表第0行1列瓤鼻,當然可以在回調(diào)函數(shù)根據(jù)自己的需要設(shè)計
        password=new StringBuffer();
        mPaint = new Paint();
        mPaint.setStrokeWidth(dp2px(5));
        //初始化保存圓心位置的二維數(shù)組
        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;
    }

重寫繪制方法

這里體現(xiàn)是對界面的繪制,主要是繪制圓和線茬祷,具體解釋參考注釋,在看一下這個懶到家的模型圖,以N=3為參數(shù)畫的清焕,主要是注意一些點的設(shè)置

無標題.png
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設(shè)置繪制圓形的時候,圓心的移動步長
        int setp_x = getWidth() / circlrNumber;
        int setp_y = getHeight() / circlrNumber;
        //設(shè)置第一個圓的位置祭犯,后面的圓形的繪制都是相對于第一各院的圓心的位置進行移動秸妥,移動的單位也就是setp_x和setp_y
        int mPaint_x = getWidth() / (2*circlrNumber), mPaint_y = getHeight() / (2*circlrNumber);
        //設(shè)置繪制圓形的畫筆信息
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(circleWidth);
        mPaint.setColor(circleColor);
        //循環(huán),開始繪制圓形
        for (int i = 0; i < circlrNumber; i++)
            for (int j = 0; j < circlrNumber; j++) {
                //此處開始繪保存圓心位置信息,設(shè)置為沒有選中
                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) {
            //如果存在被選中的點粥惧,則開始進行連線操作
            //重新設(shè)置畫筆的參數(shù)信息
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(lineWidth);
            //如果現(xiàn)在的點的位置為(0,0)那么說明,手已經(jīng)抬起來了最盅,將這顯得顏色更改為設(shè)置顏色突雪,否則的話使用另外的顏色
            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);
                }
            }
            //如果手沒有抬起,繼續(xù)跟隨手的位置來移動
             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);
            }

        }

    }

設(shè)置觸摸事件涡贱,并進行數(shù)據(jù)處理

    @Override
    public boolean onTouchEvent(MotionEvent event) {
//        return super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //每次重新按下之前咏删,要清除arratList中的列表保存的信息
                arrayList.clear();
            case MotionEvent.ACTION_MOVE:
                //判斷當前的手指的位置有沒有在院內(nèi),如果在院內(nèi)返回這個圓的的圓心并設(shè)置改圓為選中狀態(tài)问词,否則返回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:
                //手指從屏幕離開后督函,將當前點的坐標設(shè)置為(0,0)
                nowPoint.set(0, 0);
                //離開后,讀取已經(jīng)選中的位置信息激挪,返回給回調(diào)函數(shù)
                //這里僅僅返回來的坐標點的位置辰狡,需要處理下才行
                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 + "\n");
                }
                if (onFinsh != null)
                    onFinsh.leaver(password.toString());

                //重繪
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    public SeniorPoint checkLocation(float x, float y) {
        //此處循環(huán)檢測九個點的位置,此處代碼使用算法優(yōu)化,沒有必要循環(huán)判斷位置
        //后面有時間會專門寫一個文章來分析下垄分,追求更快的方法
        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);
    }

設(shè)置回調(diào)函數(shù)薄湿,在Activity里面調(diào)用

手抬起的時候叫倍,應(yīng)該將選擇的結(jié)果返回給Activity豌鸡,在Activity中檢查是否解鎖成功,然后進行相應(yīng)的處理.
首先定義接口,并在Lock.java文件中定義

private OnFinsh onFinsh;
//set方法
    public void setOnFinsh(OnFinsh onFinsh) {
        this.onFinsh = onFinsh;
    }

//當手指抬起的時候,調(diào)用其leaver()方法,將結(jié)果回調(diào)到Activity中
//在上面的OnTouch事件中調(diào)用
 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);
        //綁定變量段标,并設(shè)置回調(diào)函數(shù)
        lock= (Lock) findViewById(R.id.lock);
        lock.setOnFinsh(new Lock.OnFinsh() {
            @Override
            public void leaver(String password) {
                Log.e("PassWord",password);
            }
        });
    }
}

打印的結(jié)果如下:
N=4的選中10個點

QQ拼音截圖未命名4.png

后記

當然也可以設(shè)置一下屬性:

  • 是否可見,設(shè)置為布爾型數(shù)據(jù)炉奴,如果true則繪制直線逼庞,否則不繪制直線肤寝。
  • 增加選中一個點之后即調(diào)用的回調(diào)函數(shù)
  • 設(shè)置類型為填充圓或者點或者圖片
    ......

目前還存在的問題:

  • 當手指處于某一點的時候十电,判斷這個位置是不是在某一圓內(nèi),這里為了簡單摸恍,使用了循環(huán)判斷的方法砸逊,但是顯然這種效率是很慢的璧南,所以我想了下面的過程,不知是否合適:
    1、將view界面想象分割成N*N的界面
    2师逸、首先大致判斷手指的位置是不是在某個方格內(nèi)司倚,如果在,那么找到這個方格內(nèi)的那個圓
    3篓像、通過一些邏輯計算得到這個圓的圓心位置

本博客內(nèi)容一致同步到本人的博客站點:http://www.zhoutaotao.xyz 歡迎訪問留言交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末动知,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子员辩,更是在濱河造成了極大的恐慌盒粮,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奠滑,死亡現(xiàn)場離奇詭異丹皱,居然都是意外死亡,警方通過查閱死者的電腦和手機宋税,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門摊崭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弃甥,你說我怎么就攤上這事爽室。” “怎么了淆攻?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵阔墩,是天一觀的道長。 經(jīng)常有香客問我瓶珊,道長啸箫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任伞芹,我火速辦了婚禮忘苛,結(jié)果婚禮上蝉娜,老公的妹妹穿的比我還像新娘。我一直安慰自己扎唾,他們只是感情好召川,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胸遇,像睡著了一般荧呐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纸镊,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天倍阐,我揣著相機與錄音,去河邊找鬼逗威。 笑死峰搪,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的凯旭。 我是一名探鬼主播概耻,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尽纽!你這毒婦竟也來了咐蚯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤弄贿,失蹤者是張志新(化名)和其女友劉穎春锋,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體差凹,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡期奔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了危尿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呐萌。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谊娇,靈堂內(nèi)的尸體忽然破棺而出肺孤,到底是詐尸還是另有隱情,我是刑警寧澤济欢,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布赠堵,位于F島的核電站,受9級特大地震影響法褥,放射性物質(zhì)發(fā)生泄漏茫叭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一半等、第九天 我趴在偏房一處隱蔽的房頂上張望揍愁。 院中可真熱鬧呐萨,春花似錦、人聲如沸莽囤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朽缎。三九已至怯屉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饵沧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工赌躺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留狼牺,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓礼患,卻偏偏與公主長得像是钥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子缅叠,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容