需求
- 當(dāng)TextView限制最大行數(shù)的時(shí)候,文本內(nèi)容超過(guò)最大行數(shù)可自動(dòng)實(shí)現(xiàn)文本內(nèi)容向上滾動(dòng)
- 隨著TextView的文本內(nèi)容的改變永品,可自動(dòng)計(jì)算換行并實(shí)時(shí)的向上滾動(dòng)
- 文字向上滾動(dòng)后可向下滾動(dòng)回到正確的水平位置
自定義方法
- 自定義一個(gè)View,繼承自View滔以,定重寫(xiě)里面的onDraw方法
- 文字的滾動(dòng)是用Canvas對(duì)象的drawText方法去實(shí)現(xiàn)的
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
paint.getNativeInstance(), paint.mNativeTypeface);
}
通過(guò)控制y參數(shù)可實(shí)現(xiàn)文字不同的垂直距離四濒,這里的x奴愉,y并不代表默認(rèn)橫向坐標(biāo)為0蔬螟,縱向坐標(biāo)為0的坐標(biāo)此迅,具體詳解我覺(jué)得這篇博客解釋的比較清楚,我們主要關(guān)注的是參數(shù)y的控制旧巾,y其實(shí)就是text的baseline耸序,這里還需要解釋text的杰哥基準(zhǔn)線:
ascent:該距離是從所繪字符的baseline之上至該字符所繪制的最高點(diǎn)。這個(gè)距離是系統(tǒng)推薦鲁猩。
descent:該距離是從所繪字符的baseline之下至該字符所繪制的最低點(diǎn)坎怪。這個(gè)距離是系統(tǒng)推薦的。
top:該距離是從所繪字符的baseline之上至可繪制區(qū)域的最高點(diǎn)廓握。
bottom:該距離是從所繪字符的baseline之下至可繪制區(qū)域的最低點(diǎn)搅窿。
leading:為文本的線之間添加額外的空間嘁酿,這是官方文檔直譯,debug時(shí)發(fā)現(xiàn)一般都為0.0男应,該值也是系統(tǒng)推薦的闹司。
特別注意: ascent和top都是負(fù)值,而descent和bottom:都是正值殉了。
由于text的baseline比較難計(jì)算开仰,所以我們大約取y = bottom - top的值拟枚,這么坐位baseline的值不是很精確薪铜,但是用在此自定義控件上文字的大小間距恰好合適,在其他場(chǎng)景可能還是需要精確的去計(jì)算baseline的值
動(dòng)畫(huà)效果實(shí)現(xiàn)
- 通過(guò)循環(huán)觸發(fā)執(zhí)行onDraw方法來(lái)實(shí)現(xiàn)文字的上下滑動(dòng)恩溅,當(dāng)然在每次觸發(fā)onDraw之前首先要計(jì)算文字的baseline的值
- 通過(guò)設(shè)置Paint的alpha的值來(lái)控制透明度隔箍,alpha的值的變化要和文字baseline的變化保持同步,因?yàn)槲淖稚舷禄瑒?dòng)和文字的透明度要做成一個(gè)統(tǒng)一的動(dòng)畫(huà)效果
- 文字的換行脚乡,首先用measureText來(lái)測(cè)量每一個(gè)字的寬度蜒滩,然后持續(xù)累加,直到累加寬度超過(guò)一行的最大限制長(zhǎng)度之后就追加一個(gè)換行符號(hào)奶稠,當(dāng)然我們是用一個(gè)List作為容器來(lái)容納文本內(nèi)容俯艰,一行文本就是list的一個(gè)item所以不用追加換行符號(hào),直接添加list的item
- 在實(shí)現(xiàn)文字上下滑動(dòng)以及透明度變化的時(shí)候遇到一個(gè)問(wèn)題锌订,就是上一次的滑動(dòng)剛剛滑到一半竹握,文字的baseline和透明度已經(jīng)改變到一半了,這時(shí)候又有新的文本追加進(jìn)來(lái)辆飘,那么新的文本會(huì)導(dǎo)致一次新的滑動(dòng)動(dòng)畫(huà)和文字透明度改變動(dòng)畫(huà)會(huì)和之前的重疊啦辐,造成上一次的滑動(dòng)效果被中斷,文字重新從初始值開(kāi)始滑動(dòng)蜈项,所以會(huì)看到文字滑動(dòng)到一半又回到初始位置重新開(kāi)始滑動(dòng)芹关,那么如果一直不斷的有文字追加進(jìn)來(lái)會(huì)導(dǎo)致文字滑動(dòng)反復(fù)的中斷開(kāi)始,這種效果當(dāng)然不是我們想要的紧卒,我們想要的就是文字滑動(dòng)到一半了侥衬,那么已經(jīng)滑動(dòng)的文字保持當(dāng)前的狀態(tài),新追加進(jìn)來(lái)的問(wèn)題從初始值開(kāi)始滑動(dòng)跑芳,滑動(dòng)到一半的文字從之前的狀態(tài)繼續(xù)滑動(dòng)浇冰,所以就需要記錄文字的滑動(dòng)間距,透明度等信息并保存下來(lái)
代碼實(shí)現(xiàn)
public class AutoScrollTextView extends View {
public interface OnTextChangedListener {
void onTextChanged(String text);
}
private class TextStyle {
int alpha;
float y;
String text;
TextStyle(String text, int alpha, float y) {
this.text = text;
this.alpha = alpha;
this.y = y;
}
}
public static final int SCROLL_UP = 0, SCROLL_DOWN = 1;
private List<TextStyle> textRows = new ArrayList<>();
private OnTextChangedListener onTextChangedListener;
private Paint textPaint;
/**
* 標(biāo)題內(nèi)容
*/
private String title;
/**
* 是否是標(biāo)題模式
*/
private boolean setTitle;
/**
* 當(dāng)前的文本內(nèi)容是否正在滾動(dòng)
*/
private boolean scrolling;
/**
* 文字滾動(dòng)方向聋亡,支持上下滾動(dòng)
*/
private int scrollDirect;
/**
* 每行的最大寬度
*/
private float lineMaxWidth;
/**
* 最大行數(shù)
*/
private int maxLineCount;
/**
* 每行的高度肘习,此值是根據(jù)文字的大小自動(dòng)去測(cè)量出來(lái)的
*/
private float lineHeight;
public AutoScrollTextView(Context context) {
super(context);
init();
}
public AutoScrollTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public AutoScrollTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public AutoScrollTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
textPaint = createTextPaint(255);
lineMaxWidth = textPaint.measureText("一二三四五六七八九十"); // 默認(rèn)一行最大長(zhǎng)度為10個(gè)漢字的長(zhǎng)度
maxLineCount = 4;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float x;
float y = fontMetrics.bottom - fontMetrics.top;
lineHeight = y;
if (setTitle) {
x = getWidth() / 2 - textPaint.measureText(title) / 2;
canvas.drawText(title, x, y, textPaint);
} else {
synchronized (this) {
if (textRows.isEmpty()) {
return;
}
scrolling = true;
x = getWidth() / 2 - textPaint.measureText(textRows.get(0).text) / 2;
if (textRows.size() <= 2) {
for (int index = 0;index < 2 && index < textRows.size();index++) {
TextStyle textStyle = textRows.get(index);
textPaint.setAlpha(textStyle.alpha);
canvas.drawText(textStyle.text, x, textStyle.y, textPaint);
}
} else {
boolean draw = false;
for (int row = 0;row < textRows.size();row++) {
TextStyle textStyle = textRows.get(row);
textPaint.setAlpha(textStyle.alpha);
canvas.drawText(textStyle.text, x, textStyle.y, textPaint);
if (textStyle.alpha < 255) {
textStyle.alpha += 51;
draw = true;
}
if (textRows.size() > 2) {
if (scrollDirect == SCROLL_UP) {
// 此處的9.0f的值是由255/51得來(lái)的,要保證文字透明度的變化速度和文字滾動(dòng)的速度要保持一致
// 否則可能造成透明度已經(jīng)變化完了坡倔,文字還在滾動(dòng)或者透明度還沒(méi)變化完成漂佩,但是文字已經(jīng)不滾動(dòng)了
textStyle.y = textStyle.y - (lineHeight / 9.0f);
} else {
if (textStyle.y < lineHeight + lineHeight * row) {
textStyle.y = textStyle.y + (lineHeight / 9.0f);
draw = true;
}
}
}
}
if (draw) {
postInvalidateDelayed(50);
} else {
scrolling = false;
}
}
}
}
}
private Paint createTextPaint(int a) {
Paint textPaint = new Paint();
textPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, getContext().getResources().getDisplayMetrics()));
textPaint.setColor(getContext().getColor(R.color.color_999999));
textPaint.setAlpha(a);
return textPaint;
}
public void resetText() {
synchronized (this) {
textRows.clear();
}
}
public void formatText() {
scrollDirect = SCROLL_DOWN;
StringBuffer stringBuffer = new StringBuffer("\n");
synchronized (this) {
for (int i = 0;i < textRows.size();i++) {
TextStyle textStyle = textRows.get(i);
if (textStyle != null) {
textStyle.alpha = 255;
// textStyle.y = 45 + 45 * i;
stringBuffer.append(textStyle.text + "\n");
}
}
}
postInvalidateDelayed(100);
LogUtil.i("formatText:" + stringBuffer.toString());
}
public void appendText(String text) {
setTitle = false;
scrollDirect = SCROLL_UP;
synchronized (this) {
if (textRows.size() > maxLineCount) {
return;
}
if (text.length() <= 10) {
if (textRows.isEmpty()) {
textRows.add(new TextStyle(text, 255, lineHeight + lineHeight * textRows.size()));
} else {
TextStyle pre = textRows.get(textRows.size() - 1);
textRows.set(textRows.size() - 1, new TextStyle(text, pre.alpha, pre.y));
}
} else {
List<String> list = new ArrayList<>();
StringBuffer stringBuffer = new StringBuffer();
float curWidth = 0;
for (int index = 0;index < text.length();index++) {
char c = text.charAt(index);
curWidth += textPaint.measureText(String.valueOf(c));
if (curWidth <= lineMaxWidth) {
stringBuffer.append(c);
} else {
if (list.size() < maxLineCount) {
list.add(stringBuffer.toString());
curWidth = 0;
index--;
stringBuffer.delete(0, stringBuffer.length());
} else {
break;
}
}
}
if (!TextUtils.isEmpty(stringBuffer.toString()) && list.size() < maxLineCount) {
list.add(stringBuffer.toString());
}
if (textRows.isEmpty()) {
for (int i = 0;i < list.size();i++) {
if (i < 2) {
textRows.add(new TextStyle(list.get(i), 255, lineHeight + lineHeight * i));
} else {
textRows.add(new TextStyle(list.get(i), 0, lineHeight + lineHeight * i));
}
}
} else {
for (int i = 0;i < list.size();i++) {
if (textRows.size() > i) {
TextStyle pre = textRows.get(i);
textRows.set(i, new TextStyle(list.get(i), pre.alpha, pre.y));
} else {
TextStyle pre = textRows.get(textRows.size() - 1);
if (i < 2) {
textRows.add(new TextStyle(list.get(i), 255, pre.y + lineHeight));
} else {
textRows.add(new TextStyle(list.get(i), 0, pre.y + lineHeight));
}
}
}
}
}
if (!scrolling) {
invalidate();
}
}
textChanged();
}
public void setTextColor(int corlor) {
textPaint.setColor(corlor);
invalidate();
}
public void setTitle(int resId) {
this.title = getContext().getString(resId);
setTitle = true;
invalidate();
}
public void setOnTextChangedListener(OnTextChangedListener onTextChangedListener) {
this.onTextChangedListener = onTextChangedListener;
}
private void textChanged() {
if (onTextChangedListener != null) {
onTextChangedListener.onTextChanged(getText());
}
}
public String getText() {
StringBuffer allText = new StringBuffer();
for (TextStyle textStyle : textRows) {
allText.append(textStyle.text);
}
return allText.toString();
}
public int getScrollDirect() {
return scrollDirect;
}
public void setScrollDirect(int scrollDirect) {
this.scrollDirect = scrollDirect;
}
public float getLineMaxWidth() {
return lineMaxWidth;
}
public void setLineMaxWidth(float lineMaxWidth) {
this.lineMaxWidth = lineMaxWidth;
}
public int getMaxLineCount() {
return maxLineCount;
}
public void setMaxLineCount(int maxLineCount) {
this.maxLineCount = maxLineCount;
}
public boolean isScrolling() {
return scrolling;
}
}
代碼還可以重構(gòu)的更加簡(jiǎn)潔脖含,但是這邊主要是為了做demo演示,所以就滿看下實(shí)現(xiàn)的原理就好了