Android 自定義View(五)實現跑馬燈垂直滾動效果

一、前言

最近一直鞏固 Android 自定義 View 相關知識,以前都是閱讀一些理論性的文章,很少抽時間自己去實現一個自定義 View,項目中遇到問題就上 github 上去找效果。其實自定義 View 涉及到很多內容,只有親自動手完成幾個案例,才能對相關知識點有深入瞭解。

本文是對上篇文章的一個補充,股票 APP 列表底部有一個實時更新交易的跑馬燈效果,縱觀市面上很多產品都應用到這個效果,決定自己動手實現一下。

二、開發準備工作

1、先看效果圖

在這裏插入圖片描述

2、案例源碼下載

GitHub下載地址

CSDN下載地址

3、案例應用知識點

  1. ViewFlipper 控件基礎知識
  2. Android 動畫基礎知識
  3. 自定義 View 基礎知識
  4. 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 之自定義屬性方式,主要分以下幾個步驟:

  1. 根據集合 Size 和每頁顯示條目取餘“%”計算一共需要展示幾頁;
  2. 遍歷步驟 1 中獲取的頁數;
  3. 根據單行/多行顯示,遍歷每頁創建子 View 佈局;
  4. 調用 Adapter.onBindView()方法完成每個子 View 數據綁定;
  5. 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 狀態都會被調用,適合做最後的清理操作;

  1. onAttachedToWindow 被調用,即代表着 View 被添加到了一個繪製過的視圖樹中。
  2. onAttachedToWindow 和 onDetachedFromWindow 可以被調用多次。
  3. 當 View 被添加到已經繪製過的視圖樹上時,onAttachedToWindow 會被立即執行,接着 onVisibilityChanged 也會立即執行。
  4. 當 View 從視圖上移除時,如果 onAttachedToWindow 方法曾經執行過,那麼 onDetachedFromWindow 將會被執行。
  5. onVisibilityChanged 被調用的前提是 View 執行過 onAttachedToWindow 方法。
  6. 判斷 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 路,與你同在!

在這裏插入圖片描述

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