距离上一篇文章时间已经有点久远了,其实中间也写了几个好玩的view,但是由于自己太懒也没放上来,2017年新年开始了,公司的项目不紧张,闲下来就来补几篇.
自己平时也比较喜欢刷头条,看到头条上面好多个类别,在类别选择页面有一个类别选择的页面,如下图
感觉这个如果用gridview应该也能够实现功能,但是上下两个列表用起来也挺麻烦,还是数据联动问题,后来我就考虑把这个整体做在一个view里面.
做出来之后的效果是这个样子的.下面就来一步一步完成这个自定义view.
初始化信息
private void init() {
//初始化列表
wait = new ArrayList<>();
select = new ArrayList<>();
waitList = new ArrayList<>();
selectList = new ArrayList<>();
//初始化画笔
paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setAntiAlias(true);
paint.setTextSize(36);
paint.setColor(Color.parseColor("#008080"));
//定义每一个类别条目所占的宽度和高度,这里宽度设置的是四个文字的宽度
Rect rect = new Rect();
paint.getTextBounds("四个文字", 0, 4, rect);
textHeight = rect.height();
singleHeight = rect.height() + textPadding * 2;
singleWidth = rect.width() + textPadding * 2;
Log.d(TAG, "init: singleHeight: " + singleHeight + "singleWidth: " + singleWidth);
}
这一部分是用来做一些初始化的才做的,重点在里面的第三部分获取到的三个变量值:textHeight / singleHeight / singleWidth.
这三个变量分别代表的意思是:
1.textHeight:字号为36时,文字的高度.
2.singleHeight:显示在手机上的条目的高度,相对于textHeight增加了两个textPadding的大小,主要是为了让后面画边框的时候不让边框直接挨着字体,为了好看.
3.singleWidth:原理同上,条目的宽度.
获取测量数据(onMeasure)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSize < singleWidth) {
//设置的宽度必须至少是一个条目的宽度
throw new RuntimeException("the width too small");
}
//每一行的条目数
countOfLine = widthSize / singleWidth;
//放置的条目很少有正好放满整行的情况,这时把剩余空间分成两份,放置在两端
viewMargin = (widthSize - countOfLine * singleWidth) / 2;
Log.d(TAG, "onMeasure: countOfLine: " + countOfLine + "viewMargin: " + viewMargin);
int measuredHeight;
//1.计算已选择区域的高度
if (selectList.size() == 0) {//没有被选中的内容
selectHeight = singleHeight;
} else {
selectHeight = selectList.size() % countOfLine == 0 ? selectList.size() / countOfLine * singleHeight : (selectList.size() / countOfLine + 1) * singleHeight;
}
//2.计算待选区域的高度
if (waitList.size() == 0) {
waitHeight = singleHeight;
} else {
waitHeight = waitList.size() % countOfLine == 0 ? waitList.size() / countOfLine * singleHeight : (waitList.size() / countOfLine + 1) * singleHeight;
}
contentHeight = selectHeight + waitHeight + dividerHeight + 1;//内容的总高度(考虑高滚动时要用)
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else {
measuredHeight = Math.min(heightSize, contentHeight);
}
initData();
Log.d(TAG, "onMeasure: widthSize: " + widthSize + "measuredHeight: " + measuredHeight);
setMeasuredDimension(widthSize, measuredHeight);
}
//将传入的字符串转换成对象
private void initData() {
int currentSelectLine = -1;
if (selectList.size() != 0) {
for (int i = 0; i < selectList.size(); i++) {
if (i % countOfLine == 0) {
currentSelectLine++;
}
selectList.get(i).setDAta(viewMargin + i % countOfLine * singleWidth, singleHeight * currentSelectLine, viewMargin + (i % countOfLine + 1) * singleWidth, singleHeight * (currentSelectLine + 1));
}
}
int currentWaitLine = -1;
if (waitList.size() != 0) {
for (int i = 0; i < waitList.size(); i++) {
if (i % countOfLine == 0) {
currentWaitLine++;
}
waitList.get(i).setDAta(viewMargin + i % countOfLine * singleWidth, singleHeight * currentWaitLine + selectHeight + dividerHeight, viewMargin + (i % countOfLine + 1) * singleWidth, singleHeight * (currentWaitLine + 1) + selectHeight + dividerHeight);
}
}
}
这一部分主要是确定view的大小,以及确定了每一个条目要draw的位置.
1.确定view的宽高
1.确定宽
如果在xml中确定了宽,则按xml中设置的来定,如果xml给的是fill_parent或者wrap_content则都按照fill_parent处理,即取父控件建议宽度,但是这两个都有一个原则:宽度至少能容下一个条目,否则就抛错.
2.确定高
高度的确定就要相对麻烦一点,如果xml中给了确定的值,则使用该值来确定高度,否则就按照wrap_content处理,通过计算:选择区高度+分割区高度+备选区高度的=内从总高度,内容的高度并不一定就是view的高度,view的高度最大就是采用fill_parent方式获取到的值,所以当xml中没有给定确定的值给高度时,取父控件建议高度heightSize和内容高度contentHeight两者的较小的一个.
2.确定每个条目item的位置
确定每个item位置的代码在initData函数中,主要逻辑是通过计算确定出每个item的位置信息left,top,right,bottom,然后将这四个值设置到对应的item对象中去,以便在ondraw中将其画到对应位置上.
开始绘制view(onDraw)
确定好了view的宽高以及每个item的位置之后,就可以开始绘制view了.
@Override
protected void onDraw(Canvas canvas) {
Log.d(TAG, "onDraw: selectList size: " + selectList.size() + " waitList size : " + waitList.size());
//1.画已选内容
paint.setStyle(Paint.Style.FILL);
if (selectList.size() == 0) {
canvas.drawText("请点击待选区类别进行添加.", widthSize / 2, selectHeight / 2, paint);
} else {
for (int i = 0; i < selectList.size(); i++) {
TextItem textItem = selectList.get(i);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(textItem.getText(), textItem.getLeft() + singleWidth / 2, textItem.getTop() + (singleHeight + textHeight) / 2, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.parseColor("#008080"));
canvas.drawRoundRect(textItem.getRectF(), singleHeight / 2, singleHeight / 2, paint);
}
}
//画间隔线
paint.setStrokeWidth(5);
canvas.drawLine(0, selectHeight + dividerHeight / 2, widthSize, selectHeight + dividerHeight / 2, paint);
paint.setStrokeWidth(1);
//2.画备选内容
if (waitList.size() == 0) {
paint.setStyle(Paint.Style.FILL);
canvas.drawText("没有备选项.", widthSize / 2, selectHeight + waitHeight / 2 + dividerHeight, paint);
} else {
for (int i = 0; i < waitList.size(); i++) {
TextItem textItem = waitList.get(i);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(textItem.getText(), textItem.getLeft() + singleWidth / 2, textItem.getTop() + (singleHeight + textHeight) / 2, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.parseColor("#008080"));
canvas.drawRoundRect(textItem.getRectF(), singleHeight / 2, singleHeight / 2, paint);
}
}
}
这部分没有太对需要解释的,因为在onMeasure中我们已经确定好了item的位置信息,这时候只需要从对应对象中取出相应信息去进行绘制即可,相关的绘制的api都不难,网上也能找到详细解释.
事件处理
这里要处理的事件有两个:1.点击备选区条目添加到选择区(从选择删除回备选区未实现,原理一样);2.当内容的长度大于view的高度时,处理滚动事件
GestureDetector detector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {
private int dy;
@Override
public boolean onDown(MotionEvent e) {
Log.d(TAG, "onDown: ");
return true;
}
@Override
public void onShowPress(MotionEvent e) {
Log.d(TAG, "onShowPress: ");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.d(TAG, "onSingleTapUp: ");
int position = checkTouchPoint(e);
if (position == -1) {
return true;
}
TextItem textItem = waitList.remove(position);
selectList.add(textItem);
if (waitItemClickListener != null) {
waitItemClickListener.onWaitItemClick(textItem.getText());
}
requestLayout();
invalidate();
return true;
}
private int checkTouchPoint(MotionEvent e) {
int x = (int) e.getX();
int y = (int) e.getY();
Region region = new Region();
for (int i = 0; i < waitList.size(); i++) {
TextItem textItem = waitList.get(i);
region.set(textItem.getLeft(), textItem.getTop(), textItem.getRight(), textItem.getBottom());
if (region.contains(x, y)) {
return i;
}
}
return -1;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d(TAG, "onScroll: " + distanceY);
if (contentHeight <= getMeasuredHeight()) {
return false;
}
if (offset == 0 && distanceY < 0) {
return false;
} else if (offset == contentHeight - getMeasuredHeight() && distanceY > 0) {
return false;
}
dy = (int) distanceY;
Log.d(TAG, "onScroll: 修改前的dy" + dy);
offset += dy;
if (offset < 0) {
dy += Math.abs(offset);
offset = 0;
} else if (offset > contentHeight - getMeasuredHeight()) {
dy -= offset - contentHeight + getMeasuredHeight();
offset = contentHeight - getMeasuredHeight();
}
Log.d(TAG, "onScroll: 修改后的dy" + dy);
scrollBy(0, dy);
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.d(TAG, "onLongPress: ");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.d(TAG, "onFling: ");
return false;
}
});
具体的代码如上,在点击事件中先去判断点中的是哪个备选的item,然后将其添加到选择区,同时如果被设置里监听,会触发对应监听.而在滑动事件中,主要处理了何时需要滑动,何时可以上滑,何时可以下滑以及滑动到定点时的特殊处理,逻辑都不难.
到这里,一个完整的类别选择view就完成了(从选择区删除回备选区功能,有兴趣可以自己参照着去实现下).