zoukankan      html  css  js  c++  java
  • Android自定义多宫格解锁控件

    在此之前,一直在想九宫格的实现方法,经过一个上午的初步研究终于完成了一个简单的N*N的宫格解锁组件,代码略显粗糙,仅仅做到简单的实现,界面等后期在做优化,纯粹是学习的目的,在算法上有点缺陷,如果有错误或者更好的方法,欢迎提出,相互学习。先来看一下预览图

    九宫格效果展示

    N=3 手指抬起

    QQ拼音截图未命名d.png

    N=4 手指没有抬起

    QQ拼音截图未命名.png

    其他的废话不多说了,直接开始吧.....

    实现步骤

    • 设置声明属性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为参数画的,主要是注意一些点的设置

    无标题.png

        @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个点

    QQ拼音截图未命名4.png

    后记

    当然也可以设置一下属性:

    • 是否可见,设置为布尔型数据,如果true则绘制直线,否则不绘制直线。
    • 增加选中一个点之后即调用的回调函数
    • 设置类型为填充圆或者点或者图片
      ......

    目前还存在的问题:

    • 当手指处于某一点的时候,判断这个位置是不是在某一圆内,这里为了简单,使用了循环判断的方法,但是显然这种效率是很慢的,所以我想了下面的过程,不知是否合适:
      1、将view界面想象分割成N*N的界面
      2、首先大致判断手指的位置是不是在某个方格内,如果在,那么找到这个方格内的那个圆
      3、通过一些逻辑计算得到这个圆的圆心位置

    本博客内容一致同步到本人的博客站点:http://www.zhoutaotao.xyz 欢迎访问留言交流

  • 相关阅读:
    第二轮冲刺-Runner站立会议06
    第二轮冲刺-Runner站立会议05
    第二轮冲刺-Runner站立会议04
    找水王续
    第二轮冲刺-Runner站立会议03
    第二轮冲刺-Runner站立会议02
    用户故事与敏捷开发方法笔记05
    软件需求与分析课堂讨论1
    16秋进度条3
    用户故事与敏捷开发方法笔记04
  • 原文地址:https://www.cnblogs.com/zhoutao825638/p/10382012.html
Copyright © 2011-2022 走看看