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篇—體育賽事積分表效果