[Android] 记一次自定义View实战-附代码

前言

先说一下需求:如图所示一张图片,分割成多行多列。分割后的格子可以选中,将选中的格子返回给后台。

最终效果

最终效果

分析

需求到手先别急着写,先分析一波需求。
上面提到:
1.是一张图片分割,那么我们是不是可以直接继承ImageView,在ImageView的基础上进行操作?
2.分割成若干个格子,几行几列,我们是不是需要将列和行提成动态参数?
3.格子需要选中,我们是不是需要计算格子选中后的坐标和目前在所有格子的第几位?

理清上面几点问题后,先画个草图。验算一波,再开始设计。

项目地址

目前VIew已基本成型,放上git地址,欢迎各位大佬一起研究和提bug。

https://github.com/tc7326/LatticeImageView

推荐结合代码阅读已获得更好的阅读体验

开始

直接上代码,注释已经比较全了,有坑的地方还会着重讲一下。

初始化View

public class LatticeImageView extends ImageView {

    Paint latticePaint, selectPaint;//两个画笔,画线的笔和画选中格子的笔。

    int lineColor;//线的颜色。

    int[] selectedOneList;//用数组来记录选中格子,0表示未选中,1表示选中。

    float viewHeight, viewWidth;//View的宽和高。

    float oneHeight, oneWidth;//一个格子的宽和高。

    private int numberOfLines, numberOfColumns; //View的行数和列数。

    int nowScrollOne;//用于滑动当前方格的处理。

    GestureDetector gestureDetector;//手势处理。
	
	//构造函数1
    public LatticeImageView(Context context) {
        this(context, null);
    }
	//构造函数2
    public LatticeImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
	//构造函数3
    public LatticeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

这里我们的LatticeImageView先是继承自ImageView,然后重写它的三个构造方法。(一般只重写最后一个即可)
接下来,初始化我们用来绘制的画笔。
在构造3中加入如下代码

    public void initPaint() {

        latticePaint = new Paint();
        latticePaint.setStyle(Paint.Style.STROKE);
        latticePaint.setColor(lineColor);

        selectPaint = new Paint();
        selectPaint.setColor(lineColor);
        selectPaint.setAlpha(127);
        selectPaint.setStyle(Paint.Style.FILL);

    }

共两种笔,一个画线的无透明度,一个画选中的格子有透明度。

onMeasure和onLayout

由于我们的LatticeImageView是基于ImageView的,所以这两个方法直接依赖ImageView即可,无需重写。

onDraw

以上准备工作完成后,开始真正的绘制了。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        viewWidth = this.getWidth();
        viewHeight = this.getHeight();

        drawDefLine(canvas);

        drawSelectOne(canvas);

    }

这里有两个点要讲一下
1.为什么要在super.onDraw()之后才执行方法?
因为super前画的东西在ImageView的图片之下,super后的东西才会在ImageView的图片只上,也就是所谓的前景和背景。
2.这里我为什么要在draw中获取了ImageVIew的宽和高?
因为是基于ImageView的,是在ImageView处理完它是事情后,我们才开始处理我们定义的东西。

drawDefLine()

绘制行和列

    //画基本的分隔线
    public void drawDefLine(Canvas canvas) {

        //最外层的一圈长方形
        canvas.drawRect(1, 1, viewWidth, viewHeight, latticePaint);

        oneHeight = viewHeight / numberOfLines;
        oneWidth = viewWidth / numberOfColumns;

        //横线
        for (int i = 1; i < numberOfLines; i++) {
            canvas.drawLine(0, oneHeight * i, viewWidth, oneHeight * i, latticePaint);
        }

        //竖线
        for (int i = 1; i < numberOfColumns; i++) {
            canvas.drawLine(oneWidth * i, 0, oneWidth * i, viewHeight, latticePaint);
        }

    }

这里应该很好理解,先是画个矩形,包住整个ImageView,可以理解为最外层的边。
再是计算单独一个格子的宽和高。最后行遍历,列遍历画线,这样一个一个格子就出现了。

drawSelectOne

绘制选中的格子,前面线画完了,就要绘制选中的格子了。

    //画选中的格子
    private void drawSelectOne(Canvas canvas) {

        for (int i = 1; i <= selectedOneList.length; i++) {
            if (selectedOneList[i - 1] == 1) {
                int x, y;
                if (i / numberOfColumns >= 1 && i % numberOfColumns == 0) {
                    x = i / numberOfColumns;
                    y = numberOfColumns;
                } else {
                    x = i % numberOfColumns == 0 ? 1 : i / numberOfColumns + 1;
                    y = i % numberOfColumns;//取余,余几就证明在第几列
                }
                canvas.drawRect((y - 1) * oneWidth, (x - 1) * oneHeight, oneWidth * y, x * oneHeight, selectPaint);
            }
        }

    }

本质还是画一个一个的矩形,只是位置确认有点绕,刚开始只用脑子想,想了半天还要绕不出来了,最后还是放弃了,上手,我在本子上画了几个格子后,马上就理清位置关系了,很快就可以求出公式了。所以该用笔的时候还是用一下。
这里selectedOneList数组是存放选中的位置的集合的,0表示默认状态,1表示选中状态。
[0,0,1,0]就表示,第1,2,4个格子为没选中,第3个格子选中了,以此类推即可。

手势检测

onDraw绘制完成后,就需要我们的手势检测了,自定义View一般会将onTouchEvent指向我们重写的GestureDetector。

onTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP)
            nowScrollOne = 0;
        return gestureDetector.onTouchEvent(event);
    }

GestureDetector.OnGestureListener

    //监听重写
    GestureDetector.OnGestureListener listener = new GestureDetector.OnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {
            selectOneByXY(e.getX(), e.getY());
            return true;//这里必须为true,否则下面的事件就无法回调
        }

        @Override
        public void onShowPress(MotionEvent e) {
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            selectOneByXY(e2.getX(), e2.getY());
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return true;
        }
    };

在监听里onDown和onScroll里分别调用了selectOneByXY方法,因为我们的View既可以单选,又可以滑动选择,又为了使单选和滑动选择不发生冲突,所以在onTouchEvent里的检测用户是按下还是抬起的操作,在抬起的时候将nowScrollOne 目前选中的块置为0。即可解决单选和滑动多选的冲突。

selectOneByXY

    //根据坐标,找需要处理的方格
    public void selectOneByXY(float x, float y) {
        int i = (int) (x / oneWidth);
        int j = (int) (y / oneHeight);

        if (x > viewWidth || y > viewHeight) {
            //这里好像有可能选中的值大于view的值
            return;
        }

        if (nowScrollOne != j * numberOfColumns + i + 1) {
            selectOne(j * numberOfColumns + i + 1);
        }
        nowScrollOne = j * numberOfColumns + i + 1;
    }

这个就是根据坐标选中某个块的方法了,就是ImageView的宽(高)除以一个块的宽(高),出来的整数值就是块所在矩阵的位置。

整个view到此基本就结束了,主要讲的还是思维和运算公式,具体可以看一看代码。

自定义属性

接下来就是自定义属性。

res—>values—>attr.xml中定义我们View的属性

    <declare-styleable name="LatticeImageView">
        //线的颜色
        <attr name="Line_color" format="color" />
        //是否全选
        <attr name="select_all" format="boolean" />
        //行数,默认4
        <attr name="lines_number" format="integer" />
        //列数,默认8
        <attr name="columns_number" format="integer" />

    </declare-styleable>

定义玩xml文件后,在View中如何调用?
记得上面的构造3嘛?在它里面可以

    //初始化属性值
    public void initTypedArray(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LatticeImageView);
        numberOfLines = typedArray.getInteger(R.styleable.LatticeImageView_lines_number, 4);
        numberOfColumns = typedArray.getInteger(R.styleable.LatticeImageView_columns_number, 8);
        lineColor = typedArray.getColor(R.styleable.LatticeImageView_Line_color, 0xFFFFFFFF);
        selectedOneList = new int[numberOfLines * numberOfColumns];
        if (typedArray.getBoolean(R.styleable.LatticeImageView_select_all, false))
            Arrays.fill(selectedOneList, 1);
        typedArray.recycle();
    }

就可以获取咱们定义的属性值。
在Activity布局中对应的是

    <info.itloser.LatticeImageView
        android:id="@+id/liv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:adjustViewBounds="true"
        android:scaleType="fitXY"
        android:src="@drawable/bg"
        app:columns_number="6"
        app:lines_number="4"
        app:select_all="false" />

注意下面三个app:开头的属性值,就是我们设置的,我们在布局中设置的值,在initTypedArray的时候就可以获取到。

总结

自定义view说难有些东西确实不好处理,绕在里边半天出不来,说简单,理清思路后再下手,将会顺畅很多。

LatticeImageView的基本用法

布局中:

    <info.itloser.LatticeImageView
        android:id="@+id/liv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:columns_number="6"
        app:Line_color="@color/colorAccent"
        app:lines_number="4"
        app:select_all="true" />

表示行数为4,列数为6,颜色为colorAccent,并且全部选中。

代码中:

//动态设置行和列
LatticeImageView.setNumberOfLinesAndColumns(int numberOfLines, int numberOfColumns);

//根据数组进行批量设置
LatticeImageView.selectByArray(int[] ints);

//选中所有或者取消选中所有
LatticeImageView.selectAll(boolean b);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值