自定义view:类似今日头条的类别选择功能
来源:互联网 发布:linux配置web服务器 编辑:程序博客网 时间:2024/05/17 23:23
距离上一篇文章时间已经有点久远了,其实中间也写了几个好玩的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就完成了(从选择区删除回备选区功能,有兴趣可以自己参照着去实现下).
点击下载源码
- 自定义view:类似今日头条的类别选择功能
- 怎么实现类似"今日头条"app
- Viewpager实现今日头条顶部导航的功能
- 今日头条的首页
- 自定义View之仿今日头条颜色渐变指示器导航栏
- 类似今日头条网易新闻导航栏水平滑动的效果
- <Android 应用 之路> 一个类似今日头条的APP
- 仿今日头条的graidview拖动
- 仿今日头条的graidview拖动
- IT垂直领域的今日头条
- 模仿今日头条的4.6版
- 仿今日头条的(一)
- 今日头条的一条笔试题
- 今日头条的用户体验分析
- 今日头条的发家与困局
- 仿今日头条的夜间模式
- 仿今日头条的频道管理
- 仿今日头条的频道管理
- 编辑pdf文件及校准测量的方法
- 正确使用Block避免Cycle Retain和Crash
- 集合中contains的用法
- 微信公众帐号开发教程第9篇-QQ表情的发送与接收
- Android ViewGroup 触摸屏事件派发机制和源码分析
- 自定义view:类似今日头条的类别选择功能
- PHP实现java的hashCode方法
- java内部类的使用
- 蓝桥杯-基础训练-特殊的回文数(枚举)
- 仙岛求药(迷宫寻找最短路径)DFS
- 如何快速在ecplise模拟器上安装APK包
- 访问模型一 最简单的访问服务器
- 有关top命令中的%st,sar命令中的%steal
- 常见的四种连接池实现