抢购倒计时自定义控件的实现与优化
二、 实现倒计时基本功能
public interface OnCountDownTimerListener {
/**
* 倒计时正在进行时调用的方法
*
* @param millisUntilFinished 剩余的时间(毫秒)
*/
void onRemain(long millisUntilFinished);
/**
* 倒计时结束
*/
void onFinish();
/**
* 每过一分钟调用的方法
*/
void onArrivalOneMinute();
}
onRemain(long millisUntilFinished): 倒计时进行中回调的方法,用于后续功能的拓展 onFinish():倒计时结束回调,用于活动状态的切换和计时的暂停等 onArrivalOneMinute():每过一分钟回调,用于定时上报的埋点
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 + ""; // 时
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) {
//策划要求:倒计时为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();
}
}
}
}
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;
//设置需要倒计时的初始值
setSecond(mMillis);
createCountDownTimer(eventStatus);// 创建倒计时
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;
//活动状态之前为进行中,倒计时变为0,如果还有下一个活动/新品,则刷新为下一个活动/新品的数据
refreshNewArrivalBeanDate(bean);
onBindView(bean, 1, true, null);
} else {
setEventStatus(bean);
}
}
public void onArrivalOneMinute() {
}
});
三、实现倒计时整体布局
在多语言环境或者不同屏幕条件下,某些语种的控件长度过长,需要自适应控件进行折行显示以适应UI规范
<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>
<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.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
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;
}
}
}
if (isShortCountDownTimerViewShow()) {
initCountDownTimerView(mBaseCountDownTimerViewShort, bean);
mBaseCountDownTimerViewShort.setVisibility(VISIBLE);
mBaseCountDownTimerViewLong.setVisibility(GONE);
} else {
initCountDownTimerView(mBaseCountDownTimerViewLong, bean);
mBaseCountDownTimerViewShort.setVisibility(GONE);
mBaseCountDownTimerViewLong.setVisibility(VISIBLE);
}
四、实现倒计时动画效果
1、将时/分/秒的两位数当成一个数字滚动组件; 2、将数字滚动组件的两位数,拆分成一个数字数组,变化操作针对数组中的单个元素操作即可; 3、保存旧数字,将旧数字和新数字的数组元素逐个比较,数字相同的位绘制新数字,数字不同的位一起移动即可; 4、在移动数字时,需要将旧数字向上移动,移动的距离是 0 至 负的最大滚动距离;同时要将新数字向上移动,移动距离为最大滚动距离 至 0;其中最大滚动距离是数字滚动控件的高度,该值需要根据实际的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));
}
//拆分新数字成为新数字数组
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));
/*
利用ValueAnimator,在规定时间FLIP_NUMBER_DURATION之内,将值从MAX_MOVE_HEIGHT变为0,
每次值变化都赋给mNewNumberMoveHeight,同时将mNewNumberMoveHeight - MAX_MOVE_HEIGHT的值赋给mOldNumberMoveHeight,
并重新绘制,实现新数字和旧数字的上滑;
*/
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.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"/>
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);
}
五、优化倒计时性能
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//移出屏幕调用,暂停倒计时
stopCountDownTimerAndAnimation();
}
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//移出屏幕调用,暂停倒计时
stopCountDownTimerAndAnimation();
}
public void onFragmentHide() {
super.onFragmentHide();
//暂停倒计时
stopNewArrivalCountDownTimerAndAnimation();
}
/**
* 获取倒计时控件所在的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();
}
}
}
}
public void onStop() {
super.onStop();
//暂停倒计时
stopNewArrivalCountDownTimerAndAnimation();
}
页面滑动,倒计时控件滑入可视区域 当倒计时控件滑出可视区域后,再次滑入可视区域,会自动调用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对象,开始倒计时
*/
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();
//重新开始倒计时
refreshNewArrival();
}
技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。
推荐阅读:
觉得不错,点个在看呗~
评论