搶購倒計時自定義控件的實現(xiàn)與優(yōu)化

一、 前言

隨著網購的持續(xù)發(fā)展辩越,搶購類倒計時在各類電商應用中已十分常見信粮,這種設計可以提高用戶的點擊率和下單率等强缘。

但是國內的電商應用大部分都僅支持中文旅掂,不適配其他的語言商虐,因此當?shù)褂嫊r與其他文案處于同一行展示時,無需考慮倒計時的展示方式劫哼。在海外應用中割笙,由于需要適配各種語言咳蔚,有些小語種的文案較長谈火,因此當?shù)褂嫊r和其他文案處于同一行展示時糯耍,需要充分考慮多語言的適配温技,如何優(yōu)雅地完成倒計時自適應顯示是一個值得深思的問題舵鳞。

為進一步優(yōu)化倒計時效果蜓堕,我們?yōu)榈褂嫊r增加了數(shù)字滾動動畫套才,如下圖所示。倒計時的功能必然會帶來性能的消耗沸毁,如何避免倒計時帶來的性能問題息尺,本文也將給出相應的解決方案掷倔。

image

二勒葱、 實現(xiàn)倒計時基本功能

2.1 需求與原理分析

該控件預期展現(xiàn)兩種狀態(tài)凛虽,距離活動開始還有X天XX:XX:XX 和距離活動結束還有X天XX:XX:XX凯旋,因此需要一個活動狀態(tài)屬性至非,并通過這個活動開始與否的屬性設置時間前的文案荒椭。具體時間時分秒之間相互獨立趣惠,因此將它們拆分成獨立的textview進行處理味悄。

倒計時控件的核心是計時器侍瑟,安卓中已經有現(xiàn)成的CountDownTimer類可供使用以實現(xiàn)倒計時功能丢习。此外咐低,還需要實現(xiàn)一些監(jiān)聽的接口袜腥。

2.2 具體實現(xiàn)

2.2.1 回調監(jiān)聽接口設計

首先,定義回調接口

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

在該接口中定義三個方法:

onRemain(long millisUntilFinished):倒計時進行中回調的方法羹令,用于后續(xù)功能的拓展

onFinish():倒計時結束回調鲤屡,用于活動狀態(tài)的切換和計時的暫停等

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

2.2.2 view的構建與綁定

其次福侈,初始化自定義view酒来,基于實際開發(fā)需求,將整個控件細分為修飾文案肪凛、天數(shù)辽社、時、分翘鸭、秒等幾個獨立的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 構建內部使用的私有方法

首先構造設置剩余時間的方法,入參是剩余的毫秒數(shù)就乓,在方法內部將時間轉化為具體的天時分秒汉匙,并將結果賦予給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);
     }
 }

需要注意的是,當單位時間為個位數(shù)時生蚁,為了視覺效果的統(tǒng)一噩翠,要在數(shù)字前加“0”進行補位。

其次邦投,構建一個創(chuàng)建倒計時的方法绎秒,其代碼如下:

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時,活動狀態(tài)刷新尼摹,倒計時不展示00:00:00這個狀態(tài)
               if (millisUntilFinished >= ONE_SEC) {
                   setSecond(millisUntilFinished);
                   //當活動狀態(tài)為進行中時见芹,每隔一分鐘調用一次回調
                   if (eventStatus == HomeItemViewNewArrival.EVENT_START) {
                       mArrivalOneMinuteFlag--;
                       if (mArrivalOneMinuteFlag == Constant.ZERO) {
                           mArrivalOneMinuteFlag = Constant.SIXTY;
                           mOnCountDownTimerListener.onArrivalOneMinute();
                       }
                   }
               }
           }
 
           @Override
           public void onFinish() {
               mOnCountDownTimerListener.onFinish();
           }
       };
   }

在該方法中,創(chuàng)建一個倒計時實例CountDownTimer蠢涝,CountDownTimer() 有兩個參數(shù)玄呛,分別是剩余的總時間和刷新間隔。

在實例的onTick()方法中和二,調用setSecond()方法在每次間隔時間(也就是1s)后定期刷新view徘铝,完成倒計時控件的更新。此外惯吕,產品中還有一個一分鐘定期上報埋點的需求惕它,也可以在onTick()方法中完成。在實際項目事件中废登,若有定時的任務需求淹魄,也可在該方法中自由設置。最后堡距,還需重寫該CountDownTimer的onFinish()方法甲锡,觸發(fā)listener接口里的onFinish()

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

首先是設置倒計時的監(jiān)聽事件:

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);// 創(chuàng)建倒計時
        mCountDownTimer.start();
    }
 
    public void cancelDownTimer() {
        mCountDownTimer.cancel();
    }

在開始倒計時的方法中缤沦,初始化倒計時的初始值并創(chuàng)建倒計時,最后調用CountDownTimer實例的start()方法開始倒計時易稠。在取消的方法中缸废,直接調用CountDownTimer實例的cancel()方法取消倒計時。

2.3 倒計時類的實際調用

實際調用倒計時控件時,只需在具體布局中添加該倒計時類布局企量,在調用的類中實例化BaseCountDownTimerView测萎。接著,使用實例的setDownTime()梁钾、setHeaderText()初始化數(shù)據绳泉,使用setDownTimerListener()給view實例設置監(jiān)聽逊抡。

最后調用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;
                        //活動狀態(tài)之前為進行中,倒計時變?yōu)?冒嫡,如果還有下一個活動/新品拇勃,則刷新為下一個活動/新品的數(shù)據
                        refreshNewArrivalBeanDate(bean);
                        onBindView(bean, 1, true, null);
                    } else {
                        setEventStatus(bean);
                    }
                }
 
                @Override
                public void onArrivalOneMinute() {
 
                }
            });

三、實現(xiàn)倒計時整體布局

3.1 需求描述

在多語言環(huán)境或者不同屏幕條件下孝凌,某些語種的控件長度過長方咆,需要自適應控件進行折行顯示以適應UI規(guī)范。

3.2 實施方案

原本考慮只實例化一個自定義倒計時控件的對象蟀架,但是在設計對象布局的過程中發(fā)現(xiàn)瓣赂,一個對象不方便同時實現(xiàn)在行尾展示或折行后在第二行行首顯示。因此片拍,本文采用了在布局的時候同時預置兩個倒計時對象的方法煌集,一個對象位于行尾,另一個位于第二行的行首捌省。

在measure過程中苫纤,如果測量得到控件的寬度大于某一個寬度閾值,則初始化次行行首的view纲缓,并將行尾的view可見狀態(tài)置為Gone卷拘,若小于某一個寬度閾值,則初始化行尾的view祝高,并將次行行首的view可見狀態(tài)置為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>

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

image

但是此布局只能展示單行能展示所有內容的情況工闺,因此還需要在此布局上拓展雙行展示的情況横腿,再看一看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>

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

image

在類中將以上兩個view分別進行實例關聯(lián)。

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

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

在多語言環(huán)境中,標題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;
            }
        }
    }

在代碼中,可以根據實際需要定制具體某幾款語言是否換行顯示避除。

而對于剩下的大多數(shù)語言怎披,可以使用MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)獲取measureSpecW 和 measureSpecH ,第一個參數(shù)是系統(tǒng)測量該View后得到的規(guī)格值瓶摆,這里使用0代表省略(在系統(tǒng)對該View繪制之前就直接調用了measure方法凉逛,所以寬高為0,該值與最終獲取的寬高無關)群井,第二個參數(shù)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,依然可以使用這種測量方法實現(xiàn)自適應換行的美觀展示稍坯。

四酬荞、實現(xiàn)倒計時動畫效果

4.1 倒計時數(shù)字滾動動畫的原理分析

image

從效果圖上可以看到,時瞧哟、分混巧、秒都是兩位數(shù),且數(shù)字的變化規(guī)律都相同:首先是從個位數(shù)開始變化勤揩,舊數(shù)字從正常展示區(qū)域向上移動一定距離咧党,新數(shù)字從下向上移動一定距離到達正常展示區(qū)域。如果個位數(shù)遞減至0陨亡,則十位數(shù)需要遞減傍衡,所以變化是十位和個位一起移動。

具體的實現(xiàn)思路為:

1负蠕、將時/分/秒的兩位數(shù)當成一個數(shù)字滾動組件蛙埂;

2、將數(shù)字滾動組件的兩位數(shù)遮糖,拆分成一個數(shù)字數(shù)組绣的,變化操作針對數(shù)組中的單個元素操作即可;

3、保存舊數(shù)字屡江,將舊數(shù)字和新數(shù)字的數(shù)組元素逐個比較芭概,數(shù)字相同的位繪制新數(shù)字,數(shù)字不同的位一起移動即可惩嘉;

4罢洲、在移動數(shù)字時,需要將舊數(shù)字向上移動文黎,移動的距離是 0 至 負的最大滾動距離惹苗;同時要將新數(shù)字向上移動,移動距離為最大滾動距離 至 0臊诊;其中最大滾動距離是數(shù)字滾動控件的高度鸽粉,該值需要根據實際的UI稿確定斜脂。

4.2 具體實現(xiàn)

4.2.1 倒計時滾動組件初始化

倒計時滾動組件繼承自TextView抓艳,在構造函數(shù)中設置【最大滾動距離】和【畫筆相關屬性】,這兩者都需要根據實際UI稿確定帚戳。

其中玷或,最大滾動距離mMaxMoveHeight是UI稿中時/分/秒數(shù)字控件的整體高度;畫筆設置的字體顏色片任、大小等偏友,均為UI稿中時/分/秒數(shù)字的字體顏色、大小等对供。具體代碼如下所示:

//構造函數(shù)
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() {
    //設置繪制數(shù)字為白色
    mPaint.setColor(Color.WHITE);
    //設置繪制數(shù)字樣式為實心
    mPaint.setStyle(Paint.Style.FILL);
    //設置繪制數(shù)字字體加粗
    mPaint.setFakeBoldText(true);
    //設置繪制文字大小14dp
    mPaint.setTextSize(mResources.getDimensionPixelSize(R.dimen.qb_px_14));
}

4.2.2 繪制倒計時滾動組件

繪制倒計時數(shù)字是通過重寫onDraw()實現(xiàn)的位他。首先拆分舊數(shù)字和新數(shù)字成為相應的數(shù)字數(shù)組;

具體代碼如下所示:

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

然后繪制數(shù)字:繪制新數(shù)字時产场,逐位判斷舊數(shù)字和新數(shù)字是否相同鹅髓,如果數(shù)字相同,直接繪制新數(shù)字京景;如果數(shù)字不相同窿冯,舊數(shù)字和新數(shù)字均需要移動。

具體代碼如下所示:

//兩位數(shù)的newNumber的文字寬度
int textWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_16);
 
float curTextWidth = 0;
 
for (int i = 0; i < mNewNumberArray.size(); i++) {
    //newNumber中每個數(shù)字的邊界
    mPaint.getTextBounds(mNewNumberArray.get(i), 0, mNewNumberArray.get(i).length(), mTextRect);
    //newNumber中每個數(shù)字的寬度
    int numWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_5);
 
    //逐位判斷舊數(shù)字和新數(shù)字是否相同
    if (mNewNumberArray.get(i).equals(mOldNumberArray.get(i))) {
        //數(shù)字相同确徙,直接繪制新數(shù)字
        canvas.drawText(mNewNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
        getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
 
    } else {
        //數(shù)字不相同醒串,舊數(shù)字和新數(shù)字均需要移動
        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是兩位數(shù)字的寬度鄙皇;numWidth是單個數(shù)字的寬度芜赌;curTextWidth是每個數(shù)字水平起始繪制位置的間距,curTextWidth=numWidth+兩個數(shù)字之間的間距伴逸。

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

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

4.2.3 倒計時數(shù)字滾動效果實現(xiàn)

舊數(shù)字和新數(shù)字的滾動效果是通過ValueAnimator不斷改變舊數(shù)字的滾動距離mOldNumberMoveHeight和新數(shù)字的滾動距離mNewNumberMoveHeight實現(xiàn)的禽篱。

在規(guī)定的動畫時間FLIP_NUMBER_DURATION內畜伐,mNewNumberMoveHeight需要從最大滾動距離mMaxMoveHeight變?yōu)?,mOldNumberMoveHeight需要從0變?yōu)樨摰淖畲鬂L動距離mMaxMoveHeight躺率;每次計算出新的滾動距離后玛界,調用invalidate()方法,觸發(fā)onDraw()方法悼吱,不斷地繪制舊數(shù)字和新數(shù)字慎框,以實現(xiàn)數(shù)字滾動的效果。

具體代碼如下所示:

/*
利用ValueAnimator后添,在規(guī)定時間FLIP_NUMBER_DURATION之內笨枯,將值從MAX_MOVE_HEIGHT變?yōu)?,
每次值變化都賦給mNewNumberMoveHeight遇西,同時將mNewNumberMoveHeight - MAX_MOVE_HEIGHT的值賦給mOldNumberMoveHeight馅精,
并重新繪制,實現(xiàn)新數(shù)字和舊數(shù)字的上滑粱檀;
 */
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找到對應的倒計時數(shù)字控件:

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

最后調用時/分/秒倒計時數(shù)字控件的方法,設置倒計時初始值或者倒計時新數(shù)字渗常。如果是首次進行倒計時壮不,需要調用setInitialNumber()方法設置初始值;否則調用flipNumber()方法設置新的倒計時數(shù)值凳谦。

具體用法如下所示:

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

五忆畅、優(yōu)化倒計時性能

5.1 倒計時數(shù)字滾動動畫的原理分析

在實現(xiàn)中,倒計時控件是作為ListView的子元素尸执,而且ListView是處于一個Fragment中家凯。

為了減少功耗,需要在倒計時控件不在可見范圍內時如失,暫停倒計時绊诲;當?shù)褂嫊r控件重新出現(xiàn)在可見范圍內時,重新開始倒計時褪贵。下圖是倒計時暫停與開始的場景掂之。

5.2 具體實現(xiàn)

5.2.1 暫停倒計時

頁面滑動抗俄,倒計時控件滑出可視區(qū)域,當?shù)褂嫊r控件滑出ListView的可視范圍內世舰,需要暫停倒計時动雹。該情況的重點是:需要判斷出子view是否已經移出ListView中。

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

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    //移出屏幕調用茸塞,暫停倒計時
    stopCountDownTimerAndAnimation();
}

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

@Override
public void onStartTemporaryDetach() {
    super.onStartTemporaryDetach();
    //移出屏幕調用笋庄,暫停倒計時
    stopCountDownTimerAndAnimation();
}

通過tab切換到其他Fragment

當?shù)褂嫊r控件位于可視范圍內效扫,此時通過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();
            }
        }
    }
}

應用切換至后臺/跳轉到其他界面

當?shù)褂嫊r控件位于可視范圍內,此時應用切換到至后臺 或者 點擊倒計時控件所在界面的其他內容听绳,跳轉到其他界面颂碘,都需要暫停倒計時。由于這些情況都會觸發(fā)倒計時所在Fragment的onStop()方法椅挣。因此可以重寫onStop()头岔,并在該方法體內獲取倒計時控件的View,然后暫停倒計時鼠证。

stopNewArrivalCountDownTimerAndAnimation()方法同上峡竣。

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

5.2.2 開始倒計時

頁面滑動,倒計時控件滑入可視區(qū)域

當?shù)褂嫊r控件滑出可視區(qū)域后量九,再次滑入可視區(qū)域适掰,會自動調用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();
            }
        }
    }
}

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

當應用切換到回前臺 或者 從其他界面回退到倒計時控件所在的界面栅干,若此時倒計時控件在可視范圍內,則都需要重新開始倒計時捐祠。由于這些情況都會觸發(fā)倒計時所在Fragment的onResume()方法碱鳞。因此可以重寫onResume(),并在該方法體內獲取倒計時控件的View踱蛀,然后調用其方法重新開始倒計時窿给。

其中refreshNewArrival()方法同上。

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

作者:vivo 互聯(lián)網客戶端團隊Liu Zhiyi率拒、Zhen Yiqing

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末崩泡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子猬膨,更是在濱河造成了極大的恐慌角撞,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勃痴,死亡現(xiàn)場離奇詭異谒所,居然都是意外死亡,警方通過查閱死者的電腦和手機召耘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門百炬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人污它,你說我怎么就攤上這事剖踊∈” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵德澈,是天一觀的道長歇攻。 經常有香客問我,道長梆造,這世上最難降的妖魔是什么缴守? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮镇辉,結果婚禮上屡穗,老公的妹妹穿的比我還像新娘。我一直安慰自己忽肛,他們只是感情好村砂,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著屹逛,像睡著了一般础废。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上罕模,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天评腺,我揣著相機與錄音,去河邊找鬼淑掌。 笑死蒿讥,一個胖子當著我的面吹牛,可吹牛的內容都是我干的锋拖。 我是一名探鬼主播诈悍,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼兽埃!你這毒婦竟也來了?” 一聲冷哼從身側響起适袜,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤柄错,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后苦酱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體售貌,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年疫萤,在試婚紗的時候發(fā)現(xiàn)自己被綠了颂跨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡扯饶,死狀恐怖恒削,靈堂內的尸體忽然破棺而出池颈,到底是詐尸還是另有隱情,我是刑警寧澤钓丰,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布躯砰,位于F島的核電站,受9級特大地震影響携丁,放射性物質發(fā)生泄漏琢歇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一梦鉴、第九天 我趴在偏房一處隱蔽的房頂上張望李茫。 院中可真熱鬧,春花似錦肥橙、人聲如沸涌矢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娜庇。三九已至,卻和暖如春方篮,著一層夾襖步出監(jiān)牢的瞬間名秀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工藕溅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匕得,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓巾表,卻偏偏與公主長得像汁掠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子集币,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容