最近项目需求做个循环滚动字幕功能,自己找了相关资料,分别实现了垂直滚动和水平滚动方式,根据自己的风格用两种方法实现了该功能;
2015-12-15更新:以前实现效果并不理想(文字的格式排版,自定义颜色,大小字号等TextView原有属性都不能很好兼容实现),在Github上找了一个较好的实现,并对此进行修改改进处理,这个支持所有TextView属性,还支持速度
实现原理:垂直滚动textview加ScrollView实现,水平滚动textview加HorizontalScrollView实现
具体实现:
import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.os.Handler; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.TranslateAnimation; import android.widget.TextView; public class TextMarqueeView { private Context mContext = null; private TextView txtView = null; private View vroot = 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; /** 由左到右滚动 */ public static final int SCROLL_HORIZONTAL_LEFT = 111; /** 由下到上滚动 */ public static final int SCROLL_VERTICAL_UP = 112; private int currentScrollDirection = SCROLL_HORIZONTAL_LEFT; public int getCurrentScrollDirection() { return currentScrollDirection; } public void setCurrentScrollDirection(int currentScrollDirection) { this.currentScrollDirection = currentScrollDirection; } /** * 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 Interpolator mInterpolator = new LinearInterpolator(); private boolean mCancelled = false; private Runnable mAnimationStartRunnable; private boolean mStarted = false; private int currentSrollMode = TextMarqueeView.SCROLL_HORIZONTAL_LEFT; public TextMarqueeView(Context context) { this.mContext = context; intView(); } public void setTextSpeed(int speedMode) { if (speedMode == SPEED_FAST) { this.mSpeedHorizontal = SPEED_FAST; this.mSpeedVertical = SPEED_FAST * 2; } else if (speedMode == SPEED_LOW) { this.mSpeedHorizontal = SPEED_LOW; this.mSpeedVertical = SPEED_LOW * 2; } else { this.mSpeedHorizontal = SPEED_NORMAL; this.mSpeedVertical = SPEED_NORMAL * 2; } } private ITextPlayEnd endLis; public void setEndListner(ITextPlayEnd endLis) { this.endLis = endLis; } public interface ITextPlayEnd { public void playEnd(); } private void intView() { LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (currentScrollDirection == TextMarqueeView.SCROLL_HORIZONTAL_LEFT) { vroot = inflater.inflate(R.layout.text_horizontal_scroll, null); txtView = (TextView) vroot.findViewById(R.id.textViewHorizontal); } else if (currentScrollDirection == TextMarqueeView.SCROLL_VERTICAL_UP) { vroot = inflater.inflate(R.layout.text_vertical_scroll, null); txtView = (TextView) vroot.findViewById(R.id.textViewVertical); } } protected View getCreateView() { return vroot; } protected void setTextContent(String txt) { txtView.setText(txt); } public TextView getElevatorWidgetView() { return txtView; } public void startMarquee() { // Log.d(TAG, "startMarquee()"); if (this.currentSrollMode == TextMarqueeView.SCROLL_HORIZONTAL_LEFT) { if (mMarqueeNeeded && mMoveTextOut != null) startHorizontalAnimation(); } else if (this.currentSrollMode == TextMarqueeView.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(); // Log.d(TAG, // "text content="+text); 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 (currentSrollMode == TextMarqueeView.SCROLL_HORIZONTAL_LEFT) { mMarqueeNeeded = textWidth > vroot.getMeasuredWidth(); // 垂直居中坐标 int centerPosY = Math.round(vroot.getMeasuredHeight() / 2 - textHeight / 2); 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 (currentSrollMode == TextMarqueeView.SCROLL_VERTICAL_UP) { // int lines=txtView.getLineCount(); int lines = (int) Math.ceil(textWidth / vroot.getMeasuredHeight()); mMarqueeNeeded = lines * textHeight > vroot.getMeasuredHeight();// mTextField.getHeight(); // 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 - textHeight * 3), duration);// } } 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(); if (endLis != null) endLis.playEnd(); 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(); if (endLis != null) endLis.playEnd(); 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"); 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"); 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); } } }
<?xml version="1.0" encoding="utf-8"?> <HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="9000dip" android:layout_height="match_parent" android:scrollbars="none"> <TextView android:id="@+id/textViewHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="left" android:textSize="30sp" android:textStyle="bold" /> </HorizontalScrollView>
<?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="9000dip" android:scrollbars="none"> <TextView android:id="@+id/textViewVertical" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" android:textSize="35sp" android:textStyle="bold" /> </ScrollView>
private TextMarqueeView textContainer; void textScorllText(){ textContainer.setTextContent(content); textContainer.setTextSpeed(TextMarqueeView.SPEED_FAST); textContainer.setCurrentScrollDirection(TextMarqueeView.SCROLL_HORIZONTAL_LEFT); textContainer.startMarquee(); }