<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 8299字,需瀏覽 17分鐘

           ·

          2021-10-15 16:31

          一、 前言

          隨著網(wǎng)購(gòu)的持續(xù)發(fā)展,搶購(gòu)類倒計(jì)時(shí)在各類電商應(yīng)用中已十分常見,這種設(shè)計(jì)可以提高用戶的點(diǎn)擊率和下單率等。

          但是國(guó)內(nèi)的電商應(yīng)用大部分都僅支持中文,不適配其他的語(yǔ)言,因此當(dāng)?shù)褂?jì)時(shí)與其他文案處于同一行展示時(shí),無(wú)需考慮倒計(jì)時(shí)的展示方式。在海外應(yīng)用中,由于需要適配各種語(yǔ)言,有些小語(yǔ)種的文案較長(zhǎng),因此當(dāng)?shù)褂?jì)時(shí)和其他文案處于同一行展示時(shí),需要充分考慮多語(yǔ)言的適配,如何優(yōu)雅地完成倒計(jì)時(shí)自適應(yīng)顯示是一個(gè)值得深思的問題。

          為進(jìn)一步優(yōu)化倒計(jì)時(shí)效果,我們?yōu)榈褂?jì)時(shí)增加了數(shù)字滾動(dòng)動(dòng)畫,如下圖所示。倒計(jì)時(shí)的功能必然會(huì)帶來(lái)性能的消耗,如何避免倒計(jì)時(shí)帶來(lái)的性能問題,本文也將給出相應(yīng)的解決方案。




          二、 實(shí)現(xiàn)倒計(jì)時(shí)基本功能


          2.1 需求與原理分析

          該控件預(yù)期展現(xiàn)兩種狀態(tài),距離活動(dòng)開始還有X天XX:XX:XX 和距離活動(dòng)結(jié)束還有X天XX:XX:XX,因此需要一個(gè)活動(dòng)狀態(tài)屬性,并通過(guò)這個(gè)活動(dòng)開始與否的屬性設(shè)置時(shí)間前的文案。具體時(shí)間時(shí)分秒之間相互獨(dú)立,因此將它們拆分成獨(dú)立的textview進(jìn)行處理。

          倒計(jì)時(shí)控件的核心是計(jì)時(shí)器,安卓中已經(jīng)有現(xiàn)成的CountDownTimer類可供使用以實(shí)現(xiàn)倒計(jì)時(shí)功能。此外,還需要實(shí)現(xiàn)一些監(jiān)聽的接口。

          2.2 具體實(shí)現(xiàn)

          2.2.1 回調(diào)監(jiān)聽接口設(shè)計(jì)

          首先,定義回調(diào)接口

          public interface OnCountDownTimerListener {    /**     * 倒計(jì)時(shí)正在進(jìn)行時(shí)調(diào)用的方法     *     * @param millisUntilFinished 剩余的時(shí)間(毫秒)     */    void onRemain(long millisUntilFinished);     /**     * 倒計(jì)時(shí)結(jié)束     */    void onFinish();     /**     * 每過(guò)一分鐘調(diào)用的方法     */    void onArrivalOneMinute(); }

          在該接口中定義三個(gè)方法:
          onRemain(long millisUntilFinished)
          倒計(jì)時(shí)進(jìn)行中回調(diào)的方法,用于后續(xù)功能的拓展

          onFinish():倒計(jì)時(shí)結(jié)束回調(diào),用于活動(dòng)狀態(tài)的切換和計(jì)時(shí)的暫停等

          onArrivalOneMinute():每過(guò)一分鐘回調(diào),用于定時(shí)上報(bào)的埋點(diǎn)

          2.2.2?view的構(gòu)建與綁定

          其次,初始化自定義view,基于實(shí)際開發(fā)需求,將整個(gè)控件細(xì)分為修飾文案、天數(shù)、時(shí)、分、秒等幾個(gè)獨(dú)立的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?構(gòu)建內(nèi)部使用的私有方法

          首先構(gòu)造設(shè)置剩余時(shí)間的方法,入?yún)⑹鞘S嗟暮撩霐?shù),在方法內(nèi)部將時(shí)間轉(zhuǎn)化為具體的天時(shí)分秒,并將結(jié)果賦予給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 + ""; // 時(shí)     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);     } }
          需要注意的是,當(dāng)單位時(shí)間為個(gè)位數(shù)時(shí),為了視覺效果的統(tǒng)一,要在數(shù)字前加“0”進(jìn)行補(bǔ)位。

          其次,構(gòu)建一個(gè)創(chuàng)建倒計(jì)時(shí)的方法,其代碼如下:
          private void createCountDownTimer(final int eventStatus) {       if (mCountDownTimer != null) {           mCountDownTimer.cancel();       }       mCountDownTimer = new CountDownTimer(mMillis, 1000) {           @Override           public void onTick(long millisUntilFinished) {               //策劃要求:倒計(jì)時(shí)為00:00:01時(shí),活動(dòng)狀態(tài)刷新,倒計(jì)時(shí)不展示00:00:00這個(gè)狀態(tài)               if (millisUntilFinished >= ONE_SEC) {                   setSecond(millisUntilFinished);                   //當(dāng)活動(dòng)狀態(tài)為進(jìn)行中時(shí),每隔一分鐘調(diào)用一次回調(diào)                   if (eventStatus == HomeItemViewNewArrival.EVENT_START) {                       mArrivalOneMinuteFlag--;                       if (mArrivalOneMinuteFlag == Constant.ZERO) {                           mArrivalOneMinuteFlag = Constant.SIXTY;                           mOnCountDownTimerListener.onArrivalOneMinute();                       }                   }               }           }            @Override           public void onFinish() {               mOnCountDownTimerListener.onFinish();           }       };   }

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

          在實(shí)例的onTick()方法中,調(diào)用setSecond()方法在每次間隔時(shí)間(也就是1s)后定期刷新view,完成倒計(jì)時(shí)控件的更新。

          此外,產(chǎn)品中還有一個(gè)一分鐘定期上報(bào)埋點(diǎn)的需求,也可以在onTick()方法中完成。在實(shí)際項(xiàng)目事件中,若有定時(shí)的任務(wù)需求,也可在該方法中自由設(shè)置。

          最后,還需重寫該CountDownTimer的onFinish()方法,觸發(fā)listener接口里的onFinish()

          2.2.4?構(gòu)建公有方法供外部使用

          首先是設(shè)置倒計(jì)時(shí)的監(jiān)聽事件:
          public void setDownTimerListener(OnCountDownTimerListener listener) {    this.mOnCountDownTimerListener = listener;}
          其次是外露一個(gè)設(shè)置初始時(shí)間和活動(dòng)開始或結(jié)束文案的方法:
          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");    }}

          最后,也是最重要的,需要給倒計(jì)時(shí)類設(shè)計(jì)開始與取消倒計(jì)時(shí)的方法:
          public void startDownTimer(int eventStatus) {        mArrivalOneMinuteFlag = Constant.SIXTY;        mFirstSetTimer = true;        //設(shè)置需要倒計(jì)時(shí)的初始值        setSecond(mMillis);        createCountDownTimer(eventStatus);// 創(chuàng)建倒計(jì)時(shí)        mCountDownTimer.start();    }     public void cancelDownTimer() {        mCountDownTimer.cancel();    }
          在開始倒計(jì)時(shí)的方法中,初始化倒計(jì)時(shí)的初始值并創(chuàng)建倒計(jì)時(shí),最后調(diào)用CountDownTimer實(shí)例的start()方法開始倒計(jì)時(shí)。

          在取消的方法中,直接調(diào)用CountDownTimer實(shí)例的cancel()方法取消倒計(jì)時(shí)。

          2.3?倒計(jì)時(shí)類的實(shí)際調(diào)用

          實(shí)際調(diào)用倒計(jì)時(shí)控件時(shí),只需在具體布局中添加該倒計(jì)時(shí)類布局,在調(diào)用的類中實(shí)例化BaseCountDownTimerView。

          接著,使用實(shí)例的setDownTime()、setHeaderText()初始化數(shù)據(jù),使用setDownTimerListener()給view實(shí)例設(shè)置監(jiān)聽。

          最后調(diào)用startDownTimer()開啟倒計(jì)時(shí)。
          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;                        //活動(dòng)狀態(tài)之前為進(jìn)行中,倒計(jì)時(shí)變?yōu)?,如果還有下一個(gè)活動(dòng)/新品,則刷新為下一個(gè)活動(dòng)/新品的數(shù)據(jù)                        refreshNewArrivalBeanDate(bean);                        onBindView(bean, 1, true, null);                    } else {                        setEventStatus(bean);                    }                }                 @Override                public void onArrivalOneMinute() {                 }            });


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


          3.1 需求描述

          在多語(yǔ)言環(huán)境或者不同屏幕條件下,某些語(yǔ)種的控件長(zhǎng)度過(guò)長(zhǎng),需要自適應(yīng)控件進(jìn)行折行顯示以適應(yīng)UI規(guī)范

          3.2 實(shí)施方案

          原本考慮只實(shí)例化一個(gè)自定義倒計(jì)時(shí)控件的對(duì)象,但是在設(shè)計(jì)對(duì)象布局的過(guò)程中發(fā)現(xiàn),一個(gè)對(duì)象不方便同時(shí)實(shí)現(xiàn)在行尾展示或折行后在第二行行首顯示。因此,本文采用了在布局的時(shí)候同時(shí)預(yù)置兩個(gè)倒計(jì)時(shí)對(duì)象的方法,一個(gè)對(duì)象位于行尾,另一個(gè)位于第二行的行首。

          在measure過(guò)程中,如果測(cè)量得到控件的寬度大于某一個(gè)寬度閾值,則初始化次行行首的view,并將行尾的view可見狀態(tài)置為Gone,若小于某一個(gè)寬度閾值,則初始化行尾的view,并將次行行首的view可見狀態(tài)置為Gone

          首先來(lái)看一看xml布局文件,以下是標(biāo)題加倒計(jì)時(shí)位于行尾的一個(gè)整體布局文件main_view_header_new_arrival
          <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>
          ?

          它的實(shí)際展示效果如下圖所示


          但是此布局只能展示單行能展示所有內(nèi)容的情況,因此還需要在此布局上拓展雙行展示的情況,再看一看main_list_item_home_new_arrival的布局
          <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>

          它的實(shí)際展示效果如下圖所示


          在類中將以上兩個(gè)view分別進(jìn)行實(shí)例關(guān)聯(lián)。
          View.inflate(getContext(), R.layout.main_list_item_home_new_arrival, this);mBaseCountDownTimerViewShort = findViewById(R.id.count_down_timer_short); //行尾倒計(jì)時(shí)viewmBaseCountDownTimerViewLong = findViewById(R.id.count_down_timer_long); //次行行首倒計(jì)時(shí)view

          通過(guò)以上的步驟搞定了兩種情況下倒計(jì)時(shí)控件的布局,接下來(lái)就該考慮折行展示的判斷條件了。

          在多語(yǔ)言環(huán)境中,標(biāo)題textview與倒計(jì)時(shí)view的寬度都是不確定的,因此需要綜合考慮兩個(gè)控件的寬度。

          同時(shí),因?yàn)椴邉澮?,還需考慮某些語(yǔ)種特殊情況的展示要求。

          判斷代碼如下所示:
          private boolean isShortCountDownTimerViewShow() {        String languageCode = LocaleManager.getInstance().getCurrentLanguage();        if (Constant.EN_US.equals(languageCode) || Constant.EN_GB.equals(languageCode) || Constant.EN_AU.equals(languageCode)) {            //因策劃要求,美式英語(yǔ)、英國(guó)英語(yǔ)、澳大利亞英語(yǔ),強(qiáng)制在New Arrivals標(biāo)題欄右側(cè)展示            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;            }        }    }
          在代碼中,可以根據(jù)實(shí)際需要定制具體某幾款語(yǔ)言是否換行顯示。

          而對(duì)于剩下的大多數(shù)語(yǔ)言,可以使用
          MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)獲取
          measureSpecW 和 measureSpecH ,第一個(gè)參數(shù)是系統(tǒng)測(cè)量該View后得到的規(guī)格值,這里使用0代表省略(在系統(tǒng)對(duì)該View繪制之前就直接調(diào)用了measure方法,所以寬高為0,該值與最終獲取的寬高無(wú)關(guān)),第二個(gè)參數(shù)MeasureSpec.UNSPECIFIED代表父容器不對(duì)View有任何限制。獲取完成后也就順利完成具體view寬度的測(cè)量。

          通過(guò)該方法的返回值,我們就可以控制兩個(gè)倒計(jì)時(shí)view的展示與隱藏,從而達(dá)到自適應(yīng)折行展示的效果。

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

          此外,該方法也不局限于倒計(jì)時(shí)控件view,針對(duì)多語(yǔ)言中各種各樣的自定義view,依然可以使用這種測(cè)量方法實(shí)現(xiàn)自適應(yīng)換行的美觀展示。


          四、實(shí)現(xiàn)倒計(jì)時(shí)動(dòng)畫效果


          4.1?倒計(jì)時(shí)數(shù)字滾動(dòng)動(dòng)畫的原理分析


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

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

          1、將時(shí)/分/秒的兩位數(shù)當(dāng)成一個(gè)數(shù)字滾動(dòng)組件;

          2、將數(shù)字滾動(dòng)組件的兩位數(shù),拆分成一個(gè)數(shù)字?jǐn)?shù)組,變化操作針對(duì)數(shù)組中的單個(gè)元素操作即可;

          3、保存舊數(shù)字,將舊數(shù)字和新數(shù)字的數(shù)組元素逐個(gè)比較,數(shù)字相同的位繪制新數(shù)字,數(shù)字不同的位一起移動(dòng)即可;

          4、在移動(dòng)數(shù)字時(shí),需要將舊數(shù)字向上移動(dòng),移動(dòng)的距離是 0 至 負(fù)的最大滾動(dòng)距離;同時(shí)要將新數(shù)字向上移動(dòng),移動(dòng)距離為最大滾動(dòng)距離 至 0;其中最大滾動(dòng)距離是數(shù)字滾動(dòng)控件的高度,該值需要根據(jù)實(shí)際的UI稿確定。

          4.2?具體實(shí)現(xiàn)

          4.2.1 倒計(jì)時(shí)滾動(dòng)組件初始化

          倒計(jì)時(shí)滾動(dòng)組件繼承自TextView,在構(gòu)造函數(shù)中設(shè)置【最大滾動(dòng)距離】和【畫筆相關(guān)屬性】,這兩者都需要根據(jù)實(shí)際UI稿確定。

          其中,最大滾動(dòng)距離mMaxMoveHeight是UI稿中時(shí)/分/秒數(shù)字控件的整體高度;畫筆設(shè)置的字體顏色、大小等,均為UI稿中時(shí)/分/秒數(shù)字的字體顏色、大小等。

          具體代碼如下所示:
          //構(gòu)造函數(shù)public NumberFlipView(Context context, @Nullable AttributeSet attrs) {    super(context, attrs);     mResources = context.getResources();    //最大滾動(dòng)高度18dp    mMaxMoveHeight = mResources.getDimensionPixelSize(R.dimen.qb_px_18);     //設(shè)置畫筆相關(guān)屬性    setPaint();} //設(shè)置畫筆相關(guān)屬性private void setPaint() {    //設(shè)置繪制數(shù)字為白色    mPaint.setColor(Color.WHITE);    //設(shè)置繪制數(shù)字樣式為實(shí)心    mPaint.setStyle(Paint.Style.FILL);    //設(shè)置繪制數(shù)字字體加粗    mPaint.setFakeBoldText(true);    //設(shè)置繪制文字大小14dp    mPaint.setTextSize(mResources.getDimensionPixelSize(R.dimen.qb_px_14));}

          4.2.2 繪制倒計(jì)時(shí)滾動(dòng)組件

          繪制倒計(jì)時(shí)數(shù)字是通過(guò)重寫onDraw()實(shí)現(xiàn)的。
          首先拆分舊數(shù)字和新數(shù)字成為相應(yīng)的數(shù)字?jǐn)?shù)組;

          具體代碼如下所示:
          //拆分新數(shù)字成為新數(shù)字?jǐn)?shù)組for (int i = 0; i < mNewNumber.length(); i++) {    mNewNumberArray.add(String.valueOf(mNewNumber.charAt(i)));} //拆分老數(shù)字成為老數(shù)字?jǐn)?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ù)字均需要移動(dòng)。

          具體代碼如下所示:
          //兩位數(shù)的newNumber的文字寬度int textWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_16); float curTextWidth = 0; for (int i = 0; i < mNewNumberArray.size(); i++) {    //newNumber中每個(gè)數(shù)字的邊界    mPaint.getTextBounds(mNewNumberArray.get(i), 0, mNewNumberArray.get(i).length(), mTextRect);    //newNumber中每個(gè)數(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ù)字均需要移動(dòng)        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()獲取的是倒計(jì)時(shí)控件的整個(gè)寬度;textWidth是兩位數(shù)字的寬度;numWidth是單個(gè)數(shù)字的寬度;curTextWidth是每個(gè)數(shù)字水平起始繪制位置的間距,curTextWidth=numWidth
          +兩個(gè)數(shù)字之間的間距。

          十位數(shù)字的水平繪制起始位置為getWidth()/2 + textWidth/2;個(gè)位數(shù)字的水平繪制起始位置為getWidth()/2?
          + textWidth/2?+ curTextWidth。

          getHight()獲取的是倒計(jì)時(shí)控件的整個(gè)高度;textRect.height()獲取的是數(shù)字的高度。

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


          4.2.3 倒計(jì)時(shí)數(shù)字滾動(dòng)效果實(shí)現(xiàn)

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

          在規(guī)定的動(dòng)畫時(shí)間FLIP_NUMBER_DURATION內(nèi),mNewNumberMoveHeight需要從最大滾動(dòng)距離mMaxMoveHeight變?yōu)?,mOldNumberMoveHeight需要從0變?yōu)樨?fù)的最大滾動(dòng)距離mMaxMoveHeight;每次計(jì)算出新的滾動(dòng)距離后,調(diào)用invalidate()方法,觸發(fā)onDraw()方法,不斷地繪制舊數(shù)字和新數(shù)字,以實(shí)現(xiàn)數(shù)字滾動(dòng)的效果。

          具體代碼如下所示:
          /*利用ValueAnimator,在規(guī)定時(shí)間FLIP_NUMBER_DURATION之內(nèi),將值從MAX_MOVE_HEIGHT變?yōu)?,每次值變化都賦給mNewNumberMoveHeight,同時(shí)將mNewNumberMoveHeight - MAX_MOVE_HEIGHT的值賦給mOldNumberMoveHeight,并重新繪制,實(shí)現(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相同。下圖為時(shí)、分、秒對(duì)應(yīng)的布局:
          <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"/>

          然后通過(guò)id找到對(duì)應(yīng)的倒計(jì)時(shí)數(shù)字控件:
          mHourTextView = findViewById(R.id.hours_tv);mMinTextView = findViewById(R.id.min_tv);mSecondTextView = findViewById(R.id.sec_tv);

          最后調(diào)用時(shí)/分/秒倒計(jì)時(shí)數(shù)字控件的方法,設(shè)置倒計(jì)時(shí)初始值或者倒計(jì)時(shí)新數(shù)字。如果是首次進(jìn)行倒計(jì)時(shí),需要調(diào)用setInitialNumber()方法設(shè)置初始值;否則調(diào)用flipNumber()方法設(shè)置新的倒計(jì)時(shí)數(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)化倒計(jì)時(shí)性能


          5.1?倒計(jì)時(shí)數(shù)字滾動(dòng)動(dòng)畫的原理分析

          在實(shí)現(xiàn)中,倒計(jì)時(shí)控件是作為L(zhǎng)istView的子元素,而且ListView是處于一個(gè)Fragment中。

          為了減少功耗,需要在倒計(jì)時(shí)控件不在可見范圍內(nèi)時(shí),暫停倒計(jì)時(shí);當(dāng)?shù)褂?jì)時(shí)控件重新出現(xiàn)在可見范圍內(nèi)時(shí),重新開始倒計(jì)時(shí)。下圖是倒計(jì)時(shí)暫停與開始的場(chǎng)景。


          5.2?具體實(shí)現(xiàn)

          5.2.1?暫停倒計(jì)時(shí)

          頁(yè)面滑動(dòng),倒計(jì)時(shí)控件滑出可視區(qū)域,當(dāng)?shù)褂?jì)時(shí)控件滑出ListView的可視范圍內(nèi),需要暫停倒計(jì)時(shí)。該情況的重點(diǎn)是:需要判斷出子view是否已經(jīng)移出ListView中。

          如果應(yīng)用只需要兼容安卓7及以上,可以通過(guò)重寫onDetachedFromWindow()方法,在方法體內(nèi)進(jìn)行取消倒計(jì)時(shí)的操作。因?yàn)槊慨?dāng)子view移出ListView時(shí)就會(huì)調(diào)用這個(gè)方法。

          @Overrideprotected void onDetachedFromWindow() {    super.onDetachedFromWindow();    //移出屏幕調(diào)用,暫停倒計(jì)時(shí)    stopCountDownTimerAndAnimation();}

          如果應(yīng)用需要兼容安卓7以下,則上述方法會(huì)失效,因?yàn)閛nDetachedFromWindow()方法并不兼容低版本。但是可是通過(guò)重寫onStartTemporaryDetach()方法實(shí)現(xiàn)相同的效果。

          @Overrideprotected void onDetachedFromWindow() {    super.onDetachedFromWindow();    //移出屏幕調(diào)用,暫停倒計(jì)時(shí)    stopCountDownTimerAndAnimation();}

          通過(guò)tab切換到其他Fragment

          當(dāng)?shù)褂?jì)時(shí)控件位于可視范圍內(nèi),此時(shí)通過(guò)tab切換到其他Fragment時(shí),需要暫停倒計(jì)時(shí)。該情況下倒計(jì)時(shí)控件所在的Fragment會(huì)隱藏,可以在Fragment隱藏時(shí)獲取倒計(jì)時(shí)控件的View,然后調(diào)用其方法暫停倒計(jì)時(shí)。

          @Overridepublic void onFragmentHide() {    super.onFragmentHide();     //暫停倒計(jì)時(shí)    stopNewArrivalCountDownTimerAndAnimation();}

          為了獲取倒計(jì)時(shí)控件所在的View對(duì)象,通過(guò)遍歷ListView可視范圍內(nèi)的子View,判斷其是否是倒計(jì)時(shí)控件所在的View對(duì)象。然后調(diào)用倒計(jì)時(shí)控件所在View對(duì)象的
          stopCountDownTimerAndAnimation()方法,暫停倒計(jì)時(shí)。

          /** * 獲取倒計(jì)時(shí)控件所在的view對(duì)象,暫停倒計(jì)時(shí) */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();            }        }    }}

          應(yīng)用切換至后臺(tái)/跳轉(zhuǎn)到其他界面

          當(dāng)?shù)褂?jì)時(shí)控件位于可視范圍內(nèi),此時(shí)應(yīng)用切換到至后臺(tái) 或者 點(diǎn)擊倒計(jì)時(shí)控件所在界面的其他內(nèi)容,跳轉(zhuǎn)到其他界面,都需要暫停倒計(jì)時(shí)。由于這些情況都會(huì)觸發(fā)倒計(jì)時(shí)所在Fragment的onStop()方法。因此可以重寫onStop(),并在該方法體內(nèi)獲取倒計(jì)時(shí)控件的View,然后暫停倒計(jì)時(shí)。

          stopNewArrivalCountDownTimerAndAnimation()方法同上。
          @Overridepublic void onStop() {    super.onStop();     //暫停倒計(jì)時(shí)    stopNewArrivalCountDownTimerAndAnimation();}

          5.2.2?開始倒計(jì)時(shí)

          頁(yè)面滑動(dòng),倒計(jì)時(shí)控件滑入可視區(qū)域

          當(dāng)?shù)褂?jì)時(shí)控件滑出可視區(qū)域后,再次滑入可視區(qū)域,會(huì)自動(dòng)調(diào)用Adapter的getView()方法,然后調(diào)用倒計(jì)時(shí)控件的onBindView()方法。由于onBindView()方法中會(huì)初始化倒計(jì)時(shí)控件,因此該情況下,無(wú)需再手動(dòng)開始倒計(jì)時(shí)。

          通過(guò)tab切換回到倒計(jì)時(shí)所在的Fragment

          通過(guò)tab切換回到倒計(jì)時(shí)控件所在的Fragment,若此時(shí)倒計(jì)時(shí)控件在可視范圍內(nèi),則需要重新開始倒計(jì)時(shí)。由于該情況下Fragment會(huì)重新顯示,因此可以在Fragment顯示時(shí)獲取倒計(jì)時(shí)控件的View,然后調(diào)用其方法重新開始倒計(jì)時(shí)。

          @Overridepublic void onFragmentShow(int source, int floor) {    super.onFragmentShow(source, floor);    //重新開始倒計(jì)時(shí)    refreshNewArrival();}

          同樣,為了獲取倒計(jì)時(shí)控件所在的View對(duì)象,需要通過(guò)遍歷ListView可視范圍內(nèi)的子View,判斷其是否是倒計(jì)時(shí)控件所在的View對(duì)象。然后調(diào)用倒計(jì)時(shí)控件所在View對(duì)象的refreshEventStatus ()方法,開始倒計(jì)時(shí)。

          /** * 獲取倒計(jì)時(shí)控件所在的view對(duì)象,開始倒計(jì)時(shí) */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();            }        }    }}

          應(yīng)用切換回前臺(tái)/從其他界面回退

          當(dāng)應(yīng)用切換到回前臺(tái) 或者 從其他界面回退到倒計(jì)時(shí)控件所在的界面,若此時(shí)倒計(jì)時(shí)控件在可視范圍內(nèi),則都需要重新開始倒計(jì)時(shí)。由于這些情況都會(huì)觸發(fā)倒計(jì)時(shí)所在Fragment的onResume()方法。因此可以重寫onResume(),并在該方法體內(nèi)獲取倒計(jì)時(shí)控件的View,然后調(diào)用其方法重新開始倒計(jì)時(shí)。

          其中refreshNewArrival()方法同上。
          @Overridepublic void onResume() {    super.onResume();    //重新開始倒計(jì)時(shí)    refreshNewArrival();}


          技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。

          推薦閱讀:

          音視頻面試基礎(chǔ)題

          OpenGL ES 學(xué)習(xí)資源分享

          開通專輯 | 細(xì)數(shù)那些年寫過(guò)的技術(shù)文章專輯

          NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來(lái)了

          推薦幾個(gè)堪稱教科書級(jí)別的 Android 音視頻入門項(xiàng)目

          覺得不錯(cuò),點(diǎn)個(gè)在看唄~


          瀏覽 67
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人大香蕉网站 | 日本一级A片色情一区二区 | 高清+国产无码在线观看 | 国产精品成人久久久久久久 | 亚洲综合在线网 |