您的位置:首页 > 其它

自定义动态切换字符的TextView

2016-07-06 15:30 537 查看
最近项目需要让搜索框的预选词自动切换,就像360助手、应用宝之类的搜索词会动态往上顶进行切换,然后去搜了一下开源项目,找到了一个效果挺炫的开源项目:HTextView。个人觉得比360之类的好看多了,当然了,在拿来用也要学习下别人是怎么实现的,最好的方法就是自己也实现一个,下面就以HTextView中的缩放效果进行实现。因为代码是以个人的习惯写的,和开源项目中有点出入,但整体实现思想是一样,更多动画效果可看开源项目。

先来看下要实现的最终效果:



下面开始来实现

一. 自定义TextView

要实现文字的动态效果,就要继承TextView并重写onDraw()方法,来自己绘制字符,绘制字符的方法为canvas.drawText():

/**
* Draw the text, with origin at (x,y), using the specified paint.
* The origin is interpreted based on the Align setting in the paint.
*
* @param text  The text to be drawn
* @param start The index of the first character in text to draw
* @param end   (end - 1) is the index of the last character in text to draw
* @param x     The x-coordinate of the origin of the text being drawn
* @param y     The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint)
这些参数还是比较好理解的,text:要绘制的字符串;start:text绘制起始索引;end:text绘制终止索引;x:绘制的X坐标;y:文字的基线;paint:画笔。关于文字的一些属性如baseline,有兴趣自己查资料,这里不说明这些东西。

有了这个我们就可以绘制字符串了:

mText = getText();
// 绘制字符
canvas.drawText(String.valueOf(mText.charAt(i)), 0, 1, x, getBaseline(), mPaint);
// 绘制字符串
canvas.drawText(mText, 0, mText.length(), x, getBaseline(), mPaint);
后面的绘制操作其实都是通过这个方法来完成的,而动画效果通过设置画笔Paint来控制。

二. 动态放大显示

我们从基本的一点一点搭建,首先来实现一个从无到有逐渐放大的效果:



转化GIF图片显示的有点快- -,将就看个效果。

设置的动画效果是这样的:

1. 每个字符从0->1放大为TextSize大小;

2. 同样透明为0->255变换,和放大同步;

3. 每个字符动画时间为400MS;

4. 每个字符动画的启动延迟递增20MS,比如第2个字符为20MS,第4个字符为60MS;

在写自定义TextView前先来定义个动画接口:

public interface ITextAnimation {
/**
* Starts the animation.
*/
void start();

/**
* Stops the animation.
*/
void stop();

/**
* Indicates whether the animation is running.
*
* @return True if the animation is running, false otherwise.
*/
boolean isRunning();
}
其实没什么特别的,我就直接把Animatable接口拷贝过来,这样看着规范点。
下面看自定义TextView实现代码:

public class TestTextView extends TextView implements ITextAnimation {

private static final int MAX_TEXT_LENGTH = 100;
// 每个字符动画时间
private static final int CALC_TIME = 400;
// 每个字符动画启动的间隔延迟时间,递增
private static final int EACH_CHAR_DELAY = 20;

// 是否为动画Text
private boolean mIsAnimationText;
private Paint mPaint;
// 要绘制的字符串
private CharSequence mText;
// 字体大小
private float mTextSize;
// 字符个数
private int mTextCount;
// 动画
private ValueAnimator mValueAnimator;
// 动画时间
private int mDuration;
// 动画进度
private int mProgress;
// 字符的X坐标
private float[] mCharOffset;

public TestTextView(Context context) {
this(context, null);
}

public TestTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public TestTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onDraw(Canvas canvas) {
if (!mIsAnimationText) {
super.onDraw(canvas);
return;
}
for (int i = 0; i < mTextCount; i++) {
// 字符动画启动的时间
int startTime = i * EACH_CHAR_DELAY;
// 动画进行的进度百分比,0→1
float percent = (mProgress - startTime) * 1.0f / CALC_TIME;
if (percent > 1.0f) {
percent = 1;
} else if (percent < 0) {
percent = 0;
}
// 透明度
int alpha = (int) (255 * percent);
// 大小
float size = mTextSize * percent;
mPaint.setAlpha(alpha);
mPaint.setTextSize(size);
// 绘制字符
canvas.drawText(String.valueOf(mText.charAt(i)), 0, 1, mCharOffset[i], getBaseline(), mPaint);
}
}

@Override
public void setText(CharSequence text, BufferType type) {
super.setText(text, type);
mIsAnimationText = false;
_initVariables();
}

/**
* @param text
*/
public void setAnimationText(CharSequence text) {
setText(text);
mIsAnimationText = true;
_calcCharOffset();
_initAnimator();
}

/**
* 初始化变量
*/
private void _initVariables() {
if (mPaint == null) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
mPaint.setColor(getCurrentTextColor());
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(getTextSize());
mText = getText();
mTextSize = getTextSize();
mTextCount = mText.length();
}

/**
* 初始化动画
*/
private void _initAnimator() {
mDuration = CALC_TIME + (mTextCount - 1) * EACH_CHAR_DELAY;
mValueAnimator = ValueAnimator.ofInt(0, mDuration);
mValueAnimator.setDuration(mDuration);
mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mProgress = (int) animation.getAnimatedValue();
invalidate();
}
});
// 直接启动动画
start();
}

/**
* 计算字符的X坐标
*/
private void _calcCharOffset() {
if (mCharOffset == null) {
mCharOffset = new float[MAX_TEXT_LENGTH];
}
int offset = 0;
mCharOffset[0] = 0;
for (int i = 0; i < mTextCount - 1; i++) {
offset += mPaint.measureText(String.valueOf(mText.charAt(i)));
mCharOffset[i + 1] = offset;
}
}

/***************************************************************************/

@Override
public void start() {
if (isRunning()) {
stop();
}
mValueAnimator.start();
}

@Override
public void stop() {
mValueAnimator.end();
}

@Override
public boolean isRunning() {
return mValueAnimator != null || mValueAnimator.isRunning();
}
}
整个代码比较简单,我把每个要做的操作分成不同的方法调用,这里说几个注意的地方:

1. 覆写了setText(CharSequence text, BufferType type),TextView在设置显示字符串时都会调用到这个方法,在这里做了两个操作,设置为不执行动画绘制(mIsAnimationText = false),初始化变量_initVariables(),就是记录下文字信息;

2. 对外提供了setAnimationText(CharSequence text)方法,用来执行动画绘制,这里调用了setText(text)最终也会调用上面覆写的方法,设置执行动画绘制;

3. 调用mPaint.measureText(String.valueOf(mText.charAt(i)))计算字符的X坐标,注意循环中的mCharOffset[i + 1] = offset,这里进行+1处理是第n个字符的偏移值为前n-1个字符的宽度之和;

4. 动画执行时间mDuration为最后一个字符的结束动画时间,看公式应该能理解;

5. 在绘制的时候计算了每个字符的动画执行百分比,并设置对应的透明度和大小,然后一个一个绘制字符;

三. 整个动画效果

先来整理下整个动画要实现的效果:

1. 找出切换字符串的重复字符,对这些字符进行平移动画;

2. 将前一个字符串逐渐缩小消失,动画时间为(mDuration/2);

3. 对当前字符串进行放大操作;

其实这个实现主要就是对重复字符的处理,消失动画其实和前面的放大动画没本质差别,要做重复字符串处理首先当然要获取前后两个字符串和计算各自的字符偏移值:

private void _initVariables() {
// 略......
mOldText = mText;
mText = getText();
mOldTextSize = mTextSize;
mTextSize = getTextSize();
mOldTextCount = mTextCount;
mTextCount = mText.length();

mOldPaint.setColor(mPaint.getColor());
mOldPaint.setTextSize(mOldTextSize);
mPaint.setColor(getCurrentTextColor());
mPaint.setTextSize(getTextSize());
}

private void _calcCharOffset() {
// 略......
// 计算当前字符X坐标
int offset = 0;
mCharOffset[0] = 0;
for (int i = 0; i < mTextCount - 1; i++) {
offset += mPaint.measureText(String.valueOf(mText.charAt(i)));
mCharOffset[i + 1] = offset;
}
// 计算旧字符X坐标
offset = 0;
mOldCharOffset[0] = 0;
for (int i = 0; i < mOldTextCount - 1; i++) {
offset += mOldPaint.measureText(String.valueOf(mOldText.charAt(i)));
mOldCharOffset[i + 1] = offset;
}
// 查找重复字符
mRepeatCharList = RepeatCharHelper.findRepeatChar(mText, mOldText);
}
下面看重复字符串的处理操作,这里定义一个重复字符的参数实体:
public class RepeatChar {

// 上一个索引
private int oldIndex;
// 当前索引
private int curIndex;

public RepeatChar(int oldIndex, int curIndex) {
this.oldIndex = oldIndex;
this.curIndex = curIndex;
}

public int getOldIndex() {
return oldIndex;
}

public void setOldIndex(int oldIndex) {
this.oldIndex = oldIndex;
}

public int getCurIndex() {
return curIndex;
}

public void setCurIndex(int curIndex) {
this.curIndex = curIndex;
}
}
保存了旧索引和当前索引,下面是重复字符串处理操作:
public final class RepeatCharHelper {

private RepeatCharHelper() {
throw new RuntimeException("RepeatCharHelper cannot be initialized!");
}

/**
* 查找重复的字符
* @param curText 当前字符串
* @param oldText 旧的字符串
* @return
*/
public static List<RepeatChar> findRepeatChar(CharSequence curText, CharSequence oldText) {
List<RepeatChar> charList = new ArrayList<>();
if (curText == null || oldText == null) {
return charList;
}
// 用来保存已经重复的当前字符串索引
Set<Integer> skip = new HashSet<>();
for (int i = 0; i < oldText.length(); i++) {
char c = oldText.charAt(i);
for (int j = 0; j < curText.length(); j++) {
if (!skip.contains(j) && c == curText.charAt(j)) {
skip.add(j);
charList.add(new RepeatChar(i, j));
break;
}
}
}
return charList;
}

/**
* 判断旧字符是否需要移动
* @param oldIndex  旧索引
* @param charList  重复的字符列表
* @return  当前字符索引
*/
public static int needMove(int oldIndex, List<RepeatChar> charList) {
for (RepeatChar repeatChar : charList) {
if (repeatChar.getOldIndex() == oldIndex) {
return repeatChar.getCurIndex();
}
}
return -1;
}

/**
* 判断当前字符是否要做变换处理
* @param curIndex  字符索引
* @param charList  重复的字符列表
* @return
*/
public static boolean isNoChange(int curIndex, List<RepeatChar> charList) {
for (RepeatChar repeatChar : charList) {
if (repeatChar.getCurIndex() == curIndex) {
return true;
}
}
return false;
}
}
其实也没什么,就是遍历出重复的字符并保存旧索引和当前索引。
剩下要做的就是绘制操作了:

@Override
protected void onDraw(Canvas canvas) {
if (!mIsAnimationText) {
super.onDraw(canvas);
return;
}
// 变换百分比
float percent;
// 2倍速百分比
float twoPercent = 0;

// 绘制之前字符串
for (int i = 0; i < mOldTextCount; i++) {
percent = mProgress * 1f / mDuration;
if (twoPercent < 1f) {
twoPercent = percent * 2f;
twoPercent = twoPercent > 1.0f ? 1.0f : twoPercent;
}
// 判断是否为重复字符
int curIndex = RepeatCharHelper.needMove(i, mRepeatCharList);
if (curIndex != -1) {
// 计算偏移值
float offset = (mCharOffset[curIndex] - mOldCharOffset[i]) * percent;
// 计算移动中的X坐标
float moveX = mOldCharOffset[i] + offset;
// 计算大小
float size = mOldTextSize + (mTextSize - mOldTextSize) * percent;
mOldPaint.setAlpha(255);
mOldPaint.setTextSize(size);
// 绘制字符
canvas.drawText(String.valueOf(mOldText.charAt(i)), 0, 1, moveX, getBaseline(), mOldPaint);
} else {
// 在进度的50%完成操作
// 透明度
int alpha = (int) (255 * (1 - twoPercent));
// 大小
float size = mOldTextSize * (1 - twoPercent);
mOldPaint.setAlpha(alpha);
mOldPaint.setTextSize(size);
// 绘制字符
canvas.drawText(String.valueOf(mOldText.charAt(i)), 0, 1, mOldCharOffset[i], getBaseline(), mOldPaint);
}
}
// 绘制当前字符串
for (int i = 0; i < mTextCount; i++) {
if (!RepeatCharHelper.isNoChange(i, mRepeatCharList)) {
// 字符动画启动的时间
int startTime = i * EACH_CHAR_DELAY;
// 动画进行的进度百分比,0→1
percent = (mProgress - startTime) * 1.0f / CALC_TIME;
if (percent > 1.0f) {
percent = 1;
} else if (percent < 0) {
percent = 0;
}
// 透明度
int alpha = (int) (255 * percent);
// 大小
float size = mTextSize * percent;
mPaint.setAlpha(alpha);
mPaint.setTextSize(size);
// 绘制字符
canvas.drawText(String.valueOf(mText.charAt(i)), 0, 1, mCharOffset[i], getBaseline(), mPaint);
}
}
}
其实和之前相比就是多个重复字符操作,旧字符串消失操作没啥好讲的,重复字符要做的就是计算偏移坐标,相信看代码注释就清楚了。
整个自定义TextView就这些了,代码里并没有做太多的封装处理,有兴趣可以去看开源项目,里面还有很多更炫的动画效果。我这里只是简单实现一种动画效果,其它的其实就是替换不同的动画了,当然了,想做漂亮的动画往往是很费时的~

源码在这:https://github.com/Rukey7/AnimTextView
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: