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 程序員加入微信交流羣,公衆號回覆「加羣」或者添加我微信拉你入羣

相關文章
相關標籤/搜索