一、前言
自定義 View 是 Android 中高級工程師進階的必經之路,要想熟練掌握自定義 View 技能,View 繪製流程和 View 事件分發機制必須掌握的,開發過程中大多數情況下都能在網上找到類似的效果,可能修修改改也能滿足項目需求,但是一旦遇到比較棘手的問題,可能就會讓開發者很苦惱。
本篇文章是自定義 View 結合 View 事件分發實現一個賽事得分表效果。
如果對自定義 View 不熟悉的朋友可以參考以下文章:
二、準備工作
1、先看效果
2、案例源碼下載
3、應用知識點
- 自定義 View
- View 事件分發機制
- 設計模式-觀察者模式
4、思路分析
- 根據效果圖首先明確主頁面是一個列表;
- 頭部 Tab 欄可以水平滑動,所以父佈局使用 HorizontalScrollView 實現;
- 列表 Item 分 2 部分,一部分是[頭像和暱稱],另一部分是[積分];
- 頭部 Tab 欄水平滑動時,列表 Item 第二部分同步滑動;
- 熟悉事件分發的朋友都知道,看到這樣的佈局效果,肯定需要解決滑動衝突;
三、代碼實現
1、自定義橫向滾動控件
- 因爲上面已經分析,頭部 Tab 欄父佈局和 item 第二部分同步滑動,所以都採用橫向滾動控件完成,所以自定義橫向滾動控件,繼承 HorizontalScrollView 並重寫構造方法,這裏重寫了 3 個構造方法。
public class CustomHScrollView extends HorizontalScrollView {
ScrollViewObserver mScrollViewObserver = new ScrollViewObserver();
public CustomHScrollView(Context context) {
super(context);
}
public CustomHScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomHScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
- 定義外部可回調接口,當發生了滾動事件時接口,供外部訪問
public static interface OnScrollChangedListener {
public void onScrollChanged(int l, int t, int oldl, int oldt);
}
- 定義一個觀察者,用於滑動控件時,接收水平滑動的回調。並定義添加滾動監聽和移除滾動監聽的方法。
public static class ScrollViewObserver {
List<OnScrollChangedListener> mChangedListeners;
public ScrollViewObserver() {
super();
mChangedListeners = new ArrayList<OnScrollChangedListener>();
}
//添加滾動事件監聽
public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
mChangedListeners.add(listener);
}
//移除滾動事件監聽
public void RemoveOnScrollChangedListener(OnScrollChangedListener listener) {
mChangedListeners.remove(listener);
}
//回調
public void NotifyOnScrollChanged(int l, int t, int oldl, int oldt) {
if (mChangedListeners == null || mChangedListeners.size() == 0) {
return;
}
for (int i = 0; i < mChangedListeners.size(); i++) {
if (mChangedListeners.get(i) != null) {
mChangedListeners.get(i).onScrollChanged(l, t, oldl, oldt);
}
}
}
}
- 因爲父佈局是水平滾動 HorizontalScrollView,所以需要重寫 onScrollChanged()方法,監聽滑動變化。當觀察者 mScrollViewObserver 不爲 null 時,滑動監聽通知回調觀察者 ScrollViewObserver。
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//滾動時通知
if (mScrollViewObserver != null) {
mScrollViewObserver.NotifyOnScrollChanged(l, t, oldl, oldt);
}
super.onScrollChanged(l, t, oldl, oldt);
}
以上水平滾動 HorizontalScrollView 基本上已經完成,上面預留外部訪問接口:OnScrollChangedListener,在 Tab 欄水平滑動時,可以在 Adapter 中獲取滾動水平、垂直滾動位置,來設置 item 中第二部分控件位置。
2、完善列表
列表頁採用 RecyclerView 完成,這部分不做過多描述,感興趣的朋友可以下載源碼查看。
- 這裏對 RecyclerView.Adapter 中的處理,描述說明。在 ViewHolder 初始化控件時,將 Tab 欄的 ScrollView(被觀察者)和 item 中的 ScrollView(觀察者)完成訂閱。當 Tab 欄發生水平滾動時,實時通知觀察者完成界面刷新。
// item列表中的ScrollView
CustomHScrollView scrollView = itemView.findViewById(R.id.h_scrollView);
// Tab欄的ScrollView
CustomHScrollView headSrcrollView = mHead.findViewById(R.id.h_scrollView);
// 訂閱
headSrcrollView.AddOnScrollChangedListener(new OnScrollChangedListenerImp(scrollView));
-
在 Adapter 中新建 OnScrollChangedListenerImp 類,繼承自定義 CustomHScrollView 中的 OnScrollChangedListener 接口,上文提到,該接口是當發生了滾動事件時接口,供外部訪問。
-
實現 onScrollChanged()方法,調用 HorizontalScrollView.smoothScrollTo()方法實現 item 中 HorizontalScrollView 滾動位置與 Tab 欄滾動實時同步效果。
需要注意的是,這裏需要記錄一下初始化位置,避免因刷新數據引起錯亂。
class OnScrollChangedListenerImp implements CustomHScrollView.OnScrollChangedListener {
CustomHScrollView mScrollViewArg;
public OnScrollChangedListenerImp(CustomHScrollView scrollViewar) {
mScrollViewArg = scrollViewar;
}
@Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {
mScrollViewArg.smoothScrollTo(l, t);
//記錄滾動的起始位置,避免因刷新數據引起錯亂
if (n == 1) {
setPosData(oldl, oldt);
}
n++;
}
}
3、解決滑動衝突
完成以上步驟後,我們發現,ListView 的垂直滑動和和 ListView 中 item 水平 ScrollView 滑動發生衝突,如果需要解決這個問題,就需要應用到我們 View 事件分發機制了,如果對事件分發機制和流程還不熟悉的朋友,可以先學習下。
- 首先,當在表頭和 listView 控件上 Touch 時,我們都需要將事件分發給 ScrollView 處理,所以根據 View 事件分發機制,定義一個父佈局 InterceptScrollLinearLayout 繼承 LinearLayout,並且做時間攔截處理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
- 然後在 Activity 中創建 OnTouchListener 子類,重寫 onTouch()方法,並且讓父控件 ListView 控件分別調用 setOnTouchListener()方法註冊回調函數,以便在觸摸事件發送到此視圖時調用。
class MyTouchLinstener implements View.OnTouchListener {
@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
//當在表頭和listView控件上touch時,將事件分發給 ScrollView
mScrollView.onTouchEvent(arg1);
return false;
}
}
mHead.setOnTouchListener(new MyTouchLinstener());
recycler.setOnTouchListener(new MyTouchLinstener());
綜上,就已經完成文章開頭展示的效果圖樣式,希望本文對 Android 初學者有所幫助,在自定義 View 和 View 事件分發知識學習過程中可以有更深的理解。文章中只粘貼了部分代碼,需要完整代碼的可以下載代碼參考。