找回密码
 会员注册
查看: 9|回复: 0

抢购倒计时自定义控件的实现与优化

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
72725
发表于 2024-10-5 08:04:42 | 显示全部楼层 |阅读模式
互联网客户端团队Liu Zhiyi、Zhen Yiqing一、 前言随着网购的持续发展,抢购类倒计时在各类电商应用中已十分常见,这种设计可以提高用户的点击率和下单率等。但是国内的电商应用大部分都仅支持中文,不适配其他的语言,因此当倒计时与其他文案处于同一行展示时,无需考虑倒计时的展示方式。在海外应用中,由于需要适配各种语言,有些小语种的文案较长,因此当倒计时和其他文案处于同一行展示时,需要充分考虑多语言的适配,如何优雅地完成倒计时自适应显示是一个值得深思的问题。为进一步优化倒计时效果,我们为倒计时增加了数字滚动动画,如下图所示。倒计时的功能必然会带来性能的消耗,如何避免倒计时带来的性能问题,本文也将给出相应的解决方案。二、 实现倒计时基本功能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.2view的构建与绑定其次,初始化自定义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构建内部使用的私有方法首先构造设置剩余时间的方法,入参是剩余的毫秒数,在方法内部将时间转化为具体的天时分秒,并将结果赋予给textviewprivate 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 它的实际展示效果如下图所示但是此布局只能展示单行能展示所有内容的情况,因此还需要在此布局上拓展双行展示的情况,再看一看main_list_item_home_new_arrival的布局 它的实际展示效果如下图所示在类中将以上两个view分别进行实例关联。View.inflate(getContext(), R.layout.main_list_item_home_new_arrival, this);mBaseCountDownTimerViewShort = findViewById(R.id.count_down_timer_short); //行尾倒计时viewmBaseCountDownTimerViewLong = 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() 然后通过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时就会调用这个方法。@Overrideprotected void onDetachedFromWindow() { super.onDetachedFromWindow(); //移出屏幕调用,暂停倒计时 stopCountDownTimerAndAnimation();}如果应用需要兼容安卓7以下,则上述方法会失效,因为onDetachedFromWindow()方法并不兼容低版本。但是可是通过重写onStartTemporaryDetach()方法实现相同的效果。@Overrideprotected void onDetachedFromWindow() { super.onDetachedFromWindow(); //移出屏幕调用,暂停倒计时 stopCountDownTimerAndAnimation();}通过tab切换到其他Fragment当倒计时控件位于可视范围内,此时通过tab切换到其他Fragment时,需要暂停倒计时。该情况下倒计时控件所在的Fragment会隐藏,可以在Fragment隐藏时获取倒计时控件的View,然后调用其方法暂停倒计时。@Overridepublic void onFragmentHide() { super.onFragmentHide(); //暂停倒计时 stopNewArrivalCountDownTimerAndAnimation();}为了获取倒计时控件所在的View对象,通过遍历ListView可视范围内的子View,判断其是否是倒计时控件所在的View对象。然后调用倒计时控件所在View对象的stopCountDownTimerAndAnimation()方法,暂停倒计时。/** * 获取倒计时控件所在的view对象,暂停倒计时 */private void stopNewArrivalCountDownTimerAndAnimation() { if (mListView != null) { for (int index = 0; index
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 23:49 , Processed in 0.829542 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表