搶購倒數計時自定義控制元件的實現與優化

vivo網際網路技術發表於2021-04-20

一、 前言

隨著網購的持續發展,搶購類倒數計時在各類電商應用中已十分常見,這種設計可以提高使用者的點選率和下單率等。

但是國內的電商應用大部分都僅支援中文,不適配其他的語言,因此當倒數計時與其他文案處於同一行展示時,無需考慮倒數計時的展示方式。在海外應用中,由於需要適配各種語言,有些小語種的文案較長,因此當倒數計時和其他文案處於同一行展示時,需要充分考慮多語言的適配,如何優雅地完成倒數計時自適應顯示是一個值得深思的問題。

為進一步優化倒數計時效果,我們為倒數計時增加了數字滾動動畫,如下圖所示。倒數計時的功能必然會帶來效能的消耗,如何避免倒數計時帶來的效能問題,本文也將給出相應的解決方案。

二、 實現倒數計時基本功能

2.1 需求與原理分析

該控制元件預期展現兩種狀態,距離活動開始還有X天XX:XX:XX 和距離活動結束還有X天XX:XX:XX,因此需要一個活動狀態屬性,並通過這個活動開始與否的屬性設定時間前的文案。具體時間時分秒之間相互獨立,因此將它們拆分成獨立的textview進行處理。

倒數計時控制元件的核心是計時器,安卓中已經有現成的CountDownTimer類可供使用以實現倒數計時功能。此外,還需要實現一些監聽的介面。

2.2 具體實現

2.2.1 回撥監聽介面設計

首先,定義回撥介面

public interface OnCountDownTimerListener {
    /**
     * 倒數計時正在進行時呼叫的方法
     *
     * @param millisUntilFinished 剩餘的時間(毫秒)
     */
    void onRemain(long millisUntilFinished);
 
    /**
     * 倒數計時結束
     */
    void onFinish();
 
    /**
     * 每過一分鐘呼叫的方法
     */
    void onArrivalOneMinute();
 
}

在該介面中定義三個方法:

onRemain(long millisUntilFinished):倒數計時進行中回撥的方法,用於後續功能的擴充

onFinish():倒數計時結束回撥,用於活動狀態的切換和計時的暫停等

onArrivalOneMinute():每過一分鐘回撥,用於定時上報的埋點

2.2.2 view的構建與繫結

其次,初始化自定義view,基於實際開發需求,將整個控制元件細分為修飾文案、天數、時、分、秒等幾個獨立的textview,並在自定義BaseCountDownTimerView中初始化:

private void init() {
     mDayTextView = findViewById(R.id.days_tv);
     mHourTextView = findViewById(R.id.hours_tv);
     mMinTextView = findViewById(R.id.min_tv);
     mSecondTextView = findViewById(R.id.sec_tv);
     mHeaderText = findViewById(R.id.header_tv);
     mDayText = findViewById(R.id.new_arrival_day);
 }

2.2.3 構建內部使用的私有方法

首先構造設定剩餘時間的方法,入參是剩餘的毫秒數,在方法內部將時間轉化為具體的天時分秒,並將結果賦予給textview

 private void setSecond(long millis) {
 
     long day = millis / ONE_DAY;
     long hour = millis / ONE_HOUR - day * 24;
     long min = millis / ONE_MIN - day * 24 * 60 - hour * 60;
     long sec = millis / ONE_SEC - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60;
 
     String second = (int) sec + ""; // 秒
     String minute = (int) min + ""; // 分
     String hours = (int) hour + ""; // 時
     String days = (int) day + ""; //天
 
     if (hours.length() == 1) {
         hours = "0" + hours;
     }
     if (minute.length() == 1) {
         minute = "0" + minute;
     }
     if (second.length() == 1) {
         second = "0" + second;
     }
 
     if (day == 0) {
         mDayTextView.setVisibility(GONE);
         mDayText.setVisibility(GONE);
     } else {
         setDayText(day);
         mDayTextView.setVisibility(VISIBLE);
         mDayText.setVisibility(VISIBLE);
     }
 
     mDayTextView.setText(days);
 
     if (mFirstSetTimer) {
         mHourTextView.setInitialNumber(hours);
         mMinTextView.setInitialNumber(minute);
         mSecondTextView.setInitialNumber(second);
         mFirstSetTimer = false;
     } else {
         mHourTextView.flipNumber(hours);
         mMinTextView.flipNumber(minute);
         mSecondTextView.flipNumber(second);
     }
 }

需要注意的是,當單位時間為個位數時,為了視覺效果的統一,要在數字前加“0”進行補位。

其次,構建一個建立倒數計時的方法,其程式碼如下:

private void createCountDownTimer(final int eventStatus) {
       if (mCountDownTimer != null) {
           mCountDownTimer.cancel();
       }
       mCountDownTimer = new CountDownTimer(mMillis, 1000) {
           @Override
           public void onTick(long millisUntilFinished) {
               //策劃要求:倒數計時為00:00:01時,活動狀態重新整理,倒數計時不展示00:00:00這個狀態
               if (millisUntilFinished >= ONE_SEC) {
                   setSecond(millisUntilFinished);
                   //當活動狀態為進行中時,每隔一分鐘呼叫一次回撥
                   if (eventStatus == HomeItemViewNewArrival.EVENT_START) {
                       mArrivalOneMinuteFlag--;
                       if (mArrivalOneMinuteFlag == Constant.ZERO) {
                           mArrivalOneMinuteFlag = Constant.SIXTY;
                           mOnCountDownTimerListener.onArrivalOneMinute();
                       }
                   }
               }
           }
 
           @Override
           public void onFinish() {
               mOnCountDownTimerListener.onFinish();
           }
       };
   }

在該方法中,建立一個倒數計時例項CountDownTimer,CountDownTimer() 有兩個引數,分別是剩餘的總時間和重新整理間隔。

在例項的onTick()方法中,呼叫setSecond()方法在每次間隔時間(也就是1s)後定期重新整理view,完成倒數計時控制元件的更新。此外,產品中還有一個一分鐘定期上報埋點的需求,也可以在onTick()方法中完成。在實際專案事件中,若有定時的任務需求,也可在該方法中自由設定。最後,還需重寫該CountDownTimer的onFinish()方法,觸發listener介面裡的onFinish()

2.2.4 構建公有方法供外部使用

首先是設定倒數計時的監聽事件:

public void setDownTimerListener(OnCountDownTimerListener listener) {
    this.mOnCountDownTimerListener = listener;
}

其次是外露一個設定初始時間和活動開始或結束文案的方法:

public void setDownTime(long millis) {
    this.mMillis = millis;
}
 
 
public void setHeaderText(int eventStatus) {
    if (eventStatus == HomeItemViewNewArrival.EVENT_NOT_START) {
        mHeaderText.setText("Start in");
    } else {
        mHeaderText.setText("Ends in");
    }
}

最後,也是最重要的,需要給倒數計時類設計開始與取消倒數計時的方法:

public void startDownTimer(int eventStatus) {
        mArrivalOneMinuteFlag = Constant.SIXTY;
        mFirstSetTimer = true;
        //設定需要倒數計時的初始值
        setSecond(mMillis);
        createCountDownTimer(eventStatus);// 建立倒數計時
        mCountDownTimer.start();
    }
 
    public void cancelDownTimer() {
        mCountDownTimer.cancel();
    }

在開始倒數計時的方法中,初始化倒數計時的初始值並建立倒數計時,最後呼叫CountDownTimer例項的start()方法開始倒數計時。在取消的方法中,直接呼叫CountDownTimer例項的cancel()方法取消倒數計時。

2.3 倒數計時類的實際呼叫

實際呼叫倒數計時控制元件時,只需在具體佈局中新增該倒數計時類佈局,在呼叫的類中例項化BaseCountDownTimerView。接著,使用例項的setDownTime()、setHeaderText()初始化資料,使用setDownTimerListener()給view例項設定監聽。

最後呼叫startDownTimer()開啟倒數計時。

if (view != null) {
            view.setDownTime(mDuration);
            view.setHeaderText(mEventStatus);
            view.startDownTimer(mEventStatus);
            view.setDownTimerListener(new BaseCountDownTimerView.OnCountDownTimerListener() {
                @Override
                public void onRemain(long millisUntilFinished) {
 
                }
 
                @Override
                public void onFinish() {
                    view.cancelDownTimer();
                    if (bean.mNewArrivalType == TYPE_EVENT && mEventStatus == EVENT_START) {
                        mEventStatus = EVENT_END;
                        //活動狀態之前為進行中,倒數計時變為0,如果還有下一個活動/新品,則重新整理為下一個活動/新品的資料
                        refreshNewArrivalBeanDate(bean);
                        onBindView(bean, 1, true, null);
                    } else {
                        setEventStatus(bean);
                    }
                }
 
                @Override
                public void onArrivalOneMinute() {
 
                }
            });

三、實現倒數計時整體佈局

3.1 需求描述

在多語言環境或者不同螢幕條件下,某些語種的控制元件長度過長,需要自適應控制元件進行折行顯示以適應UI規範

3.2 實施方案

原本考慮只例項化一個自定義倒數計時控制元件的物件,但是在設計物件佈局的過程中發現,一個物件不方便同時實現在行尾展示或折行後在第二行行首顯示。因此,本文采用了在佈局的時候同時預置兩個倒數計時物件的方法,一個物件位於行尾,另一個位於第二行的行首。

在measure過程中,如果測量得到控制元件的寬度大於某一個寬度閾值,則初始化次行行首的view,並將行尾的view可見狀態置為Gone,若小於某一個寬度閾值,則初始化行尾的view,並將次行行首的view可見狀態置為Gone

首先來看一看xml佈局檔案,以下是標題加倒數計時位於行尾的一個整體佈局檔案main_view_header_new_arrival

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/qb_px_48">
 
    <com.example.website.general.ui.widget.TextView
        android:id="@+id/new_arrival_txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_centerInParent="true"
        android:layout_marginStart="@dimen/qb_px_20"
        android:text="@string/new_arrival"
        android:textColor="@color/common_color_de000000"
        android:textSize="@dimen/qb_px_16"
        android:textStyle="bold" />
 
    <com.example.website.widget.BaseCountDownTimerView
        android:id="@+id/count_down_timer_short"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentEnd="true"
        android:layout_marginEnd="@dimen/qb_px_20"
        android:gravity="center_vertical" />
</RelativeLayout>

它的實際展示效果如下圖所示

但是此佈局只能展示單行能展示所有內容的情況,因此還需要在此佈局上擴充雙行展示的情況,再看一看main_list_item_home_new_arrival的佈局

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    tools:parentTag="android.widget.LinearLayout">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
 
        <include layout="@layout/main_view_header_new_arrival"/>
 
        <com.example.website.widget.BaseCountDownTimerView
            android:id="@+id/count_down_timer_long"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_marginStart="@dimen/qb_px_20"
            android:layout_marginTop="@dimen/qb_px_n_4"
            android:layout_marginEnd="@dimen/qb_px_20"
            android:layout_marginBottom="@dimen/qb_px_8"
            android:gravity="center_vertical" />
    </LinearLayout>
 
</merge>

它的實際展示效果如下圖所示

在類中將以上兩個view分別進行例項關聯。

View.inflate(getContext(), R.layout.main_list_item_home_new_arrival, this);
mBaseCountDownTimerViewShort = findViewById(R.id.count_down_timer_short); //行尾倒數計時view
mBaseCountDownTimerViewLong = findViewById(R.id.count_down_timer_long); //次行行首倒數計時view

通過以上的步驟搞定了兩種情況下倒數計時控制元件的佈局,接下來就該考慮折行展示的判斷條件了。

在多語言環境中,標題textview與倒數計時view的寬度都是不確定的,因此需要綜合考慮兩個控制元件的寬度。同時,因為策劃要求,還需考慮某些語種特殊情況的展示要求。判斷程式碼如下所示:

private boolean isShortCountDownTimerViewShow() {
        String languageCode = LocaleManager.getInstance().getCurrentLanguage();
        if (Constant.EN_US.equals(languageCode) || Constant.EN_GB.equals(languageCode) || Constant.EN_AU.equals(languageCode)) {
            //因策劃要求,美式英語、英國英語、澳大利亞英語,強制在New Arrivals標題欄右側展示
            return true;
        } else {
            View newArrivalHeader = inflate(mContext, R.layout.main_view_header_new_arrival, null);
            TextView newArrivalTextView = newArrivalHeader.findViewById(R.id.new_arrival_txt);
            LinearLayout countDownTimer = newArrivalHeader.findViewById(R.id.count_down_timer_short);
            int measureSpecW = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            int measureSpecH = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            newArrivalTextView.measure(measureSpecW, measureSpecH);
            countDownTimer.measure(measureSpecW, measureSpecH);
            VLog.i(TAG, countDownTimer.getMeasuredWidth() + "--" + newArrivalTextView.getMeasuredWidth());
 
            if (countDownTimer.getMeasuredWidth() + newArrivalTextView.getMeasuredWidth() <= mContext.getResources().getDimensionPixelSize(R.dimen.qb_px_302)) {
                return true;
            } else {
                return false;
            }
        }
    }

在程式碼中,可以根據實際需要定製具體某幾款語言是否換行顯示。

而對於剩下的大多數語言,可以使用MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)獲取measureSpecW 和 measureSpecH ,第一個引數是系統測量該View後得到的規格值,這裡使用0代表省略(在系統對該View繪製之前就直接呼叫了measure方法,所以寬高為0,該值與最終獲取的寬高無關),第二個引數MeasureSpec.UNSPECIFIED代表父容器不對View有任何限制。獲取完成後也就順利完成具體view寬度的測量。

通過該方法的返回值,我們就可以控制兩個倒數計時view的展示與隱藏,從而達到自適應折行展示的效果。

if (isShortCountDownTimerViewShow()) {
               initCountDownTimerView(mBaseCountDownTimerViewShort, bean);
               mBaseCountDownTimerViewShort.setVisibility(VISIBLE);
               mBaseCountDownTimerViewLong.setVisibility(GONE);
           } else {
               initCountDownTimerView(mBaseCountDownTimerViewLong, bean);
               mBaseCountDownTimerViewShort.setVisibility(GONE);
               mBaseCountDownTimerViewLong.setVisibility(VISIBLE);
           }

此外,該方法也不侷限於倒數計時控制元件view,針對多語言中各種各樣的自定義view,依然可以使用這種測量方法實現自適應換行的美觀展示。

四、實現倒數計時動畫效果

4.1 倒數計時數字滾動動畫的原理分析

從效果圖上可以看到,時、分、秒都是兩位數,且數字的變化規律都相同:首先是從個位數開始變化,舊數字從正常展示區域向上移動一定距離,新數字從下向上移動一定距離到達正常展示區域。如果個位數遞減至0,則十位數需要遞減,所以變化是十位和個位一起移動。

具體的實現思路為:

1、將時/分/秒的兩位數當成一個數字滾動元件;

2、將數字滾動元件的兩位數,拆分成一個數字陣列,變化操作針對陣列中的單個元素操作即可;

3、儲存舊數字,將舊數字和新數字的陣列元素逐個比較,數字相同的位繪製新數字,數字不同的位一起移動即可;

4、在移動數字時,需要將舊數字向上移動,移動的距離是 0 至 負的最大滾動距離;同時要將新數字向上移動,移動距離為最大滾動距離 至 0;其中最大滾動距離是數字滾動控制元件的高度,該值需要根據實際的UI稿確定。

4.2 具體實現

4.2.1 倒數計時滾動元件初始化

倒數計時滾動元件繼承自TextView,在建構函式中設定【最大滾動距離】和【畫筆相關屬性】,這兩者都需要根據實際UI稿確定。

其中,最大滾動距離mMaxMoveHeight是UI稿中時/分/秒數字控制元件的整體高度;畫筆設定的字型顏色、大小等,均為UI稿中時/分/秒數字的字型顏色、大小等。具體程式碼如下所示:

//建構函式
public NumberFlipView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
 
    mResources = context.getResources();
    //最大滾動高度18dp
    mMaxMoveHeight = mResources.getDimensionPixelSize(R.dimen.qb_px_18);
 
    //設定畫筆相關屬性
    setPaint();
}
 
//設定畫筆相關屬性
private void setPaint() {
    //設定繪製數字為白色
    mPaint.setColor(Color.WHITE);
    //設定繪製數字樣式為實心
    mPaint.setStyle(Paint.Style.FILL);
    //設定繪製數字字型加粗
    mPaint.setFakeBoldText(true);
    //設定繪製文字大小14dp
    mPaint.setTextSize(mResources.getDimensionPixelSize(R.dimen.qb_px_14));
}

4.2.2 繪製倒數計時滾動元件

繪製倒數計時數字是通過重寫onDraw()實現的。首先拆分舊數字和新數字成為相應的數字陣列;

具體程式碼如下所示:

//拆分新數字成為新數字陣列
for (int i = 0; i < mNewNumber.length(); i++) {
    mNewNumberArray.add(String.valueOf(mNewNumber.charAt(i)));
}
 
//拆分老數字成為老數字陣列
for (int i = 0; i < mOldNumber.length(); i++) {
    mOldNumberArray.add(String.valueOf(mOldNumber.charAt(i)));
}

然後繪製數字:繪製新數字時,逐位判斷舊數字和新數字是否相同,如果數字相同,直接繪製新數字;如果數字不相同,舊數字和新數字均需要移動。

具體程式碼如下所示:

//兩位數的newNumber的文字寬度
int textWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_16);
 
float curTextWidth = 0;
 
for (int i = 0; i < mNewNumberArray.size(); i++) {
    //newNumber中每個數字的邊界
    mPaint.getTextBounds(mNewNumberArray.get(i), 0, mNewNumberArray.get(i).length(), mTextRect);
    //newNumber中每個數字的寬度
    int numWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_5);
 
    //逐位判斷舊數字和新數字是否相同
    if (mNewNumberArray.get(i).equals(mOldNumberArray.get(i))) {
        //數字相同,直接繪製新數字
        canvas.drawText(mNewNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
        getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
 
    } else {
        //數字不相同,舊數字和新數字均需要移動
        canvas.drawText(mOldNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
        mOldNumberMoveHeight + getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
 
        canvas.drawText(mNewNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
        mNewNumberMoveHeight + getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
 
    }
 
    curTextWidth += (numWidth + mResources.getDimensionPixelSize(R.dimen.qb_px_3));

getWidth()獲取的是倒數計時控制元件的整個寬度;textWidth是兩位數字的寬度;numWidth是單個數字的寬度;curTextWidth是每個數字水平起始繪製位置的間距,curTextWidth=numWidth+兩個數字之間的間距。

十位數字的水平繪製起始位置為getWidth()/2 + textWidth/2;個位數字的水平繪製起始位置為getWidth()/2textWidth/2 + curTextWidth。getHight()獲取的是倒數計時控制元件的整個高度;textRect.height()獲取的是數字的高度。

舊數字的垂直繪製起始位置為mOldNumberMoveHeight + getHeight()/2 + textRect.height()/2;新數字的垂直繪製起始位置為mNewNumberMoveHeightgetHeight()/2 + textRect.height()/2。

4.2.3 倒數計時數字滾動效果實現

舊數字和新數字的滾動效果是通過ValueAnimator不斷改變舊數字的滾動距離mOldNumberMoveHeight和新數字的滾動距離mNewNumberMoveHeight實現的。

在規定的動畫時間FLIP_NUMBER_DURATION內,mNewNumberMoveHeight需要從最大滾動距離mMaxMoveHeight變為0,mOldNumberMoveHeight需要從0變為負的最大滾動距離mMaxMoveHeight;每次計算出新的滾動距離後,呼叫invalidate()方法,觸發onDraw()方法,不斷地繪製舊數字和新數字,以實現數字滾動的效果。

具體程式碼如下所示:

/*
利用ValueAnimator,在規定時間FLIP_NUMBER_DURATION之內,將值從MAX_MOVE_HEIGHT變為0,
每次值變化都賦給mNewNumberMoveHeight,同時將mNewNumberMoveHeight - MAX_MOVE_HEIGHT的值賦給mOldNumberMoveHeight,
並重新繪製,實現新數字和舊數字的上滑;
 */
mNumberAnimator = ValueAnimator.ofFloat(mMaxMoveHeight, 0);
mNumberAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mNewNumberMoveHeight = (float) animation.getAnimatedValue();
        mOldNumberMoveHeight = mNewNumberMoveHeight - mMaxMoveHeight;
        invalidate();
    }
});
mNumberAnimator.setDuration(FLIP_NUMBER_DURATION);
mNumberAnimator.start();

4.3 具體使用

首先在佈局中引入,用法和TextView相同。下圖為時、分、秒對應的佈局:

<!--時-->
<com.example.materialdesginpractice.NumberFlipView
    android:id="@+id/hours_tv"
    android:layout_width="@dimen/qb_px_22"
    android:layout_height="@dimen/qb_px_18"
    android:gravity="center"
    android:background="@drawable/number_bg"
    android:textSize="@dimen/qb_px_14"
    android:textColor="@color/common_color_ffffff"/>
 
 
<!--分-->
<com.example.materialdesginpractice.NumberFlipView
    android:id="@+id/min_tv"
    android:layout_width="@dimen/qb_px_22"
    android:layout_height="@dimen/qb_px_18"
    android:gravity="center"
    android:background="@drawable/number_bg"
    android:textSize="@dimen/qb_px_14"
    android:textColor="@color/common_color_ffffff"/>
 
<!--秒-->
<com.example.materialdesginpractice.NumberFlipView
    android:id="@+id/sec_tv"
    android:layout_width="@dimen/qb_px_22"
    android:layout_height="@dimen/qb_px_18"
    android:gravity="center"
    android:background="@drawable/number_bg"
    android:textSize="@dimen/qb_px_14"
    android:textColor="@color/common_color_ffffff"/>

然後通過id找到對應的倒數計時數字控制元件:

mHourTextView = findViewById(R.id.hours_tv);
mMinTextView = findViewById(R.id.min_tv);
mSecondTextView = findViewById(R.id.sec_tv);

最後呼叫時/分/秒倒數計時數字控制元件的方法,設定倒數計時初始值或者倒數計時新數字。如果是首次進行倒數計時,需要呼叫setInitialNumber()方法設定初始值;否則呼叫flipNumber()方法設定新的倒數計時數值。

具體用法如下所示:

if (mFirstSetTimer) {
    mHourTextView.setInitialNumber(hours);
    mMinTextView.setInitialNumber(minute);
    mSecondTextView.setInitialNumber(second);
    mFirstSetTimer = false;
} else {
    mHourTextView.flipNumber(hours);
    mMinTextView.flipNumber(minute);
    mSecondTextView.flipNumber(second);
}

五、優化倒數計時效能

5.1 倒數計時數字滾動動畫的原理分析

在實現中,倒數計時控制元件是作為ListView的子元素,而且ListView是處於一個Fragment中。

為了減少功耗,需要在倒數計時控制元件不在可見範圍內時,暫停倒數計時;當倒數計時控制元件重新出現在可見範圍內時,重新開始倒數計時。下圖是倒數計時暫停與開始的場景。

5.2 具體實現

5.2.1 暫停倒數計時

頁面滑動,倒數計時控制元件滑出可視區域,當倒數計時控制元件滑出ListView的可視範圍內,需要暫停倒數計時。該情況的重點是:需要判斷出子view是否已經移出ListView中。

如果應用只需要相容安卓7及以上,可以通過重寫onDetachedFromWindow()方法,在方法體內進行取消倒數計時的操作。因為每當子view移出ListView時就會呼叫這個方法。

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    //移出螢幕呼叫,暫停倒數計時
    stopCountDownTimerAndAnimation();
}

如果應用需要相容安卓7以下,則上述方法會失效,因為onDetachedFromWindow()方法並不相容低版本。但是可是通過重寫onStartTemporaryDetach()方法實現相同的效果。

@Override
public void onStartTemporaryDetach() {
    super.onStartTemporaryDetach();
    //移出螢幕呼叫,暫停倒數計時
    stopCountDownTimerAndAnimation();
}

通過tab切換到其他Fragment

當倒數計時控制元件位於可視範圍內,此時通過tab切換到其他Fragment時,需要暫停倒數計時。該情況下倒數計時控制元件所在的Fragment會隱藏,可以在Fragment隱藏時獲取倒數計時控制元件的View,然後呼叫其方法暫停倒數計時。

@Override
public void onFragmentHide() {
    super.onFragmentHide();
 
    //暫停倒數計時
    stopNewArrivalCountDownTimerAndAnimation();
}

為了獲取倒數計時控制元件所在的View物件,通過遍歷ListView可視範圍內的子View,判斷其是否是倒數計時控制元件所在的View物件。然後呼叫倒數計時控制元件所在View物件的stopCountDownTimerAndAnimation()方法,暫停倒數計時。

/**
 * 獲取倒數計時控制元件所在的view物件,暫停倒數計時
 */
private void stopNewArrivalCountDownTimerAndAnimation() {
    if (mListView != null) {
        for (int index = 0; index < mListView.getChildCount(); index++) {
            View view = mListView.getChildAt(index);
            if (view instanceof HomeItemViewNewArrival) {
                ((HomeItemViewNewArrival) view).stopCountDownTimerAndAnimation();
            }
        }
    }
}

應用切換至後臺/跳轉到其他介面

當倒數計時控制元件位於可視範圍內,此時應用切換到至後臺 或者 點選倒數計時控制元件所在介面的其他內容,跳轉到其他介面,都需要暫停倒數計時。由於這些情況都會觸發倒數計時所在Fragment的onStop()方法。因此可以重寫onStop(),並在該方法體內獲取倒數計時控制元件的View,然後暫停倒數計時。

stopNewArrivalCountDownTimerAndAnimation()方法同上。

@Override
public void onStop() {
    super.onStop();
 
    //暫停倒數計時
    stopNewArrivalCountDownTimerAndAnimation();
}

5.2.2 開始倒數計時

頁面滑動,倒數計時控制元件滑入可視區域

當倒數計時控制元件滑出可視區域後,再次滑入可視區域,會自動呼叫Adapter的getView()方法,然後呼叫倒數計時控制元件的onBindView()方法。由於onBindView()方法中會初始化倒數計時控制元件,因此該情況下,無需再手動開始倒數計時。

通過tab切換回到倒數計時所在的Fragment

通過tab切換回到倒數計時控制元件所在的Fragment,若此時倒數計時控制元件在可視範圍內,則需要重新開始倒數計時。由於該情況下Fragment會重新顯示,因此可以在Fragment顯示時獲取倒數計時控制元件的View,然後呼叫其方法重新開始倒數計時。

@Override
public void onFragmentShow(int source, int floor) {
    super.onFragmentShow(source, floor);
    //重新開始倒數計時
    refreshNewArrival();
}

同樣,為了獲取倒數計時控制元件所在的View物件,需要通過遍歷ListView可視範圍內的子View,判斷其是否是倒數計時控制元件所在的View物件。然後呼叫倒數計時控制元件所在View物件的refreshEventStatus ()方法,開始倒數計時。

/**
 * 獲取倒數計時控制元件所在的view物件,開始倒數計時
 */
private void refreshNewArrival() {
    if (mListView != null) {
        for (int index = 0; index < mListView.getChildCount(); index++) {
            View view = mListView.getChildAt(index);
            if (view instanceof HomeItemViewNewArrival) {
                ((HomeItemViewNewArrival) view).refreshEventStatus();
            }
        }
    }
}

應用切換回前臺/從其他介面回退

當應用切換到回前臺 或者 從其他介面回退到倒數計時控制元件所在的介面,若此時倒數計時控制元件在可視範圍內,則都需要重新開始倒數計時。由於這些情況都會觸發倒數計時所在Fragment的onResume()方法。因此可以重寫onResume(),並在該方法體內獲取倒數計時控制元件的View,然後呼叫其方法重新開始倒數計時。

其中refreshNewArrival()方法同上。

@Override
public void onResume() {
    super.onResume();
    //重新開始倒數計時
    refreshNewArrival();
}

​作者:vivo 網際網路客戶端團隊Liu Zhiyi、Zhen Yiqing

相關文章