参考大神的笔记,在大神的基础上添加了速度到步长的转换以及周期性悬停的效果
CustomMarqueeTextView.java
package com.oem.view.textview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Handler;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
public class CustomMarqueeTextView extends AppCompatTextView{
private static final String TAG = "CustomMarqueeTextView";
private static final int FPS = 60;//刷新率
private int DEFAULT_DISTANCE = 20;
private float DEFAULT_OFFSET = 0;
private float DEFAULT_SPEED = 100;
private long DEFAULT_STAYED_MILISECONDS = 1200;
private final static int DEFAULT_GRAVITY = 8388659;
private int textWidth;//文字长度
private float mStep = -((float) (DEFAULT_SPEED / FPS));
private float mOffsetX = DEFAULT_OFFSET;//偏移量
private int mRightDistance = DEFAULT_DISTANCE;//文件重复出现间距
private int mLeftDistance = 0;
private Rect mTextRect;//文字占位矩阵
private boolean isPressPause = false;//是否按压暂停
private boolean isPause = false;
private boolean isSingle = false;
private long mStayed = DEFAULT_STAYED_MILISECONDS; //默认停留1200 ms
private long mPeriod = 1000/FPS; //等待period
private int mCircluateTimes =3; //滚动3次后停留,-1是无限循环
private int mCircFlag = 0;
private Handler mHandler = new Handler();
private Paint mPaint;
private boolean isRoll = false;
private String mText = "";
private TextPaint textPaint;
private float mTextCenterVerticalToBaseLine;
private final int mGravity;
public CustomMarqueeTextView(Context context) {
this(context, null);
}
public CustomMarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTextRect = new Rect();
mGravity = getGravity();
setDistance(mRightDistance);
setSingle(false);
}
public CustomMarqueeTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
//Log.d(TAG, getText() + ": running... mStep = " + mStep);
if (mStep != 0 && textWidth != 0) {
if (!isPause) {
if (mStayed != 0) {
if (mCircFlag == 0) { //首次停留
mCircFlag++;
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mStayed);
} else if (mCircFlag - 1 == mCircluateTimes) { //循环停留
mCircFlag = 1;
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mStayed);
} else {
mOffsetX += mStep;
int maxW = textWidth + mRightDistance;
float cirFlag = Math.abs(mOffsetX);
mOffsetX = mOffsetX % (maxW);
if (cirFlag >= maxW) {
mCircFlag++;
}
mHandler.postDelayed(this, mPeriod);
}
}
postInvalidate();
}
}
}
};
public void setCirculation(long stayed, int circluateTimes) { //停留时长,循环显示次数,逻辑是首先停留一段时长,循环显示指定次数后,再次停留,以此循环
mStayed = stayed;
mCircluateTimes = circluateTimes;
}
private void refreshGravity(boolean isMarquee) {
Log.d(TAG, "refreshGravity: " + mGravity);
if(isMarquee) {
super.setGravity(DEFAULT_GRAVITY);
} else {
super.setGravity(mGravity);
}
}
private void refreshGradient(boolean isMarquee) {
if (isMarquee) {
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(getTextSize());
// 设置边框虚化效果
mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
// 设置渐变背景
int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT};
float[] positions = {0.0f, 0.5f, 1.0f};
Shader shader = new LinearGradient(0, 0, getWidth(), 0, colors, positions, Shader.TileMode.CLAMP);
getPaint().setShader(shader);
} else {
int[] colors = {Color.WHITE, Color.WHITE, Color.WHITE};
float[] positions = {0.0f, 0.5f, 1.0f};
Shader shader = new LinearGradient(0, 0, getWidth(), 0, colors, positions, Shader.TileMode.CLAMP);
getPaint().setShader(shader);
}
}
private void refreshParameters(boolean isMarquee) {
if (isMarquee) {
mRightDistance = DEFAULT_DISTANCE;
mStep = -((float) (DEFAULT_SPEED / FPS));
mOffsetX = DEFAULT_OFFSET;
mCircFlag = 0;
} else {
mRightDistance = getWidth() - textWidth;
mStep = 0;
mOffsetX = 0;
}
}
private void setMarquee(boolean isMarquee) {
Log.d(TAG, getText() + ": setMarquee " + isMarquee);
isRoll = isMarquee;
refreshParameters(isMarquee);
refreshGradient(isMarquee);
refreshGravity(isMarquee);
postInvalidate();
}
private void refreshTextInfo() {
Log.d(TAG, getText() + ": init text info");
mText = getText().toString();
textPaint = getPaint();
textPaint.setColor(getCurrentTextColor());
textPaint.getTextBounds(mText, 0, mText.length(), mTextRect);
mTextCenterVerticalToBaseLine =
(-textPaint.ascent() + textPaint.descent()) / 2 - textPaint.descent();
textWidth = mTextRect.width();
Log.d(TAG, "textWidth = " + textWidth + ", getWidth() = " + getWidth());
if (textWidth > getWidth()) { //超出显示区域需要显示渐变
setMarquee(true);
if (mHandler != null) {
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, 100);
}
} else { //不超出设置不滚动
setMarquee(false);
}
}
@Override
public void setText(CharSequence text, BufferType type) {
Log.d(TAG, getText() + ": setText, mCircFlag = " + mCircFlag);
super.setText(text, type);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG, getText() + ": onLayout");
if (getText() != null) {
refreshTextInfo();
}
}
@Override
protected void onDraw(Canvas canvas) {
//Log.d(TAG, getText() + ": onDraw");
if (!isRoll) { //短文字
super.onDraw(canvas);
} else { //长文字
int width = getWidth();
boolean isLeft = mStep < 0;
float y = (getHeight() >> 1) + mTextCenterVerticalToBaseLine;
if (isSingle) {
mRightDistance = width;
canvas.drawText(mText, mOffsetX + (isLeft ? mRightDistance : -textWidth), y, textPaint);
} else {
int w = textWidth + mRightDistance;
int size = width / w + 1;
canvas.drawText(mText, mOffsetX + (isLeft ? 0 : width - textWidth), y, textPaint);
for (int i = 1; i <= size; i++) {
int offset = w * i;
canvas.drawText(mText, mOffsetX + (isLeft ? offset : width - textWidth - offset), y, textPaint);
}
}
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isPressPause && event != null) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setPause(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setPause(false);
break;
}
return true;
}
return super.onTouchEvent(event);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mHandler != null) {
mHandler.removeCallbacks(mRunnable);
}
}
/*
* @param speed为负数向左位移,为正数向右位移
* @return
*
* */
public void setSpeed(int speed) {
float step = (float) ((float) speed / FPS);
Log.d(TAG, "setspeed = " + speed + ", step = " + step);
mStep = step;
postInvalidate();
}
public void setStep(float step) {
mOffsetX = 0;
mStep = step;
postInvalidate();
}
public float getStep() {
return mStep;
}
public void setDistance(int distance) {
if (!isSingle){
mRightDistance = distance;
mLeftDistance = mRightDistance;
}
}
public void setPressPause(boolean isPressPause) {
this.isPressPause = isPressPause;
}
public void setPause(boolean pause) {
isPause = pause;
}
public void setStart() {
isPause = false;
}
private void setSingle(boolean single) {
Log.d(TAG, "setSingle = true, mCirFlag = " + mCircFlag);
mOffsetX = 0;
isSingle = single;
if (single) {
mLeftDistance = mRightDistance;
} else {
mRightDistance = mLeftDistance;
}
postInvalidate();
}
public boolean isPause() {
return isPause;
}
public boolean isSingle() {
return isSingle;
}
}