Android 自定義View(四)實現股票自選列表滑動效果

一、前言

Android 開發過程中自定義 View 真的是無處不在,隨隨便便一個 UI 效果,都會用到自定義 View。前面三篇文章已經講過自定義 View 的一些案例效果,相關類和 API,還有事件分發理論知識請自行充電。作者不喜歡講一些原理性的東西,直接上效果和源碼。

Android 自定義 View(一)實現時鐘錶盤效果
Android 自定義 View(二)實現環形進度條
Android 自定義 View(三)實現體育賽事積分表效果

本篇文章原本和自定義 View 關係不大,作者強行自定義繪製了一個小控件,以符合最近的文章主題。本文是實現股票、證券列表聯動效果,

二、開發準備工作

1、先上效果圖

在這裏插入圖片描述

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 欄也同步滾動更新

三、代碼實現

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);

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

2、自定義 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 中直接引用完整類名即可。

3、主頁面佈局

佈局 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會覆蓋子類控件而直接獲得焦點.

4、主列表 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);
        }
    }
});

四、總結

自定義View其實是一個需要經常去上手練習的過程,理論知識固然重要,但是如果不自己動手擼幾個案例,依然無法熟練的掌握,所以給學習自定義View的朋友提個建議。

本章內容是不是很簡單,其實這章內容沒有什麼難點,主要是對實現列表滑動以及聯動的思路要清晰,其實編碼很多時候,都是分析問題的思路很重要,只有思路明確,才能去一步一步完成功能。

我是一名 Android 程序員,我喜歡編碼,我喜歡分享,我喜歡 Android 。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章