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

二、 實(shí)現(xiàn)倒計(jì)時(shí)基本功能
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();}
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)
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);}
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);}}
private void createCountDownTimer(final int eventStatus) {if (mCountDownTimer != null) {mCountDownTimer.cancel();}mCountDownTimer = new CountDownTimer(mMillis, 1000) {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();}}}}public void onFinish() {mOnCountDownTimerListener.onFinish();}};}
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;//設(shè)置需要倒計(jì)時(shí)的初始值setSecond(mMillis);createCountDownTimer(eventStatus);// 創(chuàng)建倒計(jì)時(shí)mCountDownTimer.start();}public void cancelDownTimer() {mCountDownTimer.cancel();}
if (view != null) {view.setDownTime(mDuration);view.setHeaderText(mEventStatus);view.startDownTimer(mEventStatus);view.setDownTimerListener(new BaseCountDownTimerView.OnCountDownTimerListener() {public void onRemain(long millisUntilFinished) {}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);}}public void onArrivalOneMinute() {}});
三、實(shí)現(xiàn)倒計(jì)時(shí)整體布局
在多語(yǔ)言環(huán)境或者不同屏幕條件下,某些語(yǔ)種的控件長(zhǎng)度過(guò)長(zhǎng),需要自適應(yīng)控件進(jìn)行折行顯示以適應(yīng)UI規(guī)范
<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.TextViewandroid: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.BaseCountDownTimerViewandroid: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>

<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"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><include layout="@layout/main_view_header_new_arrival"/><com.example.website.widget.BaseCountDownTimerViewandroid: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.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
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;}}}
if (isShortCountDownTimerViewShow()) {initCountDownTimerView(mBaseCountDownTimerViewShort, bean);mBaseCountDownTimerViewShort.setVisibility(VISIBLE);mBaseCountDownTimerViewLong.setVisibility(GONE);} else {initCountDownTimerView(mBaseCountDownTimerViewLong, bean);mBaseCountDownTimerViewShort.setVisibility(GONE);mBaseCountDownTimerViewLong.setVisibility(VISIBLE);}
四、實(shí)現(xiàn)倒計(jì)時(shí)動(dòng)畫效果

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稿確定。
//構(gòu)造函數(shù)public NumberFlipView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);mResources = context.getResources();//最大滾動(dòng)高度18dpmMaxMoveHeight = 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è)置繪制文字大小14dpmPaint.setTextSize(mResources.getDimensionPixelSize(R.dimen.qb_px_14));}
//拆分新數(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ù)的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));
/*利用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() {public void onAnimationUpdate(ValueAnimator animation) {mNewNumberMoveHeight = (float) animation.getAnimatedValue();mOldNumberMoveHeight = mNewNumberMoveHeight - mMaxMoveHeight;invalidate();}});mNumberAnimator.setDuration(FLIP_NUMBER_DURATION);mNumberAnimator.start();
<com.example.materialdesginpractice.NumberFlipViewandroid: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.NumberFlipViewandroid: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.NumberFlipViewandroid: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"/>
mHourTextView = findViewById(R.id.hours_tv);mMinTextView = findViewById(R.id.min_tv);mSecondTextView = findViewById(R.id.sec_tv);
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í)性能

protected void onDetachedFromWindow() {super.onDetachedFromWindow();//移出屏幕調(diào)用,暫停倒計(jì)時(shí)stopCountDownTimerAndAnimation();}
protected void onDetachedFromWindow() {super.onDetachedFromWindow();//移出屏幕調(diào)用,暫停倒計(jì)時(shí)stopCountDownTimerAndAnimation();}
public void onFragmentHide() {super.onFragmentHide();//暫停倒計(jì)時(shí)stopNewArrivalCountDownTimerAndAnimation();}
/*** 獲取倒計(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();}}}}
public void onStop() {super.onStop();//暫停倒計(jì)時(shí)stopNewArrivalCountDownTimerAndAnimation();}
頁(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ì)象,開始倒計(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();}}}}
public void onResume() {super.onResume();//重新開始倒計(jì)時(shí)refreshNewArrival();}

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
推薦閱讀:
開通專輯 | 細(xì)數(shù)那些年寫過(guò)的技術(shù)文章專輯
NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來(lái)了
推薦幾個(gè)堪稱教科書級(jí)別的 Android 音視頻入門項(xiàng)目
覺得不錯(cuò),點(diǎn)個(gè)在看唄~

評(píng)論
圖片
表情
