Chinaunix首页 | 论坛 | 博客
  • 博客访问: 912342
  • 博文数量: 253
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2609
  • 用 户 组: 普通用户
  • 注册时间: 2019-03-08 17:29
个人简介

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

文章分类

全部博文(253)

文章存档

2022年(60)

2021年(81)

2020年(83)

2019年(29)

我的朋友

分类: Android平台

2021-04-20 14:49:52

一、 前言


随着网购的持续发展,抢购类倒计时在各类电商应用中已十分常见,这种设计可以提高用户的点击率和下单率等。
但是国内的电商应用大部分都仅支持中文,不适配其他的语言,因此当倒计时与其他文案处于同一行展示时,无需考虑倒计时的展示方式。在海外应用中,由于需要适配各种语言,有些小语种的文案较长,因此当倒计时和其他文案处于同一行展示时,需要充分考虑多语言的适配,如何优雅地完成倒计时自适应显示是一个值得深思的问题。
为进一步优化倒计时效果,我们为倒计时增加了数字滚动动画,如下图所示。倒计时的功能必然会带来性能的消耗,如何避免倒计时带来的性能问题,本文也将给出相应的解决方案。

二、 实现倒计时基本功能

2.1 需求与原理分析

该控件预期展现两种状态,距离活动开始还有X天XX:XX:XX 和距离活动结束还有X天XX:XX:XX,因此需要一个活动状态属性,并通过这个活动开始与否的属性设置时间前的文案。具体时间时分秒之间相互独立,因此将它们拆分成独立的textview进行处理。
倒计时控件的核心是计时器,安卓中已经有现成的CountDownTimer类可供使用以实现倒计时功能。此外,还需要实现一些监听的接口。

2.2 具体实现

2.2.1 回调监听接口设计

首先,定义回调接口

  1. private void init() {
  2.      mDayTextView = findViewById(R.id.days_tv);
  3.      mHourTextView = findViewById(R.id.hours_tv);
  4.      mMinTextView = findViewById(R.id.min_tv);
  5.      mSecondTextView = findViewById(R.id.sec_tv);
  6.      mHeaderText = findViewById(R.id.header_tv);
  7.      mDayText = findViewById(R.id.new_arrival_day);
  8.  }
在该接口中定义三个方法:
onRemain(long millisUntilFinished):倒计时进行中回调的方法,用于后续功能的拓展

onFinish():倒计时结束回调,用于活动状态的切换和计时的暂停等

onArrivalOneMinute():每过一分钟回调,用于定时上报的埋点

2.2.2 view的构建与绑定

其次,初始化自定义view,基于实际开发需求,将整个控件细分为修饰文案、天数、时、分、秒等几个独立的textview,并在自定义BaseCountDownTimerView中初始化:

  1. private void setSecond(long millis) {
  2.  
  3.      long day = millis / ONE_DAY;
  4.      long hour = millis / ONE_HOUR - day * 24;
  5.      long min = millis / ONE_MIN - day * 24 * 60 - hour * 60;
  6.      long sec = millis / ONE_SEC - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60;
  7.  
  8.      String second = (int) sec + ""; // 秒
  9.      String minute = (int) min + ""; // 分
  10.      String hours = (int) hour + ""; // 时
  11.      String days = (int) day + ""; //天
  12.  
  13.      if (hours.length() == 1) {
  14.          hours = "0" + hours;
  15.      }
  16.      if (minute.length() == 1) {
  17.          minute = "0" + minute;
  18.      }
  19.      if (second.length() == 1) {
  20.          second = "0" + second;
  21.      }
  22.  
  23.      if (day == 0) {
  24.          mDayTextView.setVisibility(GONE);
  25.          mDayText.setVisibility(GONE);
  26.      } else {
  27.          setDayText(day);
  28.          mDayTextView.setVisibility(VISIBLE);
  29.          mDayText.setVisibility(VISIBLE);
  30.      }
  31.  
  32.      mDayTextView.setText(days);
  33.  
  34.      if (mFirstSetTimer) {
  35.          mHourTextView.setInitialNumber(hours);
  36.          mMinTextView.setInitialNumber(minute);
  37.          mSecondTextView.setInitialNumber(second);
  38.          mFirstSetTimer = false;
  39.      } else {
  40.          mHourTextView.flipNumber(hours);
  41.          mMinTextView.flipNumber(minute);
  42.          mSecondTextView.flipNumber(second);
  43.      }
  44.  }

2.2.3 构建内部使用的私有方法

首先构造设置剩余时间的方法,入参是剩余的毫秒数,在方法内部将时间转化为具体的天时分秒,并将结果赋予给textview;

  1. private void createCountDownTimer(final int eventStatus) {
  2.        if (mCountDownTimer != null) {
  3.            mCountDownTimer.cancel();
  4.        }
  5.        mCountDownTimer = new CountDownTimer(mMillis, 1000) {
  6.            @Override
  7.            public void onTick(long millisUntilFinished) {
  8.                //策划要求:倒计时为00:00:01时,活动状态刷新,倒计时不展示00:00:00这个状态
  9.                if (millisUntilFinished >= ONE_SEC) {
  10.                    setSecond(millisUntilFinished);
  11.                    //当活动状态为进行中时,每隔一分钟调用一次回调
  12.                    if (eventStatus == HomeItemViewNewArrival.EVENT_START) {
  13.                        mArrivalOneMinuteFlag--;
  14.                        if (mArrivalOneMinuteFlag == Constant.ZERO) {
  15.                            mArrivalOneMinuteFlag = Constant.SIXTY;
  16.                            mOnCountDownTimerListener.onArrivalOneMinute();
  17.                        }
  18.                    }
  19.                }
  20.            }
  21.  
  22.            @Override
  23.            public void onFinish() {
  24.                mOnCountDownTimerListener.onFinish();
  25.            }
  26.        };
  27.    }
需要注意的是,当单位时间为个位数时,为了视觉效果的统一,要在数字前加“0”进行补位。
其次,构建一个创建倒计时的方法,其代码如下:

  1. private void createCountDownTimer(final int eventStatus) {
  2.        if (mCountDownTimer != null) {
  3.            mCountDownTimer.cancel();
  4.        }
  5.        mCountDownTimer = new CountDownTimer(mMillis, 1000) {
  6.            @Override
  7.            public void onTick(long millisUntilFinished) {
  8.                //策划要求:倒计时为00:00:01时,活动状态刷新,倒计时不展示00:00:00这个状态
  9.                if (millisUntilFinished >= ONE_SEC) {
  10.                    setSecond(millisUntilFinished);
  11.                    //当活动状态为进行中时,每隔一分钟调用一次回调
  12.                    if (eventStatus == HomeItemViewNewArrival.EVENT_START) {
  13.                        mArrivalOneMinuteFlag--;
  14.                        if (mArrivalOneMinuteFlag == Constant.ZERO) {
  15.                            mArrivalOneMinuteFlag = Constant.SIXTY;
  16.                            mOnCountDownTimerListener.onArrivalOneMinute();
  17.                        }
  18.                    }
  19.                }
  20.            }
  21.  
  22.            @Override
  23.            public void onFinish() {
  24.                mOnCountDownTimerListener.onFinish();
  25.            }
  26.        };
  27.    }
在该方法中,创建一个倒计时实例CountDownTimer,CountDownTimer() 有两个参数,分别是剩余的总时间和刷新间隔。
在实例的onTick()方法中,调用setSecond()方法在每次间隔时间(也就是1s)后定期刷新view,完成倒计时控件的更新。此外,产品中还有一个一分钟定期上报埋点的需求,也可以在onTick()方法中完成。在实际项目事件中,若有定时的任务需求,也可在该方法中自由设置。最后,还需重写该CountDownTimer的onFinish()方法,触发listener接口里的onFinish()

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

首先是设置倒计时的监听事件:

  1. public void setDownTimerListener(OnCountDownTimerListener listener) {
  2.     this.mOnCountDownTimerListener = listener;
  3. }
其次是外露一个设置初始时间和活动开始或结束文案的方法:

  1. public void setDownTime(long millis) {
  2.     this.mMillis = millis;
  3. }
  4.  
  5.  
  6. public void setHeaderText(int eventStatus) {
  7.     if (eventStatus == HomeItemViewNewArrival.EVENT_NOT_START) {
  8.         mHeaderText.setText("Start in");
  9.     } else {
  10.         mHeaderText.setText("Ends in");
  11.     }
  12. }
最后,也是最重要的,需要给倒计时类设计开始与取消倒计时的方法:

  1. public void startDownTimer(int eventStatus) {
  2.         mArrivalOneMinuteFlag = Constant.SIXTY;
  3.         mFirstSetTimer = true;
  4.         //设置需要倒计时的初始值
  5.         setSecond(mMillis);
  6.         createCountDownTimer(eventStatus);// 创建倒计时
  7.         mCountDownTimer.start();
  8.     }
  9.  
  10.     public void cancelDownTimer() {
  11.         mCountDownTimer.cancel();
  12.     }
在开始倒计时的方法中,初始化倒计时的初始值并创建倒计时,最后调用CountDownTimer实例的start()方法开始倒计时。在取消的方法中,直接调用CountDownTimer实例的cancel()方法取消倒计时。

2.3 倒计时类的实际调用

实际调用倒计时控件时,只需在具体布局中添加该倒计时类布局,在调用的类中实例化BaseCountDownTimerView。接着,使用实例的setDownTime()、setHeaderText()初始化数据,使用setDownTimerListener()给view实例设置监听。
最后调用startDownTimer()开启倒计时。

  1. if (view != null) {
  2.             view.setDownTime(mDuration);
  3.             view.setHeaderText(mEventStatus);
  4.             view.startDownTimer(mEventStatus);
  5.             view.setDownTimerListener(new BaseCountDownTimerView.OnCountDownTimerListener() {
  6.                 @Override
  7.                 public void onRemain(long millisUntilFinished) {
  8.  
  9.                 }
  10.  
  11.                 @Override
  12.                 public void onFinish() {
  13.                     view.cancelDownTimer();
  14.                     if (bean.mNewArrivalType == TYPE_EVENT && mEventStatus == EVENT_START) {
  15.                         mEventStatus = EVENT_END;
  16.                         //活动状态之前为进行中,倒计时变为0,如果还有下一个活动/新品,则刷新为下一个活动/新品的数据
  17.                         refreshNewArrivalBeanDate(bean);
  18.                         onBindView(bean, 1, true, null);
  19.                     } else {
  20.                         setEventStatus(bean);
  21.                     }
  22.                 }
  23.  
  24.                 @Override
  25.                 public void onArrivalOneMinute() {
  26.  
  27.                 }
  28.             });

三、实现倒计时整体布局

3.1 需求描述

在多语言环境或者不同屏幕条件下,某些语种的控件长度过长,需要自适应控件进行折行显示以适应UI规范。

3.2 实施方案

原本考虑只实例化一个自定义倒计时控件的对象,但是在设计对象布局的过程中发现,一个对象不方便同时实现在行尾展示或折行后在第二行行首显示。因此,本文采用了在布局的时候同时预置两个倒计时对象的方法,一个对象位于行尾,另一个位于第二行的行首。
在measure过程中,如果测量得到控件的宽度大于某一个宽度阈值,则初始化次行行首的view,并将行尾的view可见状态置为Gone,若小于某一个宽度阈值,则初始化行尾的view,并将次行行首的view可见状态置为Gone。
首先来看一看xml布局文件,以下是标题加倒计时位于行尾的一个整体布局文件main_view_header_new_arrival;

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android=""
  3.     android:layout_width="match_parent"
  4.     android:layout_height="@dimen/qb_px_48">
  5.  
  6.     <com.example.website.general.ui.widget.TextView
  7.         android:id="@+id/new_arrival_txt"
  8.         android:layout_width="wrap_content"
  9.         android:layout_height="wrap_content"
  10.         android:layout_alignParentStart="true"
  11.         android:layout_centerInParent="true"
  12.         android:layout_marginStart="@dimen/qb_px_20"
  13.         android:text="@string/new_arrival"
  14.         android:textColor="@color/common_color_de000000"
  15.         android:textSize="@dimen/qb_px_16"
  16.         android:textStyle="bold" />
  17.  
  18.     <com.example.website.widget.BaseCountDownTimerView
  19.         android:id="@+id/count_down_timer_short"
  20.         android:layout_width="wrap_content"
  21.         android:layout_height="match_parent"
  22.         android:layout_alignParentEnd="true"
  23.         android:layout_marginEnd="@dimen/qb_px_20"
  24.         android:gravity="center_vertical" />
  25. </RelativeLayout>
它的实际展示效果如下图所示

但是此布局只能展示单行能展示所有内容的情况,因此还需要在此布局上拓展双行展示的情况,再看一看main_list_item_home_new_arrival的布局;

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <merge xmlns:android=""
  3.     xmlns:app=""
  4.     xmlns:tools=""
  5.     android:layout_width="match_parent"
  6.     android:layout_height="wrap_content"
  7.     android:orientation="vertical"
  8.     tools:parentTag="android.widget.LinearLayout">
  9.  
  10.     <LinearLayout
  11.         android:layout_width="match_parent"
  12.         android:layout_height="wrap_content"
  13.         android:orientation="vertical">
  14.  
  15.         <include layout="@layout/main_view_header_new_arrival"/>
  16.  
  17.         <com.example.website.widget.BaseCountDownTimerView
  18.             android:id="@+id/count_down_timer_long"
  19.             android:layout_width="match_parent"
  20.             android:layout_height="wrap_content"
  21.             android:layout_alignParentStart="true"
  22.             android:layout_marginStart="@dimen/qb_px_20"
  23.             android:layout_marginTop="@dimen/qb_px_n_4"
  24.             android:layout_marginEnd="@dimen/qb_px_20"
  25.             android:layout_marginBottom="@dimen/qb_px_8"
  26.             android:gravity="center_vertical" />
  27.     </LinearLayout>
  28.  
  29. </merge>
它的实际展示效果如下图所示
在类中将以上两个view分别进行实例关联。

  1. View.inflate(getContext(), R.layout.main_list_item_home_new_arrival, this);
  2. mBaseCountDownTimerViewShort = findViewById(R.id.count_down_timer_short); //行尾倒计时view
  3. mBaseCountDownTimerViewLong = findViewById(R.id.count_down_timer_long);
通过以上的步骤搞定了两种情况下倒计时控件的布局,接下来就该考虑折行展示的判断条件了。在多语言环境中,标题textview与倒计时view的宽度都是不确定的,因此需要综合考虑两个控件的宽度。
同时,因为策划要求,还需考虑某些语种特殊情况的展示要求。
判断代码如下所示:

  1. private boolean isShortCountDownTimerViewShow() {
  2.         String languageCode = LocaleManager.getInstance().getCurrentLanguage();
  3.         if (Constant.EN_US.equals(languageCode) || Constant.EN_GB.equals(languageCode) || Constant.EN_AU.equals(languageCode)) {
  4.             //因策划要求,美式英语、英国英语、澳大利亚英语,强制在New Arrivals标题栏右侧展示
  5.             return true;
  6.         } else {
  7.             View newArrivalHeader = inflate(mContext, R.layout.main_view_header_new_arrival, null);
  8.             TextView newArrivalTextView = newArrivalHeader.findViewById(R.id.new_arrival_txt);
  9.             LinearLayout countDownTimer = newArrivalHeader.findViewById(R.id.count_down_timer_short);
  10.             int measureSpecW = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  11.             int measureSpecH = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  12.             newArrivalTextView.measure(measureSpecW, measureSpecH);
  13.             countDownTimer.measure(measureSpecW, measureSpecH);
  14.             VLog.i(TAG, countDownTimer.getMeasuredWidth() + "--" + newArrivalTextView.getMeasuredWidth());
  15.  
  16.             if (countDownTimer.getMeasuredWidth() + newArrivalTextView.getMeasuredWidth() <= mContext.getResources().getDimensionPixelSize(R.dimen.qb_px_302)) {
  17.                 return true;
  18.             } else {
  19.                 return false;
  20.             }
  21.         }
  22.     }
在代码中,可以根据实际需要定制具体某几款语言是否换行显示。
而对于剩下的大多数语言,可以使用MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)获取measureSpecW 和 measureSpecH ,第一个参数是系统测量该View后得到的规格值,这里使用0代表省略(在系统对该View绘制之前就直接调用了measure方法,所以宽高为0,该值与最终获取的宽高无关),第二个参数MeasureSpec.UNSPECIFIED代表父容器不对View有任何限制。获取完成后也就顺利完成具体view宽度的测量。
通过该方法的返回值,我们就可以控制两个倒计时view的展示与隐藏,从而达到自适应折行展示的效果。

  1. if (isShortCountDownTimerViewShow()) {
  2.                initCountDownTimerView(mBaseCountDownTimerViewShort, bean);
  3.                mBaseCountDownTimerViewShort.setVisibility(VISIBLE);
  4.                mBaseCountDownTimerViewLong.setVisibility(GONE);
  5.            } else {
  6.                initCountDownTimerView(mBaseCountDownTimerViewLong, bean);
  7.                mBaseCountDownTimerViewShort.setVisibility(GONE);
  8.                mBaseCountDownTimerViewLong.setVisibility(VISIBLE);
  9.            }
此外,该方法也不局限于倒计时控件view,针对多语言中各种各样的自定义view,依然可以使用这种测量方法实现自适应换行的美观展示。

四、实现倒计时动画效果

4.1 倒计时数字滚动动画的原理分析

从效果图上可以看到,时、分、秒都是两位数,且数字的变化规律都相同:首先是从个位数开始变化,旧数字从正常展示区域向上移动一定距离,新数字从下向上移动一定距离到达正常展示区域。如果个位数递减至0,则十位数需要递减,所以变化是十位和个位一起移动。
具体的实现思路为:
1、将时/分/秒的两位数当成一个数字滚动组件;

2、将数字滚动组件的两位数,拆分成一个数字数组,变化操作针对数组中的单个元素操作即可;

3、保存旧数字,将旧数字和新数字的数组元素逐个比较,数字相同的位绘制新数字,数字不同的位一起移动即可;

4、在移动数字时,需要将旧数字向上移动,移动的距离是 0 至 负的最大滚动距离;同时要将新数字向上移动,移动距离为最大滚动距离 至 0;其中最大滚动距离是数字滚动控件的高度,该值需要根据实际的UI稿确定。

4.2 具体实现

4.2.1 倒计时滚动组件初始化

倒计时滚动组件继承自TextView,在构造函数中设置【最大滚动距离】和【画笔相关属性】,这两者都需要根据实际UI稿确定。
其中,最大滚动距离mMaxMoveHeight是UI稿中时/分/秒数字控件的整体高度;画笔设置的字体颜色、大小等,均为UI稿中时/分/秒数字的字体颜色、大小等。
具体代码如下所示:

  1. //构造函数
  2. public NumberFlipView(Context context, @Nullable AttributeSet attrs) {
  3.     super(context, attrs);
  4.  
  5.     mResources = context.getResources();
  6.     //最大滚动高度18dp
  7.     mMaxMoveHeight = mResources.getDimensionPixelSize(R.dimen.qb_px_18);
  8.  
  9.     //设置画笔相关属性
  10.     setPaint();
  11. }
  12.  
  13. //设置画笔相关属性
  14. private void setPaint() {
  15.     //设置绘制数字为白色
  16.     mPaint.setColor(Color.WHITE);
  17.     //设置绘制数字样式为实心
  18.     mPaint.setStyle(Paint.Style.FILL);
  19.     //设置绘制数字字体加粗
  20.     mPaint.setFakeBoldText(true);
  21.     //设置绘制文字大小14dp
  22.     mPaint.setTextSize(mResources.getDimensionPixelSize(R.dimen.qb_px_14));
  23. }

4.2.2 绘制倒计时滚动组件

绘制倒计时数字是通过重写onDraw()实现的。首先拆分旧数字和新数字成为相应的数字数组;具体代码如下所示:

  1. //拆分新数字成为新数字数组
  2. for (int i = 0; i < mNewNumber.length(); i++) {
  3.     mNewNumberArray.add(String.valueOf(mNewNumber.charAt(i)));
  4. }
  5.  
  6. //拆分老数字成为老数字数组
  7. for (int i = 0; i < mOldNumber.length(); i++) {
  8.     mOldNumberArray.add(String.valueOf(mOldNumber.charAt(i)));
  9. }//拆分新数字成为新数字数组
  10. for (int i = 0; i < mNewNumber.length(); i++) {
  11.     mNewNumberArray.add(String.valueOf(mNewNumber.charAt(i)));
  12. }
  13.  
  14. //拆分老数字成为老数字数组
  15. for (int i = 0; i < mOldNumber.length(); i++) {
  16.     mOldNumberArray.add(String.valueOf(mOldNumber.charAt(i)));
  17. }
然后绘制数字:绘制新数字时,逐位判断旧数字和新数字是否相同,如果数字相同,直接绘制新数字;如果数字不相同,旧数字和新数字均需要移动。
具体代码如下所示:

  1. //两位数的newNumber的文字宽度
  2. int textWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_16);
  3.  
  4. float curTextWidth = 0;
  5.  
  6. for (int i = 0; i < mNewNumberArray.size(); i++) {
  7.     //newNumber中每个数字的边界
  8.     mPaint.getTextBounds(mNewNumberArray.get(i), 0, mNewNumberArray.get(i).length(), mTextRect);
  9.     //newNumber中每个数字的宽度
  10.     int numWidth = mResources.getDimensionPixelSize(R.dimen.qb_px_5);
  11.  
  12.     //逐位判断旧数字和新数字是否相同
  13.     if (mNewNumberArray.get(i).equals(mOldNumberArray.get(i))) {
  14.         //数字相同,直接绘制新数字
  15.         canvas.drawText(mNewNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
  16.         getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
  17.  
  18.     } else {
  19.         //数字不相同,旧数字和新数字均需要移动
  20.         canvas.drawText(mOldNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
  21.         mOldNumberMoveHeight + getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
  22.  
  23.         canvas.drawText(mNewNumberArray.get(i), getWidth() * ONE_HALF - textWidth * ONE_HALF + curTextWidth,
  24.         mNewNumberMoveHeight + getHeight() * ONE_HALF + mTextRect.height() * ONE_HALF, mPaint);
  25.  
  26.     }
  27.  
  28.     curTextWidth += (numWidth + mResources.getDimensionPixelSize(R.dimen.qb_px_3));
getWidth()获取的是倒计时控件的整个宽度;textWidth是两位数字的宽度;numWidth是单个数字的宽度;curTextWidth是每个数字水平起始绘制位置的间距,curTextWidth=numWidth+两个数字之间的间距。
十位数字的水平绘制起始位置为getWidth()/2 + textWidth/2;个位数字的水平绘制起始位置为getWidth()/2textWidth/2 + curTextWidth。getHight()获取的是倒计时控件的整个高度;textRect.height()获取的是数字的高度。
旧数字的垂直绘制起始位置为mOldNumberMoveHeight + getHeight()/2 + textRect.height()/2;新数字的垂直绘制起始位置为mNewNumberMoveHeightgetHeight()/2 + textRect.height()/2。

4.2.3 倒计时数字滚动效果实现

旧数字和新数字的滚动效果是通过ValueAnimator不断改变旧数字的滚动距离mOldNumberMoveHeight和新数字的滚动距离mNewNumberMoveHeight实现的。
在规定的动画时间FLIP_NUMBER_DURATION内,mNewNumberMoveHeight需要从最大滚动距离mMaxMoveHeight变为0,mOldNumberMoveHeight需要从0变为负的最大滚动距离mMaxMoveHeight;每次计算出新的滚动距离后,调用invalidate()方法,触发onDraw()方法,不断地绘制旧数字和新数字,以实现数字滚动的效果。
具体代码如下所示:
  1. /*
  2. 利用ValueAnimator,在规定时间FLIP_NUMBER_DURATION之内,将值从MAX_MOVE_HEIGHT变为0,
  3. 每次值变化都赋给mNewNumberMoveHeight,同时将mNewNumberMoveHeight - MAX_MOVE_HEIGHT的值赋给mOldNumberMoveHeight,
  4. 并重新绘制,实现新数字和旧数字的上滑;
  5.  */
  6. mNumberAnimator = ValueAnimator.ofFloat(mMaxMoveHeight, 0);
  7. mNumberAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  8.     @Override
  9.     public void onAnimationUpdate(ValueAnimator animation) {
  10.         mNewNumberMoveHeight = (float) animation.getAnimatedValue();
  11.         mOldNumberMoveHeight = mNewNumberMoveHeight - mMaxMoveHeight;
  12.         invalidate();
  13.     }
  14. });
  15. mNumberAnimator.setDuration(FLIP_NUMBER_DURATION);
  16. mNumberAnimator.start();

4.3 具体使用

首先在布局中引入,用法和TextView相同。下图为时、分、秒对应的布局:

  1. <!---->
  2. <com.example.materialdesginpractice.NumberFlipView
  3.     android:id="@+id/hours_tv"
  4.     android:layout_width="@dimen/qb_px_22"
  5.     android:layout_height="@dimen/qb_px_18"
  6.     android:gravity="center"
  7.     android:background="@drawable/number_bg"
  8.     android:textSize="@dimen/qb_px_14"
  9.     android:textColor="@color/common_color_ffffff"/>
  10.  
  11.  
  12. <!---->
  13. <com.example.materialdesginpractice.NumberFlipView
  14.     android:id="@+id/min_tv"
  15.     android:layout_width="@dimen/qb_px_22"
  16.     android:layout_height="@dimen/qb_px_18"
  17.     android:gravity="center"
  18.     android:background="@drawable/number_bg"
  19.     android:textSize="@dimen/qb_px_14"
  20.     android:textColor="@color/common_color_ffffff"/>
  21.  
  22. <!---->
  23. <com.example.materialdesginpractice.NumberFlipView
  24.     android:id="@+id/sec_tv"
  25.     android:layout_width="@dimen/qb_px_22"
  26.     android:layout_height="@dimen/qb_px_18"
  27.     android:gravity="center"
  28.     android:background="@drawable/number_bg"
  29.     android:textSize="@dimen/qb_px_14"
  30.     android:textColor="@color/common_color_ffffff"/>
然后通过id找到对应的倒计时数字控件:

  1. mHourTextView = findViewById(R.id.hours_tv);
  2. mMinTextView = findViewById(R.id.min_tv);
  3. mSecondTextView = findViewById(R.id.sec_tv)
最后调用时/分/秒倒计时数字控件的方法,设置倒计时初始值或者倒计时新数字。如果是首次进行倒计时,需要调用setInitialNumber()方法设置初始值;否则调用flipNumber()方法设置新的倒计时数值。
具体用法如下所示:

  1. if (mFirstSetTimer) {
  2.     mHourTextView.setInitialNumber(hours);
  3.     mMinTextView.setInitialNumber(minute);
  4.     mSecondTextView.setInitialNumber(second);
  5.     mFirstSetTimer = false;
  6. } else {
  7.     mHourTextView.flipNumber(hours);
  8.     mMinTextView.flipNumber(minute);
  9.     mSecondTextView.flipNumber(second);
  10. }

五、优化倒计时性能

5.1 倒计时数字滚动动画的原理分析

在实现中,倒计时控件是作为ListView的子元素,而且ListView是处于一个Fragment中。
为了减少功耗,需要在倒计时控件不在可见范围内时,暂停倒计时;当倒计时控件重新出现在可见范围内时,重新开始倒计时。下图是倒计时暂停与开始的场景。

5.2 具体实现

5.2.1 暂停倒计时

页面滑动,倒计时控件滑出可视区域,当倒计时控件滑出ListView的可视范围内,需要暂停倒计时。该情况的重点是:需要判断出子view是否已经移出ListView中。
如果应用只需要兼容安卓7及以上,可以通过重写onDetachedFromWindow()方法,在方法体内进行取消倒计时的操作。因为每当子view移出ListView时就会调用这个方法。

  1. @Override
  2. protected void onDetachedFromWindow() {
  3.     super.onDetachedFromWindow();
  4.     //移出屏幕调用,暂停倒计时
  5.     stopCountDownTimerAndAnimation();
  6. }
如果应用需要兼容安卓7以下,则上述方法会失效,因为onDetachedFromWindow()方法并不兼容低版本。但是可是通过重写onStartTemporaryDetach()方法实现相同的效果。

  1. @Override
  2. public void onStartTemporaryDetach() {
  3.     super.onStartTemporaryDetach();
  4.     //移出屏幕调用,暂停倒计时
  5.     stopCountDownTimerAndAnimation();
  6. }
通过tab切换到其他Fragment
当倒计时控件位于可视范围内,此时通过tab切换到其他Fragment时,需要暂停倒计时。该情况下倒计时控件所在的Fragment会隐藏,可以在Fragment隐藏时获取倒计时控件的View,然后调用其方法暂停倒计时。

  1. @Override
  2. public void onFragmentHide() {
  3.     super.onFragmentHide();
  4.  
  5.     //暂停倒计时
  6.     stopNewArrivalCountDownTimerAndAnimation();
  7. }
为了获取倒计时控件所在的View对象,通过遍历ListView可视范围内的子View,判断其是否是倒计时控件所在的View对象。然后调用倒计时控件所在View对象的stopCountDownTimerAndAnimation()方法,暂停倒计时。

  1. /**
  2.  * 获取倒计时控件所在的view对象,暂停倒计时
  3.  */
  4. private void stopNewArrivalCountDownTimerAndAnimation() {
  5.     if (mListView != null) {
  6.         for (int index = 0; index < mListView.getChildCount(); index++) {
  7.             View view = mListView.getChildAt(index);
  8.             if (view instanceof HomeItemViewNewArrival) {
  9.                 ((HomeItemViewNewArrival) view).stopCountDownTimerAndAnimation();
  10.             }
  11.         }
  12.     }
  13. }
应用切换至后台/跳转到其他界面
当倒计时控件位于可视范围内,此时应用切换到至后台 或者 点击倒计时控件所在界面的其他内容,跳转到其他界面,都需要暂停倒计时。由于这些情况都会触发倒计时所在Fragment的onStop()方法。因此可以重写onStop(),并在该方法体内获取倒计时控件的View,然后暂停倒计时。
stopNewArrivalCountDownTimerAndAnimation()方法同上。

  1. @Override
  2. public void onStop() {
  3.     super.onStop();
  4.  
  5.     //暂停倒计时
  6.     stopNewArrivalCountDownTimerAndAnimation();
  7. }

5.2.2 开始倒计时

页面滑动,倒计时控件滑入可视区域 当倒计时控件滑出可视区域后,再次滑入可视区域,会自动调用Adapter的getView()方法,然后调用倒计时控件的onBindView()方法。由于onBindView()方法中会初始化倒计时控件,因此该情况下,无需再手动开始倒计时。 通过tab切换回到倒计时所在的Fragment 通过tab切换回到倒计时控件所在的Fragment,若此时倒计时控件在可视范围内,则需要重新开始倒计时。由于该情况下Fragment会重新显示,因此可以在Fragment显示时获取倒计时控件的View,然后调用其方法重新开始倒计时。

																									
  1. @Override
  2. public void onStop() {
  3.     super.onStop();
  4.  
  5.     //暂停倒计时
  6.     stopNewArrivalCountDownTimerAndAnimation();
  7. }
同样,为了获取倒计时控件所在的View对象,需要通过遍历ListView可视范围内的子View,判断其是否是倒计时控件所在的View对象。然后调用倒计时控件所在View对象的refreshEventStatus ()方法,开始倒计时。
  1. /**
  2.  * 获取倒计时控件所在的view对象,开始倒计时
  3.  */
  4. private void refreshNewArrival() {
  5.     if (mListView != null) {
  6.         for (int index = 0; index < mListView.getChildCount(); index++) {
  7.             View view = mListView.getChildAt(index);
  8.             if (view instanceof HomeItemViewNewArrival) {
  9.                 ((HomeItemViewNewArrival) view).refreshEventStatus();
  10.             }
  11.         }
  12.     }
  13. }
应用切换回前台/从其他界面回退
当应用切换到回前台 或者 从其他界面回退到倒计时控件所在的界面,若此时倒计时控件在可视范围内,则都需要重新开始倒计时。由于这些情况都会触发倒计时所在Fragment的onResume()方法。因此可以重写onResume(),并在该方法体内获取倒计时控件的View,然后调用其方法重新开始倒计时。
其中refreshNewArrival()方法同上。
  1. @Override
  2. public void onResume() {
  3.     super.onResume();
  4.     //重新开始倒计时
  5.     refreshNewArrival();
  6. }
作者:vivo 互联网客户端团队Liu Zhiyi、Zhen Yiqing
阅读(1513) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~