最近项目需求做个循环滚动字幕功能,自己找了相关资料,分别实现了垂直滚动和水平滚动方式,根据自己的风格用两种方法实现了该功能;
2015-12-15更新:以前实现效果并不理想(文字的格式排版,自定义颜色,大小字号等TextView原有属性都不能很好兼容实现),现改为ScrollView+TextView处理方式,
这个支持所有TextView属性,还支持速度,垂直滚动textview加ScrollView实现,水平滚动textview加HorizontalScrollView实现
具体实现:
public class TextPlayer extends BasePlayer { private TextViewAttr attrs = null; private View vroot = null; private TextView txtView = null; private Animation mMoveTextOut = null; // private Animation mMoveTextIn = null; private Animation mMoveTextUp = null; // private Animation mMoveTextDown=null; private boolean mMarqueeNeeded = false; private static final String TAG = "TextMarqueeView112"; private static final int TEXTVIEW_VIRTUAL_WIDTH = 990000; private static final int TEXTVIEW_HEIGHT = 990000; /** * Control the speed. The lower this value, the faster it will scroll. */ public static final int SPEED_NORMAL = 20; public static final int SPEED_FAST = 12; public static final int SPEED_LOW = 30; /** * Control the pause between the animations. Also, after starting this activity. */ private static final int DEFAULT_ANIMATION_PAUSE = 1000; private int mSpeedHorizontal = SPEED_NORMAL; private int mSpeedVertical = SPEED_NORMAL * 2; private int mAnimationPause = DEFAULT_ANIMATION_PAUSE; private boolean mAutoStart = true; private Interpolator mInterpolator = new LinearInterpolator(); private boolean mCancelled = false; private Runnable mAnimationStartRunnable; private boolean mStarted = false; private int currentScrollMode = Constance.SCROLL_VERTICAL_UP; private boolean mOnLayoutEnd = false; public TextPlayer(Context ctx ,TextViewAttr attr){ super(ctx); this.attrs = attr; intView(); } @Override public View getView() { return vroot; } public void setHorizontalScrollView(View vroot){ this.vroot = vroot; txtView = (TextView) vroot.findViewById(R.id.textViewHorizontal); this.currentScrollMode = Constance.SCROLL_HORIZONTAL_LEFT; } public void setVerticalScrollView(View vroot){ this.vroot = vroot; txtView = (TextView) vroot.findViewById(R.id.textViewVertical); this.currentScrollMode = Constance.SCROLL_VERTICAL_UP; } private void intView() { LayoutInflater inflater = (LayoutInflater) mCtx.getSystemService (Context.LAYOUT_INFLATER_SERVICE); if(null == vroot) { if (attrs.getScrollDirection() == Constance.SCROLL_HORIZONTAL_LEFT) { vroot = inflater.inflate(R.layout.text_horizontal_scroll, null); txtView = (TextView) vroot.findViewById(R.id.textViewHorizontal); this.currentScrollMode = Constance.SCROLL_HORIZONTAL_LEFT; } else if (attrs.getScrollDirection() == Constance.SCROLL_VERTICAL_UP) { vroot = inflater.inflate(R.layout.text_vertical_scroll, null); txtView = (TextView) vroot.findViewById(R.id.textViewVertical); this.currentScrollMode = Constance.SCROLL_VERTICAL_UP; } } if (!TextUtils.isEmpty(attrs.getTextColor())) { txtView.setTextColor(Color.parseColor(attrs.getTextColor())); } if (!TextUtils.isEmpty(attrs.getTextFontSize())) { txtView.setTextSize(Float.parseFloat(attrs.getTextFontSize())); } } private File[] res_files; private int elementsCount; @Override public void setData(File resourceDir) { if(null!=resourceDir && resourceDir.exists()){ // File[] eles = resourceDir.listFiles(); File[] elements = resourceDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.endsWith(".txt"); } }); if(null!=elements && elements.length>0){ res_files = elements; elementsCount = elements.length; } } } public void setTextContent(String content){ if(TextUtils.isEmpty(content)) return; vroot.post(mPlayRun); txtView.setText(content); // Message m = Message.obtain(); // m.what = MSG_CHANGETEXT; // m.obj = content; // mHandler.sendMessageDelayed(m,2000); } // public void setTextAlign(int align){ // int gravity = Gravity.LEFT; // switch (align){ // case Gravity.CENTER_HORIZONTAL: // gravity = Gravity.CENTER_HORIZONTAL; // break; // case Gravity.CENTER_VERTICAL: // gravity = Gravity.CENTER_VERTICAL; // break; // case Gravity.CENTER: // gravity = Gravity.CENTER; // break; // } // txtView.setGravity(gravity); // } Runnable mPlayRun = new Runnable() { @Override public void run() { executeScroll(); } }; private void executeScroll(){ if (this.currentScrollMode == Constance.SCROLL_HORIZONTAL_LEFT) initHorizontalView(); else initVerticalView(); prepareAnimation(); if (mAutoStart) { String text = txtView.getText().toString(); if (TextUtils.isEmpty(text)) return; startMarquee(); } this.mOnLayoutEnd = true; } private Handler mHandler = new RefHandler(this); private final static int MSG_CHANGETEXT = 11; private final static int MSG_READTEXT = 12; private static class RefHandler extends Handler{ Reference<TextPlayer> mTarget = null; RefHandler(TextPlayer player) { mTarget = new WeakReference<>(player); } @Override public void handleMessage(Message msg) { TextPlayer target = mTarget.get(); if(null == target) return; switch (msg.what){ case MSG_CHANGETEXT: target.setTextContent((String) msg.obj); break; case MSG_READTEXT: WorkerThreadPool.getInstance().executeTask(target.readFileTask); break; } } } private int currentIndex =0; Runnable readFileTask = new Runnable() { @Override public void run() { String content = FileUtils.readSDFile(res_files[currentIndex]); Message m = Message.obtain(mHandler); m.what = MSG_CHANGETEXT; m.obj = content; m.sendToTarget(); if(++currentIndex >= elementsCount){ currentIndex = 0; } } }; @Override public void startPlay() { if(0== elementsCount) return; mHandler.sendEmptyMessage(MSG_READTEXT); vroot.post(mPlayRun); } @Override public void pausePlay() { } @Override public void continuePlay() { } @Override public void stopPlay() { mHandler.removeCallbacksAndMessages(null); } @Override public void completePlay() { } @Override public void destroyPlay() { mHandler.removeCallbacksAndMessages(null); } public void startMarquee() { //Log.d(TAG, "startMarquee()"); if (this.currentScrollMode == Constance.SCROLL_HORIZONTAL_LEFT) { if (mMarqueeNeeded && mMoveTextOut != null) startHorizontalAnimation(); } else if (this.currentScrollMode == Constance.SCROLL_VERTICAL_UP) { if (mMarqueeNeeded && mMoveTextUp != null) startVerticalAnimation(); } mCancelled = false; mStarted = true; } private void startHorizontalAnimation() { mAnimationStartRunnable = new Runnable() { @Override public void run() { Log.d(TAG, "startHorizontalAnimation"); txtView.startAnimation(mMoveTextOut); } }; vroot.postDelayed(mAnimationStartRunnable, mAnimationPause); } private void startVerticalAnimation() { mAnimationStartRunnable = new Runnable() { @Override public void run() { Log.d(TAG, "startVerticalAnimation"); txtView.startAnimation(mMoveTextUp); } }; vroot.postDelayed(mAnimationStartRunnable, mAnimationPause); } /** * Disables the animations. */ public void reset() { mCancelled = true; if (mAnimationStartRunnable != null) { vroot.removeCallbacks(mAnimationStartRunnable); } txtView.clearAnimation(); mStarted = false; if (mMoveTextOut != null) mMoveTextOut.reset(); if (mMoveTextUp != null) mMoveTextUp.reset(); // mAnimationStartRunnable=null; vroot.invalidate(); } private void prepareAnimation() { String text = txtView.getText().toString(); if (TextUtils.isEmpty(text)) return; Paint mPaint = txtView.getPaint(); mPaint.setTextSize(txtView.getTextSize()); mPaint.setTypeface(txtView.getTypeface()); Rect mBounds = new Rect(); mPaint.getTextBounds(text, 0, text.length(), mBounds); float textWidth = mBounds.width(); float textHeight = mBounds.height(); // Log.d(TAG, "Rect calucate w h="+textWidth+" :"+textHeight); // Log.d(TAG, "measuredWidth : " + vroot.getMeasuredWidth()+" measuredHeight :"+vroot.getMeasuredHeight()); float padTextWidth = mPaint.measureText(text, 0, 1); /* float mTextWidth = mPaint.measureText(mTextField.getText().toString()); Log.d(TAG, "mTextWidth : " + mTextWidth);*/ if (currentScrollMode == Constance.SCROLL_HORIZONTAL_LEFT) { mMarqueeNeeded = textWidth > vroot.getMeasuredWidth(); //垂直居中坐标 int centerPosY = Math.round(vroot.getMeasuredHeight() / 2 - textHeight / 2); if (false == mMarqueeNeeded || false == attrs.IsScrollMode()) { if (attrs.getTextGravity() == Gravity.RIGHT) txtView.setPadding(Math.round(vroot.getMeasuredWidth() - textWidth - padTextWidth), centerPosY, 0, 0); else if (attrs.getTextGravity() == Gravity.CENTER) txtView.setPadding(Math.round(vroot.getMeasuredWidth() / 2 - textWidth / 2), centerPosY, 0, 0); return; } int duration = (int) Math.ceil(textWidth * mSpeedHorizontal); initHorizontalAnim(vroot.getMeasuredWidth() - padTextWidth, -(textWidth + padTextWidth), centerPosY, duration); // Log.d(TAG, "get w h left top="+vroot.getWidth()+" "+vroot.getHeight()+" "+vroot.getLeft()+" "+vroot.getTop()); // initHorizontalAnim(getLeft()+getMeasuredWidth()/2,getLeft(),8*1000); } else if (currentScrollMode == Constance.SCROLL_VERTICAL_UP) { // int lines=txtView.getLineCount(); int lines = (int) Math.ceil(textWidth / vroot.getMeasuredWidth()); mMarqueeNeeded = lines * textHeight > vroot.getMeasuredHeight();//mTextField.getHeight(); if (false == mMarqueeNeeded || false == attrs.IsScrollMode()) { return; } // Log.d(TAG, "verticalView line count="+lines); // Log.d(TAG, " getView w h"+txtView.getWidth()+", "+txtView.getHeight()); //滚动完最后一行 lines++; float moveHeight = lines * textHeight; int duration = (int) Math.ceil(moveHeight * mSpeedVertical); // Log.d(TAG, "moveDistance="+moveHeight); initVerticalAnim(vroot.getMeasuredHeight() + textHeight * 2, -(moveHeight), duration);// - textHeight * 3 } } private void initHorizontalAnim(float fromX, float toX, float posY, int duration) {//,float locationY // Log.d(TAG, "initHorizontalAnim fromx="+fromX+" toX="+toX); mMoveTextOut = new TranslateAnimation(fromX, toX, posY, posY); mMoveTextOut.setDuration(duration); mMoveTextOut.setInterpolator(mInterpolator); mMoveTextOut.setFillAfter(true); mMoveTextOut.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { // Log.d(TAG, "mMoveTextOut_onAnimationStart"); expandHorizontalTextView(); } @Override public void onAnimationEnd(Animation animation) { //Log.d(TAG, "mMoveTextOut_animEnd"); cutHorizontalTextView(); completePlay(); if (mCancelled) { return; } startHorizontalAnimation(); } @Override public void onAnimationRepeat(Animation animation) { } }); } private void initVerticalAnim(float fromY, float toY, int duration) { mMoveTextUp = new TranslateAnimation(0, 0, fromY, toY); mMoveTextUp.setDuration(duration); mMoveTextUp.setInterpolator(mInterpolator); mMoveTextUp.setFillAfter(true); mMoveTextUp.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {// Log.d(TAG, "mMoveTextUp_onAnimationStart"); expandVerticalTextView(); } @Override public void onAnimationEnd(Animation animation) { //Log.d(TAG, "mMoveTextUp_AnimEnd"); cutVerticalTextView(); completePlay(); if (mCancelled) { return; } startVerticalAnimation(); } @Override public void onAnimationRepeat(Animation animation) { } }); } private void initHorizontalView() { txtView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void afterTextChanged(Editable editable) { //Log.d(TAG, "afterTextChanged"); if (!mOnLayoutEnd) return; final boolean continueAnimation = mStarted; reset(); prepareAnimation(); cutHorizontalTextView(); /* if (continueAnimation) { Log.d(TAG, "TextChange Restart play"); startMarquee(); }*/ vroot.post(new Runnable() { @Override public void run() { if (continueAnimation) { //Log.d(TAG, "TextChange Restart play"); startMarquee(); } } }); } }); } private void expandHorizontalTextView() { ViewGroup.LayoutParams lp = txtView.getLayoutParams(); lp.width = TEXTVIEW_VIRTUAL_WIDTH; txtView.setLayoutParams(lp); } private void cutHorizontalTextView() { if (txtView.getWidth() != vroot.getMeasuredWidth()) { ViewGroup.LayoutParams lp = txtView.getLayoutParams(); lp.width = vroot.getMeasuredWidth(); txtView.setLayoutParams(lp); } } private void initVerticalView() { txtView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void afterTextChanged(Editable editable) { //Log.d(TAG, "afterTextChanged"); if (!mOnLayoutEnd) return; final boolean continueAnimation = mStarted; reset(); prepareAnimation(); cutVerticalTextView(); /* if (continueAnimation) { Log.d(TAG, "TextChange Restart play"); startMarquee(); }*/ vroot.post(new Runnable() { @Override public void run() { if (continueAnimation) { //Log.d(TAG, "TextChange Restart play"); startMarquee(); } } }); } }); } private void expandVerticalTextView() { //Log.d(TAG, "expandVerticalTextView"); ViewGroup.LayoutParams lp = txtView.getLayoutParams(); lp.height = TEXTVIEW_HEIGHT; txtView.setLayoutParams(lp); } private void cutVerticalTextView() {// Log.d(TAG, "cutVerticalTextView"); if (txtView.getHeight() != vroot.getMeasuredHeight()) { ViewGroup.LayoutParams lp = txtView.getLayoutParams(); lp.height = vroot.getMeasuredHeight(); txtView.setLayoutParams(lp); } } }
控制播放器实现
public abstract class BasePlayer implements IPlayer { public abstract View getView(); public abstract void setData(File resourceDir); protected Context mCtx; private PlayerAttr mPlayerAttr; public BasePlayer(Context ctx){ this.mCtx = ctx; } public void setPlayerAttr(PlayerAttr attr){ this.mPlayerAttr = attr; } public Rect getRegion() { return mPlayerAttr.region; } @Override public void startPlay() { } @Override public void pausePlay() { } @Override public void continuePlay() { } @Override public void stopPlay() { } @Override public void completePlay() { } @Override public void destroyPlay() { } } public interface IPlayer { public void startPlay(); public void pausePlay(); public void continuePlay(); public void stopPlay(); public void completePlay(); public void destroyPlay(); }
文本属性相关:
public class TextViewAttr { // 5.backgroundOpacity: 透明属性,backgroundOpacity="0%"; 此时,不允许出现backgroundColor属性 // 6.backgroundColor: 背景色, backgroundColor="色彩值"; 此时,不允许出现backgroundOpacity属性 // 7.textAlign: 文本对齐方式,支持(start|left|inherit,end|right,center) // 8.textMode:滚动模式,支持动态(crawl),静态(inherit) // 9.textFontSize:文本字体大小,平台控制范围 1-64号字 【SIGMA平台受限制】 // 10.textFontStyle:文本字体,参加字体名称与字库的对应表的邮件及相应文档,其中有描述平台支持的几种字体类型 // {SimHei,SimSun,NSimSun,FangSong,KaiTi,Microsoft YaHe,WenQuanYi Micro Hei Mono(wqy-microhei.ttc)} // 11.textColor:字体颜色; // 12.textWritingMode :动态文本的滚动模式,支持从右到左滚动(lr),从左到右滚动(rl),从下到上滚动(tb-lr),从上到下滚动(tb-rl); // 13.textFontWeight:字体加粗模式,支持(light,normal,bold,black),与字号有关,字号超过64,貌似加粗无效 【SIGMA平台受限制】 private String backgroundOpacity; private String backgroundColor; private String textAlign; private String textMode; private String textFontSize; private String textFontStyle; private String textColor; private String textWritingMode; private String textFontWeight; private String clkdisplayMode; public static final String SROLL_DYNIMICMODEL="crawl"; public static final String SROLL_NOTMOVE="inherit"; public TextViewAttr(){ } public TextViewAttr(NamedNodeMap attrs){ backgroundOpacity = getAttribute(attrs,"backgroundOpacity"); backgroundColor=getAttribute(attrs,"backgroundColor"); textAlign = getAttribute(attrs,"textAlign"); textMode = getAttribute(attrs,"textMode"); textFontSize = getAttribute(attrs,"textFontSize"); textFontStyle = getAttribute(attrs,"textFontStyle"); textColor = getAttribute(attrs,"textColor"); textWritingMode = getAttribute(attrs,"textWritingMode"); textFontWeight = getAttribute(attrs,"textFontWeight"); clkdisplayMode = getAttribute(attrs,"clkdisplayMode"); } private int scrollMode = Constance.SCROLL_VERTICAL_UP; public void setHorizontalScrollDirection(){ scrollMode = Constance.SCROLL_HORIZONTAL_LEFT; } public int getScrollDirection(){ // scrollMode = Constance.SCROLL_VERTICAL_UP; /*if("lr".equals(textWritingMode)){ mode = AutoScrollTextView.SCROLL_LEFT_TO_RIGHT; }else if("rl".equals(textWritingMode)){ mode = MarqueeText.SCROLL_RIGHT_TO_LEFT; }else if("tb-lr".equals(textWritingMode)){ mode = MarqueeText.SCROLL_BOTTOM_TO_TOP; }else if("tb-rl".equals(textWritingMode)){ mode = MarqueeText.SCROLL_TOP_TO_BOTTOM; }*/ if("lr".equals(textWritingMode)||"rl".equals(textWritingMode)){ scrollMode = Constance.SCROLL_HORIZONTAL_LEFT; }else if("tb-lr".equals(textWritingMode)||"tb-rl".equals(textWritingMode)){ scrollMode = Constance.SCROLL_VERTICAL_UP; } return scrollMode; } public String getAttribute(NamedNodeMap attrs,String name) { Attr attrNode = (Attr)attrs.getNamedItem(name); String attrValue = ""; if (attrNode != null) { attrValue = attrNode.getValue(); } return attrValue; } public String getBackgroundOpacity() { return backgroundOpacity; } public boolean IsScrollMode() { boolean isScroll=true; //是否为静止 if(false==TextUtils.isEmpty(textMode)&&true==textMode.equals("inherit")){ isScroll=false; } return isScroll; } //文本字体,参加字体名称与字库的对应表 public String getTextFontStyle() { return textFontStyle; } public int getDisplayMode(){ int mode = 0; try{ mode = Integer.parseInt(clkdisplayMode); }catch(Exception e){ } return mode; } public Align getTextAlign(){ Align a = Align.LEFT; if(!TextUtils.isEmpty(textAlign)){ if(textAlign.contains("right")){ a = Align.RIGHT; }else if(textAlign.contains("center")){ a = Align.CENTER; } } return a; } /* public String getStringTextAlign(){ return textAlign; }*/ public int getTextGravity(){ /*if(!TextUtils.isEmpty(textAlign)){ String[] gravitys = textAlign.split("\\|"); int gravityInit = Gravity.NO_GRAVITY; for(int i = 0; i < gravitys.length; i++){ String gravity = gravitys[i]; if("left".equals(gravity)){ gravityInit = Gravity.LEFT | gravityInit; }else if("right".equals(gravity)){ gravityInit = Gravity.RIGHT | gravityInit; }else if("top".equals(gravity)){ gravityInit = Gravity.TOP | gravityInit; }else if("start".equals(gravity)){ gravityInit = Gravity.TOP | gravityInit; }else if("end".equals(gravity)){ gravityInit = Gravity.BOTTOM | gravityInit; }else if("center".equals(gravity)){ gravityInit = Gravity.CENTER | gravityInit; } } return gravityInit; }else{ return Gravity.CENTER; }*/ int textGravity=Gravity.CENTER_HORIZONTAL; if(!TextUtils.isEmpty(textAlign)){ /*if("start".equals(textAlign)) textGravity=Gravity.TOP; else if("end".equals(textAlign)) textGravity=Gravity.BOTTOM; else*/ if("left".equals(textAlign)) textGravity=Gravity.LEFT; else if("right".equals(textAlign)) textGravity=Gravity.RIGHT; else if("center".equals(textAlign)) textGravity=Gravity.CENTER; } return textGravity; } //字体加粗模式 public int getTextFontWeight(){ int tf = Typeface.NORMAL; if(!TextUtils.isEmpty(textFontWeight)){ if(textFontWeight.contains("bold")){ tf = Typeface.BOLD; }else if(textFontWeight.contains("italic")){ tf = Typeface.ITALIC; } } return tf; } /* public boolean isBold(){ boolean bold = false; if(!TextUtils.isEmpty(textFontWeight) && textFontWeight.contains("bold")){ bold = true; } return bold; } */ public String getBackgroundColor(){ if(TextUtils.isEmpty(backgroundColor)){ return ""; }else{ return backgroundColor; } } public String getTextColor(){ if(TextUtils.isEmpty(textColor)){ return "#000000"; }else{ return textColor; } } public String getTextFontSize(){ if(TextUtils.isEmpty(textFontSize)){ return "20"; }else{ return textFontSize; } } }
横向滚动布局:text_horizontal_scroll.xml
<?xml version="1.0" encoding="utf-8"?> <HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="1000dip" android:layout_height="match_parent" android:scrollbars="none"> <TextView android:id="@+id/textViewHorizontal" android:layout_width="wrap_content" android:layout_height="match_parent" android:textColor="@android:color/white" android:textSize="13sp"/> </HorizontalScrollView>
垂直滚动布局:text_vertical_scroll.xml
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="1000dip" android:scrollbars="none"> <!-- <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> android:maxHeight="800000dp"--> <TextView android:id="@+id/textViewVertical" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="10dp" android:paddingRight="10dp" android:gravity="center_vertical" android:textColor="@android:color/white" android:lineSpacingMultiplier="1.2" android:textSize="16sp"/> </ScrollView>
使用:
TextPlayer txtPlayerVer = new TextPlayer(this,new TextViewAttr()); txtPlayerVer.setVerticalScrollView(layout); txtPlayerVer.setTextContent(getString(R.string.notice_test)); layoutContainer.addView(txtPlayerVer.getView()); txtPlayerVer.startPlay();