package com.marshalchen.common.ui; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Paint; import android.os.Build; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; 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.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import com.marshalchen.common.R; import com.marshalchen.common.commonUtils.logUtils.Logs; public class MarqueeView extends LinearLayout { private TextView mTextField; private ScrollView mScrollView; private static final int TEXTVIEW_VIRTUAL_WIDTH = 2000; private Animation mMoveTextOut = null; private Animation mMoveTextIn = null; private Paint mPaint; private boolean mMarqueeNeeded = false; private static final String TAG = MarqueeView.class.getSimpleName(); private float mTextDifference; /** * Control the speed. The lower this value, the faster it will scroll. */ private static final int DEFAULT_SPEED = 60; /** * Control the pause between the animations. Also, after starting this activity. */ private static final int DEFAULT_ANIMATION_PAUSE = 2000; private int mSpeed = DEFAULT_SPEED; private int mAnimationPause = DEFAULT_ANIMATION_PAUSE; private boolean mAutoStart = false; private Interpolator mInterpolator = new LinearInterpolator(); private boolean mCancelled = false; private Runnable mAnimationStartRunnable; private boolean mStarted; /** * Sets the animation speed. * The lower the value, the faster the animation will be displayed. * * @param speed Milliseconds per PX. */ public void setSpeed(int speed) { this.mSpeed = speed; } /** * Sets the pause between animations * * @param pause In milliseconds. */ public void setPauseBetweenAnimations(int pause) { this.mAnimationPause = pause; } /** * Sets a custom interpolator for the animation. * * @param interpolator Animation interpolator. */ public void setInterpolator(Interpolator interpolator) { this.mInterpolator = interpolator; } @SuppressWarnings({"UnusedDeclaration"}) public MarqueeView(Context context) { super(context); init(context); } @SuppressWarnings({"UnusedDeclaration"}) public MarqueeView(Context context, AttributeSet attrs) { super(context, attrs); init(context); extractAttributes(attrs); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public MarqueeView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); extractAttributes(attrs); } private void extractAttributes(AttributeSet attrs) { if (getContext() == null) { return; } TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MarqueeView); if (a == null) { return; } mSpeed = a.getInteger(R.styleable.MarqueeView_marqueeSpeed, DEFAULT_SPEED); mAnimationPause = a.getInteger(R.styleable.MarqueeView_pause, DEFAULT_ANIMATION_PAUSE); mAutoStart = a.getBoolean(R.styleable.MarqueeView_autoStart, false); a.recycle(); } private void init(Context context) { // init helper mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStrokeWidth(1); mPaint.setStrokeCap(Paint.Cap.ROUND); mInterpolator = new LinearInterpolator(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (getChildCount() == 0 || getChildCount() > 1) { throw new RuntimeException("MarqueeView must have exactly one child element."); } if (changed) { View v = getChildAt(0); if (!(v instanceof TextView)) { throw new RuntimeException("The child view of this MarqueeView must be a TextView instance."); } initView(getContext()); prepareAnimation(); if (mAutoStart) { startMarquee(); } } } /** * Starts the configured marquee effect. */ public void startMarquee() { if (mMarqueeNeeded) { startTextFieldAnimation(); } mCancelled = false; mStarted = true; } private void startTextFieldAnimation() { mAnimationStartRunnable = new Runnable() { public void run() { mTextField.startAnimation(mMoveTextOut); } }; postDelayed(mAnimationStartRunnable, mAnimationPause); } /** * Disables the animations. */ public void reset() { mCancelled = true; if (mAnimationStartRunnable != null) { removeCallbacks(mAnimationStartRunnable); } mTextField.clearAnimation(); mStarted = false; mMoveTextOut.reset(); mMoveTextIn.reset(); // mScrollView.removeView(mTextField); // mScrollView.addView(mTextField); invalidate(); } private void prepareAnimation() { // Measure mPaint.setTextSize(mTextField.getTextSize()); mPaint.setTypeface(mTextField.getTypeface()); float mTextWidth = mPaint.measureText(mTextField.getText().toString()); // See how much functions are needed at all mMarqueeNeeded = mTextWidth > getMeasuredWidth(); mTextDifference = Math.abs((mTextWidth - getMeasuredWidth())) + 5; Logs.d(TAG, "mTextWidth : " + mTextWidth); Logs.d(TAG, "measuredWidth : " + getMeasuredWidth()); Logs.d(TAG, "mMarqueeNeeded : " + mMarqueeNeeded); Logs.d(TAG, "mTextDifference : " + mTextDifference); final int duration = (int) (mTextDifference * mSpeed); mMoveTextOut = new TranslateAnimation(0, -mTextDifference, 0, 0); mMoveTextOut.setDuration(duration); mMoveTextOut.setInterpolator(mInterpolator); mMoveTextOut.setFillAfter(true); mMoveTextIn = new TranslateAnimation(-mTextDifference, 0, 0, 0); mMoveTextIn.setDuration(duration); mMoveTextIn.setStartOffset(mAnimationPause); mMoveTextIn.setInterpolator(mInterpolator); mMoveTextIn.setFillAfter(true); mMoveTextOut.setAnimationListener(new Animation.AnimationListener() { public void onAnimationStart(Animation animation) { expandTextView(); } public void onAnimationEnd(Animation animation) { if (mCancelled) { return; } mTextField.startAnimation(mMoveTextIn); } public void onAnimationRepeat(Animation animation) { } }); mMoveTextIn.setAnimationListener(new Animation.AnimationListener() { public void onAnimationStart(Animation animation) { } public void onAnimationEnd(Animation animation) { cutTextView(); if (mCancelled) { return; } startTextFieldAnimation(); } public void onAnimationRepeat(Animation animation) { } }); } private void initView(Context context) { // Scroll View LayoutParams sv1lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); sv1lp.gravity = Gravity.CENTER_HORIZONTAL; mScrollView = new ScrollView(context); // Scroll View 1 - Text Field mTextField = (TextView) getChildAt(0); removeView(mTextField); mScrollView.addView(mTextField, new ScrollView.LayoutParams(TEXTVIEW_VIRTUAL_WIDTH, LayoutParams.WRAP_CONTENT)); mTextField.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) { final boolean continueAnimation = mStarted; reset(); prepareAnimation(); cutTextView(); post(new Runnable() { @Override public void run() { if (continueAnimation) { startMarquee(); } } }); } }); addView(mScrollView, sv1lp); } private void expandTextView() { ViewGroup.LayoutParams lp = mTextField.getLayoutParams(); lp.width = 2000; mTextField.setLayoutParams(lp); } private void cutTextView() { if (mTextField.getWidth() != getMeasuredWidth()) { ViewGroup.LayoutParams lp = mTextField.getLayoutParams(); lp.width = getMeasuredWidth(); mTextField.setLayoutParams(lp); } } }