# 一、前言
Android 开发过程中自定义 View 真的是无处不在,随随便便一个 UI 效果,都会用到自定义 View。前面三篇文章已经讲过自定义 View 的一些案例效果,相关类和 API,还有事件分发理论知识请自行充电。作者不喜欢讲一些原理性的东西,直接上效果和源码。
本篇文章原本和自定义 View 关系不大,作者强行自定义绘制了一个小控件,以符合最近的文章主题。本文是实现股票、证券列表联动效果,
# 二、开发准备工作
## 1、先上效果图
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605181908679.gif#pic_center)
## 2、案例源码下载
## 3、案例应用知识点
1. 自定义 View 基础知识(测量、Canvas、Paint、Path)
2. HorizontalScrollView 滚动事件
3. RecyclerView 嵌套 HorizontalScrollView 冲突处理
4. 接口回调知识
5. 自定义 layer-list 和 shape
## 4、案例思路分析
根据效果图,我们可以将布局拆解,分为以下独立模块:
1. 效果图整体布局是一个 Tab 栏 + RecyclerView 列表组成
2. RecyclerView 列表 item 布局和 Tab 栏一致
3. Tab 栏水平滑动时,RecyclerView 列表同步滑动
4. RecyclerView 列表 item 滑动时,整个列表跟滚动,并且 Tab 栏也同步滚动更新
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWdrci5jbi1iai51ZmlsZW9zLmNvbS8yY2M1YzI5ZC0yMGZmLTRmNjUtOWQ2Ni0xNTY5MDkyNTM0ODcucG5n?x-oss-process=image/format,png#pic_center)
# 三、代码实现
## 1、自定义 TextView
自定义 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);
```java
/**
* 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 是文字的底部。
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWdrci5jbi1iai51ZmlsZW9zLmNvbS84M2RlMThkNC0xYzZmLTQ1OWEtYTg5My1iMmMyYzZjODE0ZGIucG5n?x-oss-process=image/format,png#pic_center)
**所以文本的高度:**
```java
距离 = 文字高度的一半 - 基线到文字底部的距离(也就是bottom) =
(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom
```
```java
// 绘制文本
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)方法完成绘制工作。
```java
//绘制三角形
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 完成一些简单的背景效果,不需要每次都使用图片,而且还不会出现适配的苦恼。
```xml
<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 的基本知识。
## 2、自定义 CustomizeScrollView
- 自定义 CustomizeScrollView 继承 HorizontalScrollView。
* 重写 onScrollChanged()方法,主要用于监听 ScrollView 滑动。
* 定义回调接口 OnScrollViewListener,用于监听 onScrollChanged()方法滚动回调。
```java
@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 中直接引用完整类名即可。
## 3、主页面布局
布局 XML 这里就不全部贴出了,比较影响文章阅读性,感兴趣的朋友可以下载源码自己研究,主要讲解下 **HorizontalScrollView+RecyclerView 嵌套问题**。
如果直接在 HorizontalScrollView 中嵌套 RecyclerView,滑动时会出现内容显示不完整的情况,相关很多朋友在开发过程中也遇到过这种问题。(Tab 栏一共有 7 个 item,但是指滑动到可见的 item,后面的无法滑动):
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWdrci5jbi1iai51ZmlsZW9zLmNvbS8zZTY3Zjk2OS0xMmEwLTRiZTUtODg5Yy0xMTQwYTY1MDIwNzcuZ2lm#pic_center)
在 HorizontalScrollView 中嵌套 RecyclerView 需要注意内容显示不完整的问题,**不能直接将 2 个布局嵌套,需要在 HorizontalScrollView 中添加一个 RelativeLayout 布局,并且设置属性:android:descendantFocusability="blocksDescendants"**,这样就可以完美解决嵌套导致内容显�