可缩放时间轴和录像片段选择器的实现

最近的工作是做了两个自定义控件:
①可以缩放的时间轴
②吸附在在时间轴上有两个滑动按钮的录像片段选择器

真机测试效果如下面的gif动画所示:

———————–最近更新 华丽丽的分割线—————————
由于很多小伙伴私信我要源码,所以最近整理了一下,放在github上了,地址:
https://github.com/ljfxyj2008/ScalableTimebar
———————–End of 最近更新—————————

在此记录一下设计原理和踩过的坑。

时间轴

时间轴分为两部分轴,刻度轴和录像片段轴。刻度轴用来绘制时间刻度表盘,录像片段轴用来指示对应位置有没有录像存在。
时间轴的视觉效果如下:

其中下面有时间标注的刻度部分是刻度轴,上面灰色背景一段一段绿色的部分是录像片段轴,如下图的标注所示:

刻度轴

刻度轴的起始时间点、终止时间点、跨越长度、当前滑动到的时间点(即屏幕中间的游标指示的时间)都可以根据传入参数定制。刻度分为大刻度(关键刻度)和小刻度,大刻度要显示刻度和对应的时间文字,小刻度只显示刻度。刻度分为6档,根据缩放级别的不同(用户双指缩放或界面按钮缩放),把刻度打到不同位置。
比如,刚进入界面时刻度轴的刻度以默认档位显示,要在如图的位置打出刻度:

当用户双指在屏幕上拉伸,或点击放大按钮,导致时间轴被放大时,一开始刻度打印方式不变(依然在这几个时刻打印文字),只是刻度间的距离随手指的拉伸距离而逐渐变大。当刻度间距离达到一定程度时,刻度打印方式改变,变成如下图的刻度样式:

当用户双指在屏幕上收缩,或点击缩小按钮,导致时间轴被缩小时,一开始刻度打印方式不变(依然在这几个时刻打印文字),只是刻度间的距离随手指的收缩距离而逐渐变小。当刻度间距离小到一定程度时,刻度打印方式改变,变成如下图的刻度样式:

我的实现方式是,通过继承view实现一个自定义view,在onMeasure()阶段把view宽度设置成所有刻度都绘制出来会占用的长度,即包括超出屏幕的实际长度。由于游标实际是不移动的,游标指示时间的改变是通过刻度轴的反向滑动来实现的,所以为了在时间轴滑动到尽头时游标能够指示到刻度轴尽头的时间,还需要在用户指定的长度基础上,在刻度轴的最左端和最右端分别额外增加屏幕一半宽度的空白区。原理图如下:

刻度档位信息

由于刻度轴根据缩放级别的不同,有6档不同的刻度显示方式(上面展示了3种),所以先把6档刻度信息写进map缓存,map中的每一个item记录这档刻度以下信息:

  • 一屏幕总共要显示的秒数(即可见区域包含的秒数)
  • 大刻度对应的秒数
  • 小刻度对应的秒数
  • 关键刻度文字的显示模式(DataFormat的pattern)

比如刻度轴1对应的这几个值就是6 60 60(一屏幕总共显示6个小时长度)、60 60(大刻度对应的都是整小时,即60 60的整数倍)、5 * 60(小刻度对应的都是5分钟的整数倍)、”HH:mm”(时间文字显示小时和分钟)。

然后,利用手机屏幕宽度screenWidth、用户指定的刻度轴总长度WHOLE_TIMEBAR_TOTAL_SECONDS、以及刚刚在map中设定的一屏幕总共要显示的秒数totalSecondsInOneScreen,就可以计算出某个刻度档位的时间轴总长度(像素):

1
viewLength = (int) ((float) screenWidth * WHOLE_TIMEBAR_TOTAL_SECONDS / (float) totalSecondsInOneScreen);

把算出的这个viewLength也缓存到map的对应item中,作为某一档的默认view宽度。

view宽度设定

在view第一次被初始化时,我们默认把刻度轴的样式设定在第3档的刻度样式,那么就从map中取出对应的刻度档位信息的时间轴总长度字段viewLength,然后通过为view指定新的LayoutParams来指定时间轴view控件的宽度,如下:

123
ViewGroup.LayoutParams params = getLayoutParams();params.width = viewLength;setLayoutParams(params);

这样仅仅是把view宽度指定为有刻度的部分的总长度。为了加上view两端无刻度的留白部分,我们要在onMeasure方法中做一些手脚:

12345678910111213141516171819202122
   protected void (int widthMeasureSpec, int heightMeasureSpec) {              setMeasuredDimension(measureWidth(widthMeasureSpec), VIEW_HEIGHT);   }

    * 计算时间轴的宽度,左右都预留半个屏幕的宽度        */   private int measureWidth(int widthMeasureSpec) {       int measureMode = MeasureSpec.getMode(widthMeasureSpec);       int measureSize = MeasureSpec.getSize(widthMeasureSpec);       int result = getSuggestedMinimumWidth();       switch (measureMode) {           case MeasureSpec.AT_MOST:           case MeasureSpec.EXACTLY:               result = measureSize + screenWidth;               break;           default:               break;       }       return result;   }

这样的话,手机在测量view尺寸的时候就会把多出来的一个屏幕的宽度也算进去。

画刻度onDraw()

刻度怎么画是刻度轴实现的关键。思路是,我们在onDraw()中只绘制出屏幕上可见的那一部分,每次用户滑动或缩放时间轴,都通过invalidate()来重新回调onDraw()方法。 那么关键问题就是如何确定屏幕可见部分在view中的起始点、终止点分别是第几个像素。

不管时间轴view被用户缩放到了什么长度,它的总长度我们总是知道的(因为之所以能看到缩放效果,就是我们手动利用缩放指数factor计算新的长度,然后在onMeasure中重新手动赋值,后面会讲到),同时整个时间轴的跨度有多少秒我们也是知道的(用户需要通过参数指定view的起点、终点时间,类型为long),那么我们就可以计算出一秒钟对应多少个像素:

1
pixelsPerSecond = (float) (getWidth() - screenWidth) / (float) WHOLE_TIMEBAR_TOTAL_SECONDS;

由于用户在初始化时还指定了希望游标指示在哪个时刻上currentTimeInMillisecond,同时我们再利用这一档刻度标准规定的最小刻度间的时间间隔minTickInSecond(在第一部初始化map时指定),我们就可以求出屏幕可见区域最左端、最右端对应的是时间轴上的哪个时刻:

12
long forStartUTC = (long) (currentTimeInMillisecond / 1000 - screenWidth / pixelsPerSecond / 2 - minTickInSecond);long endStartUTC = (long) (currentTimeInMillisecond / 1000 + screenWidth / pixelsPerSecond / 2 + minTickInSecond);

虽然我们知道了屏幕两端对应的时刻,但由于这个时刻很可能是不能被最小刻度间距minTickInSecond整除的,比如我们的最小刻度是5秒一档,但是当前最左端是12:05:02,那么这个地方就不应该打刻度,而应该继续往后早到第一个能被minTickInSecond整除的位置。

这里就有一个大坑。如果直接去用求模运算(%)找第一个刻度点,就会导致手机调整不同时区时,刻度被打在不同位置上的问题,比如我希望00:00、06:00、12:00、18:00才作为关键刻度打印时间文字,但是手机时区跳到北京时间(UTC+8)时,刻度会被打在08:00、14:00、20:00、02:00这几个位置上。更糟糕的是,当缩放到关键刻度只打印年月日而不打印时间的级别时,看起来游标指在2015-12-09的位置上,用户会以为这是北京时间2015-12-09 00:00:00,而实际他指向的是北京时间2015-12-09 08:00:00。

要解决这个问题,就要在找刻度、画刻度的时候考虑手机设定时区与UTC的时差,用UTC时间加上时差的结果来计算刻度的位置,但是时间轴上每个点代表的时间仍然是UTC时间,打印关键刻度文字传给DataFormatter的参数也仍然是UTC时间,因为DataFormat我们在默认不指定时区的情况下就是把UTC时间转成手机设定的本地时区时间来显示的。找屏幕要画出的第一个刻度的代码如下:

123456789101112131415161718192021
       *找出当前屏幕要画出的第一个刻度对应的时刻       */      Calendar cal = Calendar.getInstance();      int zoneOffsetInSeconds = cal.get(java.util.Calendar.ZONE_OFFSET) / 1000;//手机本地时区与UTC相差的毫秒数

      long forStartUTC = (long) (currentTimeInMillisecond / 1000 - screenWidth / pixelsPerSecond / 2 - timebarTickCriterionMap.get(currentTimebarTickCriterionIndex).minTickInSecond);      long endStartUTC = (long) (currentTimeInMillisecond / 1000 + screenWidth / pixelsPerSecond / 2 + timebarTickCriterionMap.get(currentTimebarTickCriterionIndex).minTickInSecond);

      long forStartLocalTimezone = forStartUTC + zoneOffsetInSeconds;      long endStartLocalTimezone = endStartUTC + zoneOffsetInSeconds;

      long firstTickToSeeInSecondUTC = -1;//屏幕上能看到的最左边一个刻度对应的时间,单位是秒      for (long i = forStartLocalTimezone; i <= endStartLocalTimezone; i++) {          if (i % timebarTickCriterionMap.get(currentTimebarTickCriterionIndex).minTickInSecond == 0) {              firstTickToSeeInSecondUTC = i - zoneOffsetInSeconds;              break;

          }

      }

其中timebarTickCriterionMap.get(currentTimebarTickCriterionIndex).minTickInSecond就是用来取到当前刻度档位的最小刻度时间间距。
这里为了避免最边缘的第一个和最后一个刻度绘制不出来,所以在forStartUTC和endStartUTC里都多给了一个最小刻度的余量,也就是代码第7行和第8行最后加减的一个最小刻度间距minTickInSecond。
这样,我们就找到了在本次onDraw()时,我们需要从view的第几个像素开始画刻度(view最左端的位置为第0个像素,大部分之间可能在屏幕之外很远的地方)。

然后从firstTickToSeeInSecondUTC的位置开始,以minTickInSecond为步进值,找每一个时刻,把每一个刻度也加上本地与UTC的时差,然后去对关键刻度间距keyTickInSecond和小刻度间距minTickInSecond分别求模运算(%),能被keyTickInSecond整除的就是关键刻度,只能被minTickInSecond整除的就是小刻度。如此,再调用canvas.drawRect()、canvas.drawText()分别画刻度和文字。

双指缩放

用户在触摸屏上双指缩放或点击放大、缩小按钮时,时间轴要产生缩放效果,原理很简单,就是拿缩放比例scaleFactor乘以当前整个个view宽度(不含一屏幕的空白区)最为新的宽度,然后拿这个宽度和每一档刻度标准中view整体长度字段进行比较,在合适的位置进行档位切换,或者在算出view宽度过大或过小时进行限制。总之,就是算出一个新的view宽度值,然后重设view的LayoutParams,onMeasure()和onDraw(),就可以看到缩放效果了。

1234567891011121314151617181920212223242526272829303132333435363738394041
   * 按照比例对时间轴进行缩放   * @param scaleFactor 缩放比例   */  public void scaleTimebarByFactor(float scaleFactor){      int newWidth = (int) ((getWidth() - screenWidth) * scaleFactor);

      if (newWidth > timebarTickCriterionMap.get(0).viewLength) {          setCurrentTimebarTickCriterionIndex(0);          newWidth = timebarTickCriterionMap.get(0).viewLength;

      } else if (newWidth < timebarTickCriterionMap.get(0).viewLength              && newWidth >= getAverageWidthForTwoCriterion(0, 1)) {          setCurrentTimebarTickCriterionIndex(0);

      } else 大专栏  可缩放时间轴和录像片段选择器的实现ord">if (newWidth < getAverageWidthForTwoCriterion(0, 1)              && newWidth >= getAverageWidthForTwoCriterion(1, 2)) {          setCurrentTimebarTickCriterionIndex(1);

      } else if (newWidth < getAverageWidthForTwoCriterion(1, 2)              && newWidth >= getAverageWidthForTwoCriterion(2, 3)) {          setCurrentTimebarTickCriterionIndex(2);

      } else if (newWidth < getAverageWidthForTwoCriterion(2, 3)              && newWidth >= getAverageWidthForTwoCriterion(3, 4)) {          setCurrentTimebarTickCriterionIndex(3);

      } else if (newWidth < getAverageWidthForTwoCriterion(3, 4)              && newWidth >= timebarTickCriterionMap.get(4).viewLength) {          setCurrentTimebarTickCriterionIndex(4);

      } else if (newWidth < timebarTickCriterionMap.get(4).viewLength) {          setCurrentTimebarTickCriterionIndex(4);          newWidth = timebarTickCriterionMap.get(4).viewLength;

      }

      ViewGroup.LayoutParams params = getLayoutParams();      params.width = newWidth;      setLayoutParams(params);  }

录像片段轴

录像片段轴的绘制原理很简单,就是拿到一个List数据源,其中的每一个item都有录像片段的开始时间from、结束之间end,我们就通过换算,得到每个片段对应的开始像素位置、结束像素位置,然后在屏幕的对应位置用canvas.drawRect()画框就行了。
但是考虑到效率问题,如果录像片段数据很多,达到数千甚至数万段(比如很多都是短视频),那么一次性全部绘制出来是不现实的,还是只能绘制屏幕可见部分,然后在时间轴被缩放时,录像片段轴随之缩放,回调onDraw()方法,再次重绘屏幕可见区域。
问题就来了,在保证List中数据片段不重叠(即每一段的from到to与另一段的from到to都无交集)且有序排列的情况下(前一段的to小于后一段的from),如何快速找到哪一个item才是第一个应该被绘制出来的item?遍历list的方法显然太耗时,每次onDraw()都去遍历一遍的话,界面在滑动或缩放时会卡顿得无法接受。

我的解决办法如下:
步骤一:在外界向view设置录像片段轴的数据源List后,用我自己的类CloudRecordExistTimeClips来记录录像片段item信息,其中包括from、to的long值,还包括一个List字段coverDateZeroOClockList用来记录这个item跨越了哪几天,也就是
从startTimeInMillisecond到endTimeInMillisecond所涵盖的所有日期的00:00所对应的毫秒数。比如从2015-11-26 10:10:30到2015-11-29 19:12:55,那么coverDateZeroOClockList就记录“2015-11-26 00:00:00”、“2015-11-27 00:00:00”、“2015-11-28 00:00:00”、“2015-11-29 00:00:00”。具体实现方式如下:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
public static class CloudRecordExistTimeClips {       static long mostLeftDayZeroTime = Long.MAX_VALUE;//所有时间片段最早的开始日期对应的00:00的毫秒数       static long mostRightDayZeroTime = -1;//所有时间片段最晚的开始日期对应的00:00的毫秒数

       private long startTimeInMillisecond;       private long endTimeInMillisecond;

        * 从startTimeInMillisecond到endTimeInMillisecond所涵盖的所有日期的00:00所对应的毫秒数。        * 如从2015-11-26 10:10:30到2015-11-29 19:12:55,        * 那么coverDateZeroOClockList就记录“2015-11-26 00:00:00”、“2015-11-27 00:00:00”、“2015-11-28 00:00:00”、“2015-11-29 00:00:00”        */       private List<Long> coverDateZeroOClockList = new ArrayList<>();

       public CloudRecordExistTimeClips(long startTimeInMillisecond, long endTimeInMillisecond) {           this.startTimeInMillisecond = startTimeInMillisecond;           this.endTimeInMillisecond = endTimeInMillisecond;

           if (startTimeInMillisecond < mostLeftDayZeroTime){               this.mostLeftDayZeroTime = startTimeInMillisecond;           }

           if (endTimeInMillisecond > mostRightDayZeroTime){               this.mostRightDayZeroTime = endTimeInMillisecond;           }

           SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");           SimpleDateFormat zeroTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

           String startTimeDateString = dateFormat.format(startTimeInMillisecond);           String startTimeZeroTimeString = startTimeDateString + " 00:00:00";

           String endTimeDateString = dateFormat.format(endTimeInMillisecond);           String endTimeZeroTimeString = endTimeDateString + " 00:00:00";

           try {               Date startTimeZeroDate = zeroTimeFormat.parse(startTimeZeroTimeString);               Date endTimeZeroDate = zeroTimeFormat.parse(endTimeZeroTimeString);

               long loopZeroDateInMilliseconds = startTimeZeroDate.getTime();               while (loopZeroDateInMilliseconds <= endTimeZeroDate.getTime()){                   coverDateZeroOClockList.add(loopZeroDateInMilliseconds);                   loopZeroDateInMilliseconds = loopZeroDateInMilliseconds + SECONDS_PER_DAY * 1000;               }           } catch (ParseException e) {               e.printStackTrace();           }

       }

       public long getStartTimeInMillisecond() {           return startTimeInMillisecond;       }

       public long getEndTimeInMillisecond() {           return endTimeInMillisecond;       }

       public List<Long> getCoverDateZeroOClockList() {           return coverDateZeroOClockList;       }   }

步骤二:根据每个录像片段对象CloudRecordExistTimeClips覆盖的日期,将每一个item缓存在一个全局Map中,map的key是某个日期00:00:00对应的long,value就是这个片段CloudRecordExistTimeClips对象。比如,一个片段的from是2015-12-08 11:32:22,to值是2015-12-10 11:32:22,那么这个对象就同时处在key为2015-12-08 00:00:002015-12-09 00:00:002015-12-10 00:00:00的map槽中。

步骤三:在onDraw()方法中,我们知道屏幕可见区域左边缘与view的交点对应的时间轴UTC时间(比如2015-12-09 15:30:12),那么我们就以当天0点的时间(即例子的2015-12-09 00:00:00)为key去取出当天第一个片段,从这个片段画起。如果这个key对应的values为null,那么就把key的值往后加一天,继续找,找到找到第一个片段开始画;或者key的时间大于屏幕右边缘的时间还没找到就停止寻找,因为说明屏幕可见区域中没有需要绘制的录像片段。

滑动与缩放

缩放事件毫无疑问直接使用android提供的缩放手势探测器ScaleGestureDetector来检测,直接可以拿到缩放比例参数scaleFactor,然后用上面提到的缩放函数来处理。
滑动可以在onTouchEvent(MotionEvent event)中自己处理ACTION_DOWN、ACTION_MOVE、ACTION_UP事件流,也可以直接使用android提供的手势探测器GestureDetector来处理,拿到滑动的距离deltaX,在原来view的layout位置的基础上计算滑动后应该处于的位置,然后调用layout(left, top, right, top + getHeight())来使view滑动,最后调用invalidate()来发起重绘。

这里踩到一个坑
ScaleGestureDetector和GestureDetector都要在onTouchEvent(MotionEvent event)函数的最前面通过截获event来进行手势处理。如果是一个缩放事件,ScaleGestureDetector已经处理了缩放手势,那么ACTION_DOWN、ACTION_MOVE、ACTION_UP以及其他事件流就不应该再继续在onTouchEvent()后面的ACTION_DOWN、ACTION_MOVE、ACTION_UP等分支中处理了,按照android文档说明,我使用scaleGestureDetector.onTouchEvent(event)的返回值来判断这个event是不是已近被处理过的缩放事件,如果是就直接return不再走后面的流程。
结果证明我很傻很天真,时间轴在被我双指缩放时同时也出现了意外的位移,说明scaleGestureDetector.onTouchEvent(event)的返回值是无效的。
google之后发现,很多开发者都遇到了同样的问题,说这应该是android sdk的一个bug,scaleGestureDetector.onTouchEvent(event)永远只会返回false。所以,在这里要先通过scaleGestureDetector.onTouchEvent(event)调用缩放手势探测器,然后用scaleGestureDetector.isInProgress()来判断本次事件是否被作为缩放手势处理了,代码如下:

1234567891011121314151617181920212223
   public boolean onTouchEvent(MotionEvent event) {        scaleGestureDetector.onTouchEvent(event);       if (scaleGestureDetector.isInProgress()) {           return true;       }

       switch (event.getAction() & MotionEvent.ACTION_MASK) {           case MotionEvent.ACTION_DOWN:               ...               break;

           case MotionEvent.ACTION_MOVE:               ...               break;

           case MotionEvent.ACTION_UP:              	...               break;       }

       return true;}

录像片段选择器

通过两个滑动按钮来选择录像片段,两个滑动按钮之间的部分表示被选中的片段,两个滑动按钮上要显示对应的录像截图、时间文字提示,在滑动时、静止时根据获取成功与否显示不同的占位图。由于录像片段选择器上的时间信息是依附于时间轴控件的,所以当时间轴被滑动、缩放时,录像片段选择器也要同步更新。

很明显,这个控件简化一下就是有两个thumb的seekbar,所以就取github上找了一下两个按钮的seekbar控件,结果找到了anothem/android-range-seek-bar项目,它提供与android的seekbar视觉效果类似的双按钮seekbar控件。
我的改造工作主要就是重写onMeasure()、onLayout()、onDraw()、onTouchEvent()方法,最麻烦的就是onDraw()中绘制时对绝对坐标的计算。其计算方法与时间轴类似,甚至还简单许多,这里就不再重复写了。

这里的一个坑
由于这个自定义view中需要用到android的ImageView(给Glide使用),所以这个自定义view就不能继承于view,而是继承于ViewGroup,这样就可以在初始化阶段向其中添加其他view了。然而调试的时候发现,onDraw()方法怎么都不会被调用。原来,ViewGroup默认只是用来布局,一般没有什么需要绘制的东西,所以系统默认就不调用它的onDraw()回调。如果要让ViewGroup的onDraw()方法被回调,有两种方法:

【方法1】让系统知道这个ViewGroup有可绘制的东西,比如在xml中定义这个ViewGroup的时候为它指定background属性。
【方法2】在代码中调用setWillNotDraw(false)强制要求调用onDraw()方法。

原文地址:https://www.cnblogs.com/sanxiandoupi/p/11712933.html

时间: 2024-10-01 12:32:09

可缩放时间轴和录像片段选择器的实现的相关文章

JS时间轴效果(类似于qq空间时间轴效果)

2013-11-04 23:51 by 空智, 4041 阅读, 15 评论, 收藏, 编辑 在上一家公司写了一个时间轴效果,今天整理了下,感觉有必要写一篇博客出来 给大家分享分享 当然代码还有很多不足的地方,希望大家多指点指点下,此效果类似于QQ空间或者人人网空间时间轴效果,当时也是为了需求 研究了下qq空间逻辑(当然JS代码压缩了肯定看不到的),只是当时研究了下他们HTML结构和css结构,所以仿照他们那种逻辑自己也写了一个出来.先来看看是个什么样的吧!如下图所示: 需求分析:左侧是一个时间

DevExpress之ChartControl实现时间轴实例 z

using System; using System.Data; using System.Windows.Forms; using DevExpress.XtraCharts; namespace DevExpressChart { public partial class winDateTime : Form { public winDateTime() { InitializeComponent(); } private void winDateTime_Load(object sende

垂直时间轴HTML

1.概述 用时间点来展示事件发生点来代替用table展示一条条数据,能够给人清晰.一目了然能够看清事情发生的过程,UI页面也显示的那么清晰.如何用css+html做出时间轴展示事件点的?先来看看下面的效果 做出这样效果的时间轴展示事件点,需要了解一下知识: 1.1.css位置:position 1.2.css伪类:after,befault 1.3.css内容:content 2.CSS中Postion 语法: position : static | absolute | fixed | rel

进度条与时间轴绑定显示图片

第一步是下载Bootstrap和Glyphicons库.你可以找到外部引用Bootstrap CDN主机上的图标字体文件.我分开这些样式表到不同的文件,同时创建一个新的文档称为styles.css. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!doctype html> <html lang="en-US"> <head>   <meta charset="utf-8">   <meta

百度地图API标注+时间轴组合

百度地图API标注+时间轴组合 到新公司实习第八天,Boos让我结合百度地图api做一个动态展示标注变化的组件,要求地图展示某一天的标注,时间轴要求可以动态播放每一天的标注变化...然后我就开始coding... 准备工作: 申请百度api密钥(具体方法我也不多写了,大家应该都会) 了解一下百度地图API的开发指南和类参考文档(如果嫌麻烦的话 可以直接看Demo示例) 一.首先,先加载地图,你可以用实际的经纬度定位.浏览器定位.根据ip定位.根据城市名定位,这个你可以自己选择 // 创建Map实

HighCharts 图表插件 自定义绑定 时间轴数据

HighCharts 图表插件 自定义绑定 时间轴数据,解决时间轴自动显示数据与实际绑定数据时间不对应问题! 可能要用到的源码片段:http://code.662p.com/list/14_1.html     学习示例如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quo

时间轴

在前端网看到个时间轴的效果,故简单模范了个. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> body{margin: 50px;} .container{margin-left: 30%;l

jQuery时间轴插件:jQuery Timelinr

前言 这是一款可用于展示历史和计划的时间轴插件,尤其比较适合一些网站展示发展历程.大事件等场景.该插件基于jQuery,可以滑动切换.水平和垂直滚动.支持键盘方向键.经过扩展后可以支持鼠标滚轮事件. HTML 我们在body中建立一个div#timeline作为展示区,#dates为时间轴,示例中我们用年份作为主轴,#issues作为内容展示区,即展示对应主轴点年份的内容,注意id对应上. <div id="timeline"> <ul id="dates&

Android 时间轴

效果图: 数据是随便填的,显得有点乱,但是不影响效果.实现方面主要是用ListView来实现,主要是根据ListView的item位置与上一条数据进行比较,来控制时间的显示隐藏效果.思路很简单,下面看代码实现: 首先是页面的整体布局,很简单,就一个ListView: res/layout/activity_main.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:and