Skip to content

Instantly share code, notes, and snippets.

@pythoncat1024
Last active July 6, 2020 17:50
Show Gist options
  • Save pythoncat1024/930836211866b5e36bb29caeb066e6ff to your computer and use it in GitHub Desktop.
Save pythoncat1024/930836211866b5e36bb29caeb066e6ff to your computer and use it in GitHub Desktop.
MarqueeText 实现效果: TextView内容过长的话,先滚动显示全部内容,然后显示成末尾有...的样式
package org.victor.testlivedata.widget;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.apkfuns.logutils.LogUtils;
import java.util.concurrent.atomic.AtomicInteger;
public class MarqueeText extends FrameLayout {
private static final String TRANSLATION_X = "translationX";
private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
private Context mContext;
private TextView mTextView;
private Handler mHandler = new Handler(Looper.getMainLooper());
private volatile AtomicInteger mMeasuredCount = new AtomicInteger(0);
public MarqueeText(@NonNull Context context) {
this(context, null);
}
public MarqueeText(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MarqueeText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
mContext = context;
}
public MarqueeText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mTextView = new TextView(context);
LayoutParams params = new LayoutParams(generateDefaultLayoutParams());
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
addView(mTextView, params);
}
private int dp2px(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
private int sp2px(float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
public void setText(CharSequence text) {
int color = Color.parseColor("#33ff00ff");
// mTextView.setBackgroundColor(color);
mTextView.setSingleLine(true);
mTextView.setText(text);
if (mMeasuredCount.get() > 0) {
innerText(text);
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mMeasuredCount.get() > 0) {
mHandler.removeCallbacks(this);
innerText(text);
return;
}
mHandler.postDelayed(this, 50);
}
});
}
}
private void innerText(CharSequence text) {
float textWidth = getTextWidth(text);
int width = getMeasuredWidth();
LogUtils.e("text: %s,, [%d,%d]", text, (int) textWidth, width);
if (textWidth < width - mTextView.getPaddingLeft() - mTextView.getPaddingRight()) {
// ok
return;
}
TextView other = new TextView(mContext);
LayoutParams params = new LayoutParams(generateDefaultLayoutParams());
params.width = width;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
other.setText(text);
other.setSingleLine();
other.setEllipsize(TextUtils.TruncateAt.END);
other.setTranslationX(width);
// other.setBackgroundColor(Color.parseColor("#6f0f7f00"));
addView(other, params);
LinearInterpolator interpolator = new LinearInterpolator();
int duration = 1200 * text.length() / 4;
ObjectAnimator animator2 = ObjectAnimator.ofFloat(other, TRANSLATION_X, textWidth, 0);
animator2.setInterpolator(interpolator);
animator2.setDuration(duration);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mTextView, TRANSLATION_X, 0, -textWidth);
animator1.setInterpolator(interpolator);
animator1.setDuration(duration);
animator1.addUpdateListener(new AnimatorUpdateListener() {
boolean startSecond = false;
boolean did = false;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue(TRANSLATION_X);
LogUtils.v("value---> [%s,%s]", textWidth, value);
if (!did && -value >= textWidth * 1 / 5) {
startSecond = true;
}
if (did) {
startSecond = false;
}
if (startSecond) {
LogUtils.e("DID=========[%s,%s]", textWidth, value);
animator2.start();
did = true;
}
}
});
animator1.start();
}
private float getTextWidth(CharSequence text) {
TextPaint paint = mTextView.getPaint();
return paint.measureText(text, 0, text.length());
}
public void setText(@StringRes int text) {
setText(mContext.getText(text));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mMeasuredCount.set(0);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED) {
return;
}
final TextView child = mTextView;
final int widthPadding;
final int heightPadding;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
if (targetSdkVersion >= Build.VERSION_CODES.M) {
widthPadding = getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin;
heightPadding = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = getPaddingLeft() + getPaddingRight();
heightPadding = getPaddingTop() + getPaddingBottom();
}
int desiredWidth = (int) (getTextWidth(child.getText()) - widthPadding);
if (child.getMeasuredWidth() < desiredWidth) {
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredWidth, MeasureSpec.EXACTLY);
final int childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec, heightPadding, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
LogUtils.e("measure: [%s,%s]", getMeasuredWidth(), getMeasuredHeight());
mMeasuredCount.incrementAndGet();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
LogUtils.e("layout");
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
LogUtils.e("size = [%s,%s] -- [%s,,%s]", w, h, oldw, oldh);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
@Override
public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
super.addOnLayoutChangeListener(listener);
}
}
@pythoncat1024
Copy link
Author

实现效果: TextView内容过长的话,先滚动显示全部内容,然后显示成末尾有...的样式。

  • 1200 , 因为 android.widget.TextView.Marquee#MARQUEE_DELAY 的值是1200。 /4是试出来的,速度跟原生效果相差无几了。
  • 1/5 的前后间距因子也是试出来的。
  • 测量的核心代码int desiredWidth = (int) (getTextWidth(child.getText()) - widthPadding);
    这个代码的作用就是让TextView的宽度无视父控件的宽度,必须完成包含全部的文字。
  • 测量的代码,其他部分是参考 android.widget.HorizontalScrollView#onMeasure 来实现的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment