前言
先说一下需求:如图所示一张图片,分割成多行多列。分割后的格子可以选中,将选中的格子返回给后台。
最终效果
分析
需求到手先别急着写,先分析一波需求。
上面提到:
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);