Android 开发过程当中自定义 View 真的是无处不在,随随便便一个 UI 效果,都会用到自定义 View。前面三篇文章已经讲过自定义 View 的一些案例效果,相关类和 API,还有事件分发理论知识请自行充电。做者不喜欢讲一些原理性的东西,直接上效果和源码。
本篇文章本来和自定义 View 关系不大,做者强行自定义绘制了一个小控件,以符合最近的文章主题。本文是实现股票、证券列表联动效果。
舒适提示:股市有风险,投资需谨慎android
https://github.com/jaynm888/StockDemogit
根据效果图,咱们能够将布局拆解,分为如下独立模块:github
自定义 View 的基础知识这里不作回顾,若是对自定义 View 还不是很了解的朋友,能够查看以前的文章。
自定义 TextView,将效果图左上角的文本和小三角符号完成绘制工做,并设置一个背景效果。这里将属性直接在 Java 代码里设置了,建议使用自定义属性,方便在 XML 中设置。canvas
根据文本的尺寸和 Padding 值计算文本的宽度和高度,由于本案例中自定义 View 尺寸在 XML 中设置 wrap_content,因此主要看 switch 语句中 MeasureSpec.AT_MOST 节点,关于 MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、MeasureSpec.UNSPECIFIED 区别,请查看做者以前自定义 View 的系列文章。
测量成功后从新设置 View 尺寸:setMeasuredDimension(width, height);ide
/** * View尺寸测量 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 宽度测量 width = setMeasureSize(widthMeasureSpec, 1); // 高度测量 height = setMeasureSize(heightMeasureSpec, 2); // 设置测量后的尺寸 setMeasuredDimension(width, height); } int setMeasureSize(int measureSpec, int type) { int specSize = 0; int measurementSize = 0; int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); switch (mode) { case MeasureSpec.EXACTLY:// 精确尺寸或者最大值 specSize = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: if (type == 1) { measurementSize = rect.width() + getPaddingLeft() + getPaddingRight() + specSize + triangleSize; } else if (type == 2) { measurementSize = rect.height() + getPaddingTop() + getPaddingBottom(); } specSize = Math.min(measurementSize, size); break; } return specSize; }
绘制文本须要注意的,下图中红色的 Baseline 是基准线,紫色的 Top 是文字的最顶部,也就是在 drawText()中指定的 x 所对应,橙色的 Bottom 是文字的底部。
因此文本的高度:布局
距离 = 文字高度的一半 - 基线到文字底部的距离(也就是bottom) = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom // 绘制文本
Paint.FontMetrics fontMetrics = paint.getFontMetrics(); float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom; canvas.drawText(tabStr, getPaddingLeft(), height / 2 + distance, paint);
绘制三角形须要使用 Path 相关知识,具体相关 API 方法,请读者自行补习。
主要是肯定三角形的三个点 x、y 轴位置,而后调用 canvas.drawPath(path, paint)方法完成绘制工做。学习
//绘制三角形 Path path = new Path(); path.moveTo(rect.width() + specSize + getPaddingLeft(), height / 2 - triangleSize / 2);//三角形左下角位置坐标 path.lineTo(rect.width() + specSize + getPaddingLeft(), height / 2 + triangleSize / 2);//三角形右下角位置坐标 path.lineTo(rect.width() + specSize + getPaddingLeft() + triangleSize / 2, height / 2);//三角形顶部位置坐标 path.close(); canvas.drawPath(path, paint);
View 背景使用 layer-list 完成,这是平常开发中最经常使用的功能,常常可使用 shap 完成一些简单的背景效果,不须要每次都使用图片,并且还不会出现适配的苦恼。编码
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <shape> <solid android:color="@color/tabTextTitle" /> <corners android:topRightRadius="30dp" android:bottomRightRadius="30dp"/> </shape> </item> <!-- 只设置顶部、底部、右边边框 --> <item android:bottom="3px" android:right="3px" android:top="3px"> <shape android:shape="rectangle"> <solid android:color="#2A2720"/> <corners android:topRightRadius="30dp" android:bottomRightRadius="30dp"/> </shape> </item> </layer-list>
以上就完成了自定义 View 的所有工做,固然这不是本文的重点内容,只是顺带提一下自定义 View 的基本知识。code
@Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (viewListener != null) { viewListener.onScroll(l, t, oldl, oldt); } }
CustomizeScrollView 类很简单,没有作太多事情,在 XML 中直接引用完整类名便可。xml
布局 XML 这里就不所有贴出了,比较影响文章阅读性,感兴趣的朋友能够下载源码本身研究,主要讲解下 HorizontalScrollView+RecyclerView 嵌套问题。
若是直接在 HorizontalScrollView 中嵌套 RecyclerView,滑动时会出现内容显示不完整的状况,相关不少朋友在开发过程当中也遇到过这种问题。(Tab 栏一共有 7 个 item,可是指滑动到可见的 item,后面的没法滑动):
在 HorizontalScrollView 中嵌套 RecyclerView 须要注意内容显示不完整的问题,不能直接将 2 个布局嵌套,须要在 HorizontalScrollView 中添加一个 RelativeLayout 布局,而且设置属性:android:descendantFocusability="blocksDescendants",这样就能够完美解决嵌套致使内容显示不完整的问题。
<com.caobo.stockdemo.view.CustomizeScrollView android:id="@+id/headScrollView" android:layout_width="0dp" android:layout_height="50dp" android:layout_weight="7"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:descendantFocusability="blocksDescendants"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/headRecyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" /> </RelativeLayout> </com.caobo.stockdemo.view.CustomizeScrollView>
关于 descendantFocusability 属性简单介绍:
beforeDescendants:viewgroup会优先其子类控件而获取到焦点 afterDescendants:viewgroup只有当其子类控件不须要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接得到焦点.
/** * 保存列表ViewHolder集合 */ private List<ViewHolder> recyclerViewHolder = new ArrayList<>(); /** * 记录item滑动的位置,用于RecyclerView上下滚动时更新全部列表 */ private int offestX;
/** * 第一步:水平滑动item时,遍历全部ViewHolder,使得整个列表的HorizontalScrollView同步滚动 */ holder.mStockScrollView.setViewListener(new CustomizeScrollView.OnScrollViewListener() { @Override public void onScroll(int l, int t, int oldl, int oldt) { for (ViewHolder viewHolder : recyclerViewHolder) { if (viewHolder != holder) { viewHolder.mStockScrollView.scrollTo(l, 0); } } } });
/** * 第二步:水平滑动item时,接口回调到Tab栏的HorizontalScrollView,使得Tab栏跟随item滚动实时更新 */ if (onTabScrollViewListener != null) { onTabScrollViewListener.scrollTo(l, t); offestX = l; }
/** * 第三步:Tab栏HorizontalScrollView水平滚动时,遍历全部RecyclerView列表,并使其跟随滚动 */ headHorizontalScrollView.setViewListener(new CustomizeScrollView.OnScrollViewListener() { @Override public void onScroll(int l, int t, int oldl, int oldt) { List<StockAdapter.ViewHolder> viewHolders = mStockAdapter.getRecyclerViewHolder(); for (StockAdapter.ViewHolder viewHolder : viewHolders) { viewHolder.mStockScrollView.scrollTo(l, 0); } } });
/** * 第四步:RecyclerView垂直滑动时,遍历更新全部item中HorizontalScrollView的滚动位置,不然会出现item位置未发生变化状态 */ mContentRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); List<StockAdapter.ViewHolder> viewHolders = mStockAdapter.getRecyclerViewHolder(); for (StockAdapter.ViewHolder viewHolder : viewHolders) { viewHolder.mStockScrollView.scrollTo(mStockAdapter.getOffestX(), 0); } } });
自定义 View 实际上是一个须要常常去上手练习的过程,理论知识当然重要,可是若是不本身动手撸几个案例,依然没法熟练的掌握,因此给学习自定义 View 的朋友提个建议。
是否是很简单,其实这章内容没有什么难点,主要是对实现列表滑动以及联动的思路要清晰,其实编码不少时候,都是分析问题的思路很重要,只有思路明确,才能去一步一步完成功能。但愿本文对你 Android 开发之路有所帮助!
Android 自定义View篇—时钟表盘效果
Android 自定义View篇—环形进度条效果
Android 自定义View篇—体育赛事积分表效果