一、前言
最近一直鞏固 Android 自定義 View 相關知識,以前都是閱讀一些理論性的文章,很少抽時間自己去實現一個自定義 View,項目中遇到問題就上 github 上去找效果。其實自定義 View 涉及到很多內容,只有親自動手完成幾個案例,才能對相關知識點有深入瞭解。
本文是對上篇文章的一個補充,股票 APP 列表底部有一個實時更新交易的跑馬燈效果,縱觀市面上很多產品都應用到這個效果,決定自己動手實現一下。
二、開發準備工作
1、先看效果圖
2、案例源碼下載
3、案例應用知識點
- ViewFlipper 控件基礎知識
- Android 動畫基礎知識
- 自定義 View 基礎知識
- Activity 啓動流程基礎知識
三、ViewFlipper 介紹
ViewFlipper 是 Android 中的基礎控件,可能在一般開發中很少有人用到,所以很多開發者感覺對這個控件很陌生,在控件圈裏更遠遠沒有 ViewPager 出名,但是 ViewFlipper 用法很簡單,效果卻很不錯。
ViewFlipper 繼承自 ViewAnimator,而 ViewAnimator 又是繼承自 FrameLayout,而 FrameLayout 就是平時基本上只顯示一個子視圖的佈局,由於 FrameLayout 下不好確定子視圖的位置,所以很多情況下子視圖之前存在相互遮擋,這樣就造成了很多時候我們基本上只要求 FrameLayout 顯示一個子視圖,然後通過某些控制來實現切換。正好,ViewFlipper 幫我們實現了這個工作,我們需要做的就是,選擇恰當的時機調用其恰當的方法即可實質上只是封裝了一些 ViewAnimator 的方法來調用,真正執行操作的是 ViewAnimator。
ViewFlipper 相關屬性介紹
方法 | 描述 |
---|---|
isFlipping | 判斷 View 切換是否正在進行 |
setFilpInterval | 設置 View 之間切換的時間間隔 |
startFlipping | 開始 View 的切換,而且會循環進行 |
stopFlipping | 停止 View 的切換 |
setOutAnimation | 設置切換 View 的退出動畫 |
setInAnimation | 設置切換 View 的進入動畫 |
showNext | 顯示 ViewFlipper 裏的下一個 View |
showPrevious | 顯示 ViewFlipper 裏的上一個 View |
四、代碼實現
上面已經介紹了 ViewFlipper 控件基礎知識,如果要實現跑馬燈效果,建議自定義 ViewFlipper 實現自己的需求。本文使用自定義 ViewFlipper 的方式實現跑馬燈垂直滾動效果。
1、自定義 ViewFlipper 屬性
設置以下屬性,建議使用自定義屬性方式,便於後期修改和 XML 中使用。
/**
* 是否單行顯示
*/
private boolean isSingleLine;
/**
* 輪播間隔
*/
private int interval = 3000;
/**
* 動畫時間
*/
private int animDuration = 1000;
/**
* 一次性顯示item數目
*/
private int itemCount = 1;
2、創建動畫
- anim_marquee_in.xml 進入動畫:
- Y 軸位置從下面 100%移動到位置 0,動畫持續 300 毫秒
- 漸變透明度動畫效果由 0.0 到 1.0,動畫持續 500 毫秒
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromYDelta="100%p"
android:toYDelta="0"/>
<alpha
android:duration="500"
android:fromAlpha="0.0"
android:toAlpha="1.0"/>
</set>
-
anim_marquee_out.xml 退出動畫:
- Y 軸位置從下面 0 移動到位置-100%,動畫持續 400 毫秒
- 漸變透明度動畫效果由 1.0 到 0.0,動畫持續 500 毫秒
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="400"
android:fromYDelta="0"
android:toYDelta="-100%p"/>
<alpha
android:duration="500"
android:fromAlpha="1.0"
android:toAlpha="0.0"/>
</set>
3、初始化動畫
完成上面 2 步驟後,在自定義 ViewFlipper 中,完成動畫的初始化工作。
private void initView(Context context) {
// 動畫
Animation animIn = AnimationUtils.loadAnimation(context, R.anim.anim_marquee_in);
Animation animOut = AnimationUtils.loadAnimation(context, R.anim.anim_marquee_out);
// 設置動畫
animIn.setDuration(animDuration);
animOut.setDuration(animDuration);
// 設置切換View的進入動畫
setInAnimation(animIn);
// 設置切換View的退出動畫
setOutAnimation(animOut);
// 設置View之間切換的時間間隔
setFlipInterval(interval);
// 設置在測量時是考慮所有子項,還是隻考慮可見或不可見狀態的子項。
setMeasureAllChildren(false);
}
4、創建 Adapter
因爲跑馬燈數據基本都是集合形式存在,所以採用 Adapter 模式,定義數據刷新回調接口 OnDataChangedListener,在 CustomizeMarqueeView 中接收回調並刷新數據。
public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
mOnDataChangedListener = onDataChangedListener;
}
public void notifyDataChanged() {
if (mOnDataChangedListener != null) {
mOnDataChangedListener.onChanged();
}
}
public interface OnDataChangedListener {
void onChanged();
}
定義創建子 View 佈局方法和綁定數據方法
/**
* @param parent
* @return 自定義跑馬燈的Item佈局
*/
public View onCreateView(CustomizeMarqueeView parent) {
return LayoutInflater.from(parent.getContext()).inflate(R.layout.marqueeview_item, null);
}
/**
* 更新數據
* @param view
* @param position
*/
public void onBindView(View view, int position) {
}
5、創建佈局和綁定數據
根據 List 集合設置 View 數據,這裏主要使用自定義 View 之自定義屬性方式,主要分以下幾個步驟:
- 根據集合 Size 和每頁顯示條目取餘“%”計算一共需要展示幾頁;
- 遍歷步驟 1 中獲取的頁數;
- 根據單行/多行顯示,遍歷每頁創建子 View 佈局;
- 調用 Adapter.onBindView()方法完成每個子 View 數據綁定;
- addView()將所有子 View 添加到 ViewFlipper 中;
private void setData() {
removeAllViews();
int currentIndex = 0;
// 計算數據展示完畢需要幾頁,根據總條目%每頁條目計算得出
int loopCount = mMarqueeViewBaseAdapter.getItemCount() % itemCount == 0 ?
mMarqueeViewBaseAdapter.getItemCount() / itemCount :
mMarqueeViewBaseAdapter.getItemCount() / itemCount + 1;
// 遍歷動態添加每頁的View
for (int i = 0; i < loopCount; i++) {
// 每頁單條展示
if (isSingleLine) {
LinearLayout parentView = new LinearLayout(getContext());
parentView.setOrientation(LinearLayout.VERTICAL);
parentView.setGravity(Gravity.CENTER);
parentView.removeAllViews();
View view = mMarqueeViewBaseAdapter.onCreateView(this);
parentView.addView(view);
if (currentIndex < mMarqueeViewBaseAdapter.getItemCount()) {// 綁定View
mMarqueeViewBaseAdapter.onBindView(view, currentIndex);
}
currentIndex = currentIndex + 1;
addView(parentView);
} else {
LinearLayout parentView = new LinearLayout(getContext());
parentView.setOrientation(LinearLayout.VERTICAL);
parentView.setGravity(Gravity.CENTER);
parentView.removeAllViews();
// 每頁顯示多少條,就遍歷添加幾個子View
for (int j = 0; j < itemCount; j++) {
View view = mMarqueeViewBaseAdapter.onCreateView(this);
parentView.addView(view);
currentIndex = getRealPosition(j, currentIndex);
if (currentIndex < mMarqueeViewBaseAdapter.getItemCount()) {
mMarqueeViewBaseAdapter.onBindView(view, currentIndex);
}
}
addView(parentView);
}
}
}
6、Activity 啓動過程
有的朋友會很好奇這跟 Activity 啓動過程有什麼關係?
因爲 ViewFlipper 屬性看到需要手動調用 startFlipping()方法和 stopFlipping()完成 View 切換和循環執行。所以考慮到 View 性能和使用效果,我們重寫了 View 的三個方法,實現開啓和關閉。
-
onVisibilityChanged 是否調用,依賴於 View 是否執行過 onAttachedToWindow 方法。也就是 View 是否被添加到 Window 上。
-
onAttachedToWindow 方法是在 Activity resume 的時候被調用的,也就是 Activity 對應的 window 被添加的時候,且每個 view 只會被調用一次,父 view 的調用在前,不論 view 的 visibility 狀態都會被調用,適合做些 view 特定的初始化操作;
-
onDetachedFromWindow 方法是在 Activity destroy 的時候被調用的,也就是 Activity 對應的 window 被刪除的時候,且每個 view 只會被調用一次,父 view 的調用在後,也不論 view 的 visibility 狀態都會被調用,適合做最後的清理操作;
- onAttachedToWindow 被調用,即代表着 View 被添加到了一個繪製過的視圖樹中。
- onAttachedToWindow 和 onDetachedFromWindow 可以被調用多次。
- 當 View 被添加到已經繪製過的視圖樹上時,onAttachedToWindow 會被立即執行,接着 onVisibilityChanged 也會立即執行。
- 當 View 從視圖上移除時,如果 onAttachedToWindow 方法曾經執行過,那麼 onDetachedFromWindow 將會被執行。
- onVisibilityChanged 被調用的前提是 View 執行過 onAttachedToWindow 方法。
- 判斷 View 是否執行過 onAttachedToWindow 的依據是 View 裏的 mAttachInfo 對象不爲空。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (VISIBLE == visibility) {
startFlipping();
} else if (GONE == visibility || INVISIBLE == visibility) {
stopFlipping();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startFlipping();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopFlipping();
}
7、Activity 中使用
只需要在 XML 中加載自定義 View 佈局,然後在 Activity 中獲取 View,加載數據集合即可。
marquessViewAdapter = new MarquessViewAdapter(this);
mMarqueeView.setItemCount(1);
mMarqueeView.setSingleLine(true);
mMarqueeView.setAdapter(marquessViewAdapter);
marquessViewAdapter.setMessageBeans(messageBeans);
結合上一篇博文的最終效果圖至上:
五、總結
以上就完美實現了跑馬燈效果,通過自定義 View 方式,結合動畫屬性。代碼可以直接在項目中使用,只需要根據自己項目效果更改 item 的佈局就好。本篇文章已經是自定義 View 實戰案例的第五篇,雖然都是一些簡單效果,但是能將自定義 View 相關知識:View 繪製流程、View 測量、View 事件分發做一個系統化的深入。希望本文能對初學自定義 View 的朋友有所幫助。
我是 Jaynm,一個再互聯網苟且偷生的 Android 碼農,漫漫 Android 路,與你同在!