Android 自定义View(四)实现股票自选列表滑动效果

1、前言

Android 开发过程当中自定义 View 真的是无处不在,随随便便一个 UI 效果,都会用到自定义 View。前面三篇文章已经讲过自定义 View 的一些案例效果,相关类和 API,还有事件分发理论知识请自行充电。做者不喜欢讲一些原理性的东西,直接上效果和源码。android

本篇文章本来和自定义 View 关系不大,做者强行自定义绘制了一个小控件,以符合最近的文章主题。本文是实现股票、证券列表联动效果。git

2、开发准备工做

一、实现效果图

在这里插入图片描述
在这里插入图片描述

二、案例源码下载

点击下载程序员

三、案例应用知识点

  1. 自定义 View 基础知识(测量、Canvas、Paint、Path)github

  2. HorizontalScrollView 滚动事件web

  3. RecyclerView 嵌套 HorizontalScrollView 冲突处理canvas

  4. 接口回调知识微信

  5. 自定义 layer-list 和 shape编辑器

四、案例思路分析

根据效果图,咱们能够将布局拆解,分为如下独立模块:ide

  1. 效果图总体布局是一个 Tab 栏 + RecyclerView 列表组成布局

  2. RecyclerView 列表 item 布局和 Tab 栏一致

  3. Tab 栏水平滑动时,RecyclerView 列表同步滑动

  4. RecyclerView 列表 item 滑动时,整个列表跟滚动,而且 Tab 栏也同步滚动更新

3、代码实现

一、自定义 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);

/**  * 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">
 <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 的基本知识。

二、自定义 CustomizeScrollView

  • 自定义 CustomizeScrollView 继承 HorizontalScrollView。
  • 重写 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会覆盖子类控件而直接得到焦点. 复制代码

四、主列表 Adapter

  1. 完成以上工做后,剩下主要内容都在主列表页面的适配器中完成,定义 ViewHolder 集合和记录滑动 X 轴变量:
/**  * 保存列表ViewHolder集合  */ private List<ViewHolder> recyclerViewHolder = new ArrayList<>(); /**  * 记录item滑动的位置,用于RecyclerView上下滚动时更新全部列表  */ private int offestX; 复制代码
  1. 在 onBindViewHolder()方法中初始化数据,并将 ViewHolder 添加到集合中,而后水平滑动单个 Item 时,遍历 ViewHolder 使得整个列表的 HorizontalScrollView 同步滚动。
/**  * 第一步:水平滑动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);  }  }  } }); 复制代码
  1. 接上上面步骤,在水平滑动 Item 时,接口回调到 Tab 栏 HorizontalScrollView,在 MainActivity 中更新 Tab 栏滚动位置,而且记录滑动的 X 轴位置(用于在后面 RecyclerView 同步 item 时使用)。
/**  * 第二步:水平滑动item时,接口回调到Tab栏的HorizontalScrollView,使得Tab栏跟随item滚动实时更新  */ if (onTabScrollViewListener != null) {  onTabScrollViewListener.scrollTo(l, t);  offestX = l; } 复制代码
  1. 完成上面步骤后,就基本已经实如今 RecyclerView 列表水平滑动,Tab 栏和其余 Item 同步更新的效果,接下面须要完成 Tab 水平滑动时,使得 RecyclerView 同步更新。 根据 Adpater 中 ViewHolder 集合遍历全部 holder 对象,并给 RecyclerView 中 item 每一个 CustomizeScrollView 设置滚动方法 scrollTo()。由于水平滚动,不会涉及 Y 轴的位置,因此案例中都只设置了 X 轴的值。
/**  * 第三步: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);  }  } }); 复制代码
  1. 其实这一步是为了解决一个 Bug,当完成以上内容后,已经可使用了,可是在上下滑动 item 的时候,发现未第一次显示 item 当中 HOrizontalScrollView 位置并未发生变化,因此在 RecyclerView 中添加 addOnScrollListener()添加,该方法在 RecyclerView 上下滑动时会监听,和第三步的作法比较相似,遍历 ViewHolder,获取 Adapter 中保存的 X 轴滑动位置变量 OffestX 完成 item 中 CustomizeScrollView 的滚动位置。
/**  * 第四步: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);  }  } }); 复制代码

4、总结

自定义 View 实际上是一个须要常常去上手练习的过程,理论知识当然重要,可是若是不本身动手撸几个案例,依然没法熟练的掌握,因此给学习自定义 View 的朋友提个建议。

个人微信:Jaynm888

欢迎点评,诚邀 Android 程序员加入微信交流群,公众号回复“加群”或者添加我微信拉你入群

相关文章
相关标签/搜索