Android 开发过程中自定义 View 真的是无处不在,随随便便一个 UI 效果,都会用到自定义 View。前面三篇文章已经讲过自定义 View 的一些案例效果,相关类和 API,还有事件分发理论知识请自行充电。作者不喜欢讲一些原理性的东西,直接上效果和源码。
本篇文章原本和自定义 View 关系不大,作者强行自定义绘制了一个小控件,以符合最近的文章主题。本文是实现股票、证券列表联动效果,
自定义 View 基础知识(测量、Canvas、Paint、Path)
HorizontalScrollView 滚动事件
RecyclerView 嵌套 HorizontalScrollView 冲突处理
接口回调知识
自定义 layer-list 和 shape
根据效果图,我们可以将布局拆解,分为以下独立模块:
效果图整体布局是一个 Tab 栏 + RecyclerView 列表组成
RecyclerView 列表 item 布局和 Tab 栏一致
Tab 栏水平滑动时,RecyclerView 列表同步滑动
RecyclerView 列表 item 滑动时,整个列表跟滚动,并且 Tab 栏也同步滚动更新
自定义 View 的基础知识这里不做回顾,如果对自定义 View 还不是很了解的朋友,可以查看之前的文章。
自定义 TextView,将效果图左上角的文本和小三角符号完成绘制工作,并设置一个背景效果。这里将属性直接在 Java 代码里设置了,建议使用自定义属性,方便在 XML 中设置。
1. 测量 TextView 尺寸
根据文本的尺寸和 Padding 值计算文本的宽度和高度,因为本案例中自定义 View 尺寸在 XML 中设置 wrap_content,所以主要看 switch 语句中 MeasureSpec.AT_MOST 节点, 关于 MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、MeasureSpec.UNSPECIFIED 区别,请查看作者之前自定义 View 的系列文章。
测量成功后重新设置 View 尺寸:setMeasuredDimension(width, height);
* 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 ;2. 绘制文本
绘制文本需要注意的,下图中红色的 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);
3. 绘制三角形
绘制三角形需要使用 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);
4. 定义自定义 View 边框
View 背景使用 layer-list 完成,这是日常开发中最常用的功能,经常可以使用 shap 完成一些简单的背景效果,不需要每次都使用图片,而且还不会出现适配的苦恼。
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<shape>
<solid android:color="@color/tabTextTitle" />
<corners android:topRightRadius="30dp"
android:bottomRightRadius="30dp"/>
</shape>
</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 的基本知识。
重写 onScrollChanged()方法,主要用于监听 ScrollView 滑动。
定义回调接口 OnScrollViewListener,用于监听 onScrollChanged()方法滚动回调。
@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 这里就不全部贴出了,比较影响文章阅读性,感兴趣的朋友可以下载源码自己研究,主要讲解下 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会覆盖子类控件而直接获得焦点.
自定义View其实是一个需要经常去上手练习的过程,理论知识固然重要,但是如果不自己动手撸几个案例,依然无法熟练的掌握,所以给学习自定义View的朋友提个建议。
是不是很简单,其实这章内容没有什么难点,主要是对实现列表滑动以及联动的思路要清晰,其实编码很多时候,都是分析问题的思路很重要,只有思路明确,才能去一步一步完成功能。希望本文对你 Android 开发之路有所帮助!
我是一名 Android 程序员,我喜欢编码,我喜欢分享,我喜欢 Android 。