自定义View之跑马灯效果

640?wx_fmt=png&wxfrom=5&wx_lazy=1


今日科技快讯


据外媒报道,知名科技媒体Re/code和网上调查平台SurveyMonkey最近进行联合调查,旨在评选出美国人认为对社会积极影响最大的科技公司。调查提出了这样一个问题:“以下哪家公司对当今社会影响最大?”结果显示,位居榜首的是电商巨头亚马逊,它获得了20%受访者的支持。紧随其后的是谷歌,支持率为15%。苹果仅以11%的得票率位居第三。


作者简介


本篇来自 丁瑞 的投稿,分享了安卓开发自定义View之跑马灯,一起来看看!希望大家喜欢。

丁瑞  的博客地址:

https://blog.csdn.net/iamdingruihaha


正文


好久没写东西了,感觉有点虚度光阴了,也感觉有点生疏了,刚好最近项目里面有个跑马灯的需求,TextView一通设置之后还是出现各种冲突,尤其是当TextView与EditText共存的时候,所以干脆自己自定义了一个跑马灯MarqueeView 老规则,先上效果图 ,gif录制出来有点怪,可以安装看看实际效果:

640?wx_fmt=gif

控件具体特性为:

  • 有三种滚动模式 :

    • 滚动一次直接结束

    • 滚动结束一次完全之后,继续第二次滚动

    • 滚动到尾部出现之后,头部紧跟着滚动

  • 文本内容的设置可以是String 类型,也可以是List类型

  • 文本与文本之间的间距可设置

  • 文本滚动的初始位置可设置

  • 文本的大小、颜色、滚动速度可设置

  • 点击文本是否暂停或继续可设置

  • 再次设置文本内容的时候,是否初始化移动位置可设置

此控件效果不是很难,不过很适合对于自定义View不熟悉的同学练一练,尤其是里面那隐隐约约的数学计算和分析,还是蛮有一番滋味的,有需要的朋友也可以省点时间直接用了。

下面来具体说一下实现思路:

自定义控件属性

按照控件特性,就能很容易的自定义出来,这里不多解释了,属性如下:

      <attr name="marqueeview_repet_type" format="enum">
           <enum name="repet_oncetime" value="0"/><!-- 播放一次 -->
           <enum name="repet_interval" value="1"/>  <!--连续播放 循环结束再继续下次循环-->
           <enum name="repet_continuous" value="2"/>  <!--连续播放 到末尾直接继续下次循环-->
       </attr><!--循环模式-->

       <attr name="marqueeview_text_distance" format="integer"/><!--每个item之间的距离-->
       <attr name="marqueeview_text_startlocationdistance" format="float"/><!--开始的起始位置 按距离控件左边的百分比 0~1之间 -->

       <attr name="marqueeview_text_speed" format="float"/><!--播放速度 也就是文字滚动速度-->
       <attr name="marqueeview_text_color" format="color|reference"/><!-- 文字颜色 -->
       <attr name="marqueeview_text_size" format="float"/><!-- 文字大小 -->

       <attr name="marqueeview_isclickalbe_stop" format="boolean"/><!--是否点击暂停-->
       <attr name="marqueeview_is_resetLocation" format="boolean"/><!--重新改变内容的时候 , 是否初始化 位置,默认为true,改变-->

实现文本展示与滚动

首先是文本展示,这个很简单,直接在onDraw方法里面调用 drawText(String text, float x, float y, Paint paint)就可以实现,就可以把文本画出来,不过这里面存在个大大的坑,就是这里面的y坐标getHeight() / 2 + textHeight / 2,文本高度计算如果直接调用rect.height()方法获得的值是比实际高度大的,就导致了文本会偏下一点,准确的获取方法为:

  private float getContentHeight() {

       Paint.FontMetrics fontMetrics = paint.getFontMetrics();
       return Math.abs((fontMetrics.bottom - fontMetrics.top)) / 2;

   }

下面就是让他滚动起来,实现方式是开启死循环,在很短的时间内,改变xLocation的值,再调用 postInvalidate() 方法重新绘制文本就实现了滚动。

   @Override
   public void run() {
       while (isRoll && !TextUtils.isEmpty(content)) {
           try {
               Thread.sleep(10);
               xLocation = xLocation - speed;
               postInvalidate();//每隔10毫秒重绘视图
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }

}

实现三种滚动模式

需求的地基已经实现了,很简单,难的就是添枝加叶了,先来看下MarqueeView的重要属性:

  private String string;//最终绘制的文本
   private float speed = 1;//移动速度
   private int textColor = Color.BLACK;//文字颜色,默认黑色
   private float textSize = 12;//文字颜色,默认黑色
   private int textDistance1= 10;//item间距,dp单位,默认10dp
   private int textdistance ;//textDistance1 转化而来的px宽度
   private String black_count = "";//间距转化成空格距离

   private int repetType = REPET_INTERVAL;//滚动模式
   public static final int REPET_ONCETIME = 0;//一次结束
   public static final int REPET_INTERVAL = 1;//一次结束以后,再继续第二次
   public static final int REPET_CONTINUOUS = 2;//紧接着 滚动第二次

   private float xLocation = 0;//文本的x坐标
   private int contentWidth;//内容的宽度

   private float startLocationDistance = 1.0f;//开始的位置选取,百分比来的,距离左边,0~1,0代表不间距,1的话代表,从右面,1/2代表中间。
   private boolean isClickStop = false;//点击是否暂停
   private boolean isResetLocation = true;//默认为true
   private boolean isRoll = false;//是否继续滚动
   private float oneBlack_width;//空格的宽度

其中repetType是该View的核心围绕点,根据repetType的值,在滚动的时候根据 contentWidth(注意是内容的宽度)与xLocation(滚出屏幕左边的距离)的值的大小来采取相应的措施就可以了。

  • 如果是REPET_ONCETIME //一次结束,此时只需要判断是否contentWidth < (-xLocation),如果是的话,停止死循环就可以了,具体是 stopRoll()方法:

     /**
    * 停止滚动
    */

   public void stopRoll() {
       isRoll = false;
       if (thread != null) {
           thread.interrupt();
           thread = null;
       }

   }
  • 如果是 REPET_INTERVAL //一次结束以后,再继续第二次,同样判断contentWidth <= (-xLocation)如果为true 的话,我们令 xLocation = getWidth(); 就直接实现了需求。

  • 如果是 REPET_CONTINUOUS //首尾相连 滚动第二次,这种方式是比较复杂的, 
    这种模式的滚动,首先我们可以在脑海中想象一下,当内容本身的宽度本来就大于屏幕宽度是什么样的,当内容本身很短的时候,又是什么样子的。如果想象不出,我把两种状态画出来就明白了,刚好直观理解一下contentWidth,他不是指文本总长度,而是指初始化设置的文本的宽度。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

首先我们要明确,REPET_CONTINUOUS模式下在给控件设置文本的时候,就要考虑到控件可以同时容纳几个 文本,计算方法我统一成了 int contentCount = (getWidth() / contentWidth) + 2;也就是当宽度盛不下一个文本的时候,(getWidth() / contentWidth)为0,加2得contentCount为2,我们复制两份去添加。而当宽度盛下三个,不足4个的时候,我们复制五分去添加 ,不过这里要注意,复制的文本之间存在要存在间距,也就是说 此时contentWidth为文本宽度加上间距,而间距是可以设置的,这里我的处理是把宽度量化为” “(空格),也就是转化为几个空格的宽度,然后追加上去,这里处理有点low,不过没想大其他好的办法。设复制完的初始化内容就叫string。

这里把间距转化为空格的个数,首先要计算出空格的宽度,然后把宽度除以空格的宽度,得到空格的个数,然后就能用空格来表示我们要的间距了。这里有个有意思的事情,先看三个字符串:

String s1="   ";
String s2="嗯    ";
String s3="嗯     嗯";

这里用rect去测量以后,其中s1的宽度为0,s2的宽度其实就是一个“嗯”的宽度,而s3得到的是实际宽度。所以就采用了差值去计算的思路:

  /**
    * 计算出一个空格的宽度
    * @return
    */

   private float getBlacktWidth() {

       String text1 = "en en";
       String text2 = "enen";
       return getContentWidth(text1) - getContentWidth(text2);

   }

这样初始化文本就完成了,那么当初始化的滚动到最后了怎么办呢?可以脑中意淫一下,我想到了两种方法去解决: 

首先

640?wx_fmt=jpeg

也就是当移动到左边的距离xLocation 刚好等于contentWidth的时候,我们再把xLocation 重置为0 ,这样就出现了他一直在滚动的障眼法,实现方式是 

if ((-xLocation) == contentWidth){ 
   xLocation=0;
}

不过这种方法只能意淫了,不管xLocation的值重置为0 ,还是speed(滚动速度),还是-speed;都会出现抖动一下的bug,所以想了第二个办法。

640?wx_fmt=jpeg

解释一下,这个方法xLocation一直在负增长,每当xLocation负增长一个contentWidth长度的时候,我们在string 后面在追加一个文本就可以了,实现方式:

 int beAppend = (int) ((-xLocation) / contentWidth);

if (beAppend >= repetCount) {
    repetCount++;
    string = string + content;
}

有同学就疑惑为何不直接用(-xLocation)%contentWidth==0来判断呢,这是为了避免,移动的速度大的时候捕捉不到这个临界点,所以采取了整除然后去比较。

初始化的一些属性

首先是xLocation 的初始值:xLocation = getWidth() * startLocationDistance, 其中 startLocationDistance是我们设置的一个百分比。

设置文本内容的方法用了个重载,当传入List类型的时候,先把List遍历转化成了String

 /**
    * 设置滚动的条目内容  字符串形式的
    *
    * @parambt_control00
    */

   public void setContent(String content2) {
       if (TextUtils.isEmpty(content2)){
           return;
       }
       if (isResetLocation) {//控制重新设置文本内容的时候,是否初始化xLocation。
           xLocation = getWidth() * startLocationDistance;
       }

       if (!content2.endsWith(black_count)) {
           content2 = content2 + black_count;//避免没有后缀
       }
       this.content = content2;

       //这里需要计算宽度啦,当然要根据模式来搞
       if (repetType == REPET_CONTINUOUS) {
//如果说是循环的话,则需要计算 文本的宽度 ,然后再根据屏幕宽度 , 看能一个屏幕能盛得下几个文本

           contentWidth = (int) (getContentWidth(content) + textdistance);//可以理解为一个单元内容的长度

           repetCount = 0;//从0 开始计算重复次数了, 否则到最后 会跨不过这个坎而消失。
           int contentCount = (getWidth() / contentWidth) + 2;
           this.string = "";
           for (int i = 0; i <= contentCount; i++) {
               this.string = this.string + this.content;//根据重复次数去叠加。
           }

       } else {
           if (xLocation < 0 && repetType == REPET_ONCETIME) {
               if (-xLocation > contentWidth) {
                   xLocation = getWidth() * startLocationDistance;
               }
           }
           contentWidth = (int) getContentWidth(content);

           this.string = content2;
       }

       if (!isRoll) {//如果没有在滚动的话,重新开启线程滚动
           continueRoll();
       }


   }

文字的基本属性:

   /**
    * 设置文字大小
    *
    * @param textSize
    */

   public void setTextSize(float textSize) {
       if (textSize > 0) {
           this.textSize = textSize;
           paint.setTextSize(dp2px(textSize));//文字颜色值,可以不设定
           contentWidth = (int) (getContentWidth(content) + textdistance);//大小改变,需要重新计算宽高
       }
   }

这个文字大小设置以后,要注意把间距再contentWidth重新测量获取,要不会导致错乱的。关于文本间距textdistance的设置,还是比较复杂的

/**
    * 设置文字间距  不过如果内容是List形式的,该方法不适用 ,list的数据源,必须在设置setContent之前调用此方法。
    * @param textdistance2
    */

   public void setTextDistance(int textdistance2) {


       //设置之后就需要初始化了

       String black = " ";
       oneBlack_width = getBlacktWidth();//空格的宽度
       textdistance2 = dp2px(textdistance2);
       int count = (int) (textdistance2 / oneBlack_width);//空格个数,有点粗略,有兴趣的朋友可以精细

       if (count == 0) {
           count = 1;
       }

       textdistance = (int) (oneBlack_width * count);
       black_count = "";
       for (int i = 0; i <= count; i++) {
           black_count = black_count + black;//间隔字符串
       }
       setContent(content);
   }

不单单的是个thi.textdistance=textdistance就解决了,因为这个属性设置以后如果我们不去重新把他使用给现有文本,是没有效果的,所以需要重新改变black_count(几个空格组成的字符串),再调用setContent(String string)方法,改变间距。


总结


这样我们的控件基本就完成啦,至于控件的文字颜色、滚动速度等属性就比较简单了,源码已经上传至github,欢迎查看。

https://github.com/385841539/MarqueeView


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

640.png?

640?wx_fmt=jpeg

猜你喜欢

转载自blog.csdn.net/c10WTiybQ1Ye3/article/details/80060593