Android自定义View实现竖直跑马灯效果案例解析
首先给出跑马灯效果图
中间的色块是因为视频转成gif造成的失真,自动忽略哈。
大家知道,横向的跑马灯android自带的textview就可以实现,详情请百度【android跑马灯效果】。但是竖直的跑马灯效果原生android是不支持的。网上也有很多网友实现了自定义的效果,但是我一贯是不喜欢看别人的代码,所以这篇博客的思路完全是我自己的想法哈。
首先,我们需要给自定义的控件梳理一下格局,如下图所示:
1、首先我们将控件分为三个区块,上面绿色部分为消失不可见的块,中间黑色部分为可见区域,下面红色部分为欲出现不可见区域。蓝色的线代表的是整个控件的上线和下线。
2、首先我们只给出两个文字块在内存中,分别是黑色部分的可见块和红色部分的欲出现块。
3、求出这些块的宽度、高度与中心点的坐标值。
4、滚动时,动态地改变每个块的中心点y坐标,使之向上平移。
5、当平移结束后,可见块位于欲消失的不可见块,欲出现的可见块位于可见区域的文字块。此时将欲消失的文字块移除list,并重新设置后一个索引的text和重心坐标值,重新加入list中,刷新。
6、用一个handler来处理动画的间隔时间。用属性动画valueanimator来实现平移的动画效果。
下面开始代码讲解,首先是用链式设置法设置一些常规属性:
<span style="font-size:14px;"> public verticalmarqueeview color(int color){ this.color = color; return this; } public verticalmarqueeview textsize(int textsize){ this.textsize = textsize; return this; } public verticalmarqueeview datas(string[] datas){ this.datas = datas; return this; } public void commit(){ if(this.datas == null || datas.length == 0){ log.e("verticalmarqueeview", "the datas's length is illegal"); throw new illegalstateexception("may be not invoke the method named datas(string[])"); } paint.setcolor(color); paint.settextsize(textsize); }</span>
最后一定要调用commit方法进行提交,通过代码可以看出来这里除了有排空措施,还有最重要的一步:设置字体的大小。
然后我抽象出一个文字块的bean类:
public class textblock { private int width; private int height; private string text; private int drawx; private int drawy; private paint paint; private int position; public textblock(int width, int height, paint paint){ this.width = width; this.height = height; this.paint = paint; } public void reset(int centery){ reset(text, centerx, centery, position); } public void reset(string text, int centery){ reset(text, centerx, centery, position); } public void reset(string text, int centery, int position){ reset(text, centerx, centery, position); } public void reset(string text, int centerx, int centery, int position){ this.text = text; this.position = position; int measurewidth = (int)paint.measuretext(text); drawx = (width - measurewidth) / 2; fontmetrics metrics = paint.getfontmetrics(); drawy = (int)(centery + (metrics.bottom - metrics.top) / 2 - metrics.bottom); } public int getposition(){ return position; } public void draw(canvas canvas){ canvas.drawtext(text, drawx, drawy, paint); } }
这个bean类,最重要的方法就是几个重载的reset方法,通过改变centery的值,来动态得改变绘制文字的起点实现居中绘制。关于文字的居中绘制请参考百度【android canvas 居中绘制文本】。
然后是重写onmeasure方法
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec){ super.onmeasure(widthmeasurespec, heightmeasurespec); if(this.datas == null || this.datas.length == 0){ log.e("verticalmarqueeview", "the datas's length is illegal"); return; } width = measurespec.getsize(widthmeasurespec) - getpaddingleft() - getpaddingright(); height = measurespec.getsize(heightmeasurespec) - getpaddingtop() - getpaddingbottom(); centerx = width / 2; centery = height / 2; blocks.clear(); //添加显示区域的文字块 textblock block1 = new textblock(width, height, paint); block1.reset(datas[0], centerx, centery, 0); blocks.add(block1); if(datas.length > 1){ textblock block2 = new textblock(width, height, paint); block2.reset(datas[1], centerx, centery + height, 1); blocks.add(block2); } }
在这个方法中,首先进行非空判断以免出现致命逻辑错误。然后得到整个控件的宽高和重心坐标。然后实例化两个文字块textblock,第一个文字块通过reset设置中点y坐标为整个控件的中点y坐标,第二个文字块通过reset设置中点y坐标为centery+height,意思就是置于下一个文字块的不可见区域内。
然后是ondraw方法,这个方法非常简单,已经将业务逻辑转交给textblock的draw方法了。
@override protected void ondraw(canvas canvas){ for(int i = 0; i < blocks.size(); i++){ blocks.get(i).draw(canvas); } }
最关键的就是滚动效果的实现了,首先我们给出两个方法,开始滚动和结束滚动。
public void startscroll(){ isstopscroll = false; if(datas == null || datas.length == 0 || datas.length == 1){ log.e("verticalmarqueeview", "the datas's length is illegal"); return; } if(!isstopscroll){ handler.postdelayed(new runnable(){ @override public void run(){ scroll(); if(!isstopscroll){ handler.postdelayed(this, duration_scroll); } } }, duration_scroll); } } public void stopscroll(){ this.isstopscroll = true; }
原理很简单,首先给出一个boolean标志isstopscroll。然后用handler的postdelayed方法进行循环延迟得调用scroll方法进行滚动。接下来给出全文最重要的方法,scroll方法。
private void scroll(){ valueanimator animator = valueanimator.ofpropertyvaluesholder(propertyvaluesholder.ofint("scrolly", centery, centery - height)); animator.setduration(duration_animator); animator.addupdatelistener(new animatorupdatelistener(){ @override public void onanimationupdate(valueanimator animation){ int scrolly = (int)animation.getanimatedvalue("scrolly"); blocks.get(0).reset(scrolly); blocks.get(1).reset(scrolly + height); invalidate(); } }); animator.addlistener(new animatorlistener(){ @override public void onanimationstart(animator animation){ } @override public void onanimationrepeat(animator animation){ } @override public void onanimationend(animator animation){ //移除第一块 int position = blocks.get(1).getposition(); textblock textblock = blocks.remove(0); //最后一个 if(position == datas.length - 1){ position = 0; }else{ position ++; } textblock.reset(datas[position], centery + height, position); blocks.add(textblock); invalidate(); } @override public void onanimationcancel(animator animation){ } }); animator.start(); }
首先采用valueanimator类进行属性动画,开始值为控件的中点y坐标,结束值为centery-height,意味着要从下往上移动一个文字块的距离。然后在动画更新回调方法中,进行对文字块的reset方法。当动画结束后,得到第二个文字块的position值,然后移除第一个文字块,重新reset这个文字块的索引值,再加入到list的容器中。如此循坏。
最后给一个get方法返回position吧。
public int getcurrentposition(){ if(datas == null || datas.length == 0){ return -1; } if(datas.length == 1 && blocks.size() == 1){ return 0; } return blocks.get(0).getposition(); }
上述就将这个自定义控件的所有代码都呈现出来了,考虑到比较零散,这里讲所有代码都打印出来:
/** * @filename: verticalmarqueeview.java * @author * @description: * @date 2016年7月13日 上午9:32:27 * @copyright cnp corporation */ package cc.wxf.component; import android.animation.animator; import android.animation.animator.animatorlistener; import android.animation.propertyvaluesholder; import android.animation.valueanimator; import android.animation.valueanimator.animatorupdatelistener; import android.content.context; import android.graphics.canvas; import android.graphics.color; import android.graphics.paint; import android.graphics.paint.fontmetrics; import android.os.handler; import android.util.attributeset; import android.util.log; import android.view.view; import java.util.arraylist; import java.util.list; public class verticalmarqueeview extends view{ public static final int duration_scroll = 3000; public static final int duration_animator = 1000; private int color = color.black; private int textsize = 30; private string[] datas = null; private int width; private int height; private int centerx; private int centery; private list<textblock> blocks = new arraylist<textblock>(2); private paint paint = new paint(paint.anti_alias_flag); private handler handler = new handler(); private boolean isstopscroll = false; public verticalmarqueeview(context context, attributeset attrs, int defstyleattr){ super(context, attrs, defstyleattr); } public verticalmarqueeview(context context, attributeset attrs){ super(context, attrs); } public verticalmarqueeview(context context){ super(context); } public verticalmarqueeview color(int color){ this.color = color; return this; } public verticalmarqueeview textsize(int textsize){ this.textsize = textsize; return this; } public verticalmarqueeview datas(string[] datas){ this.datas = datas; return this; } public void commit(){ if(this.datas == null || datas.length == 0){ log.e("verticalmarqueeview", "the datas's length is illegal"); throw new illegalstateexception("may be not invoke the method named datas(string[])"); } paint.setcolor(color); paint.settextsize(textsize); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec){ super.onmeasure(widthmeasurespec, heightmeasurespec); if(this.datas == null || this.datas.length == 0){ log.e("verticalmarqueeview", "the datas's length is illegal"); return; } width = measurespec.getsize(widthmeasurespec) - getpaddingleft() - getpaddingright(); height = measurespec.getsize(heightmeasurespec) - getpaddingtop() - getpaddingbottom(); centerx = width / 2; centery = height / 2; blocks.clear(); //添加显示区域的文字块 textblock block1 = new textblock(width, height, paint); block1.reset(datas[0], centerx, centery, 0); blocks.add(block1); if(datas.length > 1){ textblock block2 = new textblock(width, height, paint); block2.reset(datas[1], centerx, centery + height, 1); blocks.add(block2); } } @override protected void ondraw(canvas canvas){ for(int i = 0; i < blocks.size(); i++){ blocks.get(i).draw(canvas); } } public void startscroll(){ isstopscroll = false; if(datas == null || datas.length == 0 || datas.length == 1){ log.e("verticalmarqueeview", "the datas's length is illegal"); return; } if(!isstopscroll){ handler.postdelayed(new runnable(){ @override public void run(){ scroll(); if(!isstopscroll){ handler.postdelayed(this, duration_scroll); } } }, duration_scroll); } } public void stopscroll(){ this.isstopscroll = true; } private void scroll(){ valueanimator animator = valueanimator.ofpropertyvaluesholder(propertyvaluesholder.ofint("scrolly", centery, centery - height)); animator.setduration(duration_animator); animator.addupdatelistener(new animatorupdatelistener(){ @override public void onanimationupdate(valueanimator animation){ int scrolly = (int)animation.getanimatedvalue("scrolly"); blocks.get(0).reset(scrolly); blocks.get(1).reset(scrolly + height); invalidate(); } }); animator.addlistener(new animatorlistener(){ @override public void onanimationstart(animator animation){ } @override public void onanimationrepeat(animator animation){ } @override public void onanimationend(animator animation){ //移除第一块 int position = blocks.get(1).getposition(); textblock textblock = blocks.remove(0); //最后一个 if(position == datas.length - 1){ position = 0; }else{ position ++; } textblock.reset(datas[position], centery + height, position); blocks.add(textblock); invalidate(); } @override public void onanimationcancel(animator animation){ } }); animator.start(); } public int getcurrentposition(){ if(datas == null || datas.length == 0){ return -1; } if(datas.length == 1 && blocks.size() == 1){ return 0; } return blocks.get(0).getposition(); } public class textblock { private int width; private int height; private string text; private int drawx; private int drawy; private paint paint; private int position; public textblock(int width, int height, paint paint){ this.width = width; this.height = height; this.paint = paint; } public void reset(int centery){ reset(text, centerx, centery, position); } public void reset(string text, int centery){ reset(text, centerx, centery, position); } public void reset(string text, int centery, int position){ reset(text, centerx, centery, position); } public void reset(string text, int centerx, int centery, int position){ this.text = text; this.position = position; int measurewidth = (int)paint.measuretext(text); drawx = (width - measurewidth) / 2; fontmetrics metrics = paint.getfontmetrics(); drawy = (int)(centery + (metrics.bottom - metrics.top) / 2 - metrics.bottom); } public int getposition(){ return position; } public void draw(canvas canvas){ canvas.drawtext(text, drawx, drawy, paint); } } }
最后给出使用的方法,很简单。
package cc.wxf.androiddemo; import android.app.activity; import android.content.context; import android.os.bundle; import android.view.view; import android.widget.toast; import cc.wxf.component.verticalmarqueeview; public class mainactivity extends activity { private verticalmarqueeview vmview; @override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); vmview = (verticalmarqueeview)findviewbyid(r.id.vmview); string[] datas = new string[]{ "南海又开始动荡了","菲律宾到处都在肇事","这次为了一张审判废纸,菲律宾投入了多少成本呢","测试数据4","测试数据5为了长度不一样","就把这条当做测试数据吧" }; vmview.color(getresources().getcolor(android.r.color.black)) .textsize(sp2px(this, 15)) .datas(datas).commit(); vmview.startscroll(); vmview.setonclicklistener(new view.onclicklistener(){ @override public void onclick(view v){ toast.maketext(mainactivity.this, "当前的索引为:" + vmview.getcurrentposition(), toast.length_short).show(); } }); } private int sp2px(context context, int sp){ float density = context.getresources().getdisplaymetrics().scaleddensity; return (int) (sp * density + 0.5f); } @override protected void ondestroy() { super.ondestroy(); //必须要调用,否则内存中会一直无限循环 vmview.stopscroll(); } }
demo就不提供了,自定义view就只有上面一个文件。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 安卓(Android)开发之自定义饼状图