自定义View——文本字母跳转替换

我尽量不打错别字,用词准确,不造成阅读障碍。

最近看到一个很有意思的自定义View,效果是这样的:

在这里插入图片描述

效果很nice,虽然实际开发中可能用到的场景少的很。

从作者给的github上拿到源码,看了一下,感觉有一些可以学习的地方,作者也有解释,我想结合自己的理解记录了一下,希望能帮到别人,主要是想记个笔记。

先说一下我理解的整体思路:我们有两个字符串,首先要把两个字符串的每一个字符拆开并挨个计算宽度,分别存放入两个list中,同时筛选出新、旧字符串中相同的文本及其在新、旧字符串中的位置,记录到一个list中,list实体需要自己定义。最后在onDraw中通过对比新、旧字符串集合与筛选出的集合中是否有相同的字符,判断每一个字符是否需要动画转移,如果不需要就直接drawText,但是Paint加了透明度,达到动画效果。其中因为字符位置等原因还要判断位移量等。

代码解释一下:

byteBeatTextView = findViewById(R.id.etv);
Button button = findViewById(R.id.btn);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (index >= (sentences.length - 1)) {
            index = 0;
        }
        byteBeatTextView.animateText(sentences[index++]);
    }
});
byteBeatTextView.animateText("hello world");

//数据源
private String[] sentences = {
            "A material",
            "metaphor is the unifying theory",
            "of a rationalized space and a system of motion",
            "material",
            "grounded",
            "in tactile reality",
            "inspired",
            "study of paper and ink",
            "understand",
            "new affordances",
            "The fundamentals of light, surface, and movement are key to conveying how objects move",
            "interact",
            "divides space",
            "fundamentals",
            "欢迎关注文淑",
            "文淑博客",
            "Android"
    };

首先我们要设置初始字符串,也就是animateText()方法,在这个方法里我们要对字符串做很多处理;

public void animateText(CharSequence text) {
    setText(text);
    mOldText = mNewText; 
    mNewText = text;

    prepareAnimate();  //初始化画笔,并将新、旧字符串每一个字符拆分保存
    animatePrepare();  //筛选出2个字符串中相同的字符保存
    animateStart();    //设置动画属性,包括时间,并开始动画
}

注释写的蛮齐全的,看看具体的代码吧:

private void prepareAnimate() {
   //初始化相关数据
   mTextSize = getTextSize();
   mPaint.setTextSize(mTextSize);
   mPaint.setColor(getCurrentTextColor());
   mPaint.setTypeface(getTypeface());

   gapList.clear();
  //拆分每一个字符并保存其宽度
   for (int i = 0; i < mNewText.length(); i++) {
        gapList.add(mPaint.measureText(String.valueOf(mNewText.charAt(i))));
   }
   mOldPaint.setTextSize(mTextSize);
   mOldPaint.setColor(getCurrentTextColor());
   mOldPaint.setTypeface(getTypeface());

   oldGapList.clear();
   //拆分每一个字符并保存其宽度
   for (int i = 0; i < mOldText.length(); i++) {
        oldGapList.add(mOldPaint.measureText(String.valueOf(mOldText.charAt(i))));
   }
}
private void animatePrepare() {
    diffs.clear();
    diffs.addAll(CharacterDiffUtil.diff(mOldText, mNewText));//筛选出相同的字符并保存

    Rect bounds = new Rect();
    mPaint.getTextBounds(mNewText.toString(), 0, mNewText.length(), bounds);
    mTextHeight = bounds.height();//获取文本高度
}

先介绍diff()方法,一会儿再介绍animateStart(),因为是比较核心的代码:

public static List<CharacterDiff> diff(CharSequence oldText, CharSequence newText) {
     List<CharacterDiff> diffList = new ArrayList<>();  //new一个新的list
     Set<Integer> skip = new HashSet<>();               //HashSet,不允许有重复
     for (int i = 0; i < oldText.length(); i++) {
          char c = oldText.charAt(i);
          for (int j = 0; j < newText.length(); j++) {
               if (!skip.contains(j) && c == newText.charAt(j)) {
                   skip.add(j);
                   CharacterDiff diff = new CharacterDiff();
                   diff.c = c;
                   diff.fromIndex = i;  //记录在旧字符串中位置
                   diff.moveIndex = j;  //记录在新字符串中的位置,
                   diffList.add(diff);
                   break;
                }
           }
     }
     return diffList;
}

拿到新、旧字符串中相同的字符。其实我觉得叫commonList不好吗?diff ? ? ?再看看animateStart():

private void animateStart() {
    int n = mNewText.length();
    n = n <= 0 ? 1 : n;
    duration = (long) (charTime + charTime / mostCount * (n - 1));
    animator.cancel();
    animator.setFloatValues(0, 1);
    animator.setDuration(duration);
    animator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            getOldStartX();
        }
    });
    animator.start();
}
private void getOldStartX() {
    try {
         int layoutDirection = ViewCompat.getLayoutDirection(ByteBeatTextView.this);
         oldStartX = layoutDirection == LAYOUT_DIRECTION_LTR ? getLayout().getLineLeft(0) : getLayout().getLineRight(0);
    } catch (Exception e) {
          e.printStackTrace();
    }
}

到此,准备工作就都做完了,该分解的都分解了也都保存了,接下来就是对比剔除和动画实现了。当animator.start()后,会触发一个监听事件,这个监听事件在view初始化的时候注册的:

public ByteBeatTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNewText = "";
        mOldText = getText();

        setMaxLines(1); //设置最多只能有一行
        setEllipsize(android.text.TextUtils.TruncateAt.END); //末尾多余省略...


        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mOldPaint = new TextPaint(mPaint);
        mOldPaint.setAntiAlias(true);

        post(new Runnable() {
            @Override
            public void run() {
                mTextSize = getTextSize();
                mWidth = getWidth();
                mHeight = getHeight();
                oldStartX = 0;
                getOldStartX();
            }
        });

        prepareAnimate();

        animator = new ValueAnimator();
        animator.setInterpolator(new AccelerateDecelerateInterpolator()); //加速减速插值器
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                progress = (float) animation.getAnimatedValue();
                invalidate();  //在监听事件中进行刷新
            }
        });

        int n = mNewText.length();
        n = n <= 0 ? 1 : n;
        duration = (long) (charTime + charTime / mostCount * (n - 1));
    }

监听事件中有刷新,这样会调用onDraw方法,而且显然这个调用很频繁,看看onDraw里面写了什么:

@Override
protected void onDraw(Canvas canvas) {
     float startX = getLayout().getLineLeft(0);
     float startY = getBaseline();  //获取基线坐标

     float offset = startX;
     float oldOffset = startY;

     int maxLength = Math.max(mNewText.length(), mOldText.length());
     for (int i = 0; i < maxLength; i++) {
          //旧文本平移动画
          if (i < mOldText.length()) {
              float pp = progress;
              mOldPaint.setTextSize(mTextSize);
              int move = CharacterDiffUtil.needMove(i, diffs); //对比字符判断是否需要移动
              if (move != -1) {
                  //如果字符需要移动
                  mOldPaint.setAlpha(255);
                  float p = pp * 2f;
                  p = p > 1 ? 1 : p;
                  float distX = CharacterDiffUtil.getOffset(i, move, p, startX, oldStartX, gapList, oldGapList);
                  canvas.drawText(mOldText.charAt(i) + "", 0, 1, distX, startY, mOldPaint);
              } else {
                  //如果该字符是不需要移动的,即可抛弃的,就通过透明度动画让它消失掉
                  //透明度动画
                  mOldPaint.setAlpha((int) ((1 - pp) * 255));
                  float y = startY - pp * mTextHeight;
                  canvas.drawText(mOldText.charAt(i) + "", 0, 1, oldOffset, y, mOldPaint);
              }
              oldOffset += oldGapList.get(i);
          }
          //新文本平移动画
          if (i < mNewText.length()) {
              if (!CharacterDiffUtil.stayHere(i, diffs)) {
                 // 渐显效果 延迟
                  int alpha = (int) (255f / charTime * (progress * duration - charTime * i / mostCount));
                  alpha = alpha > 255 ? 255 : alpha;
                  alpha = alpha < 0 ? 0 : alpha;
                  mPaint.setAlpha(alpha);
                  mPaint.setTextSize(mTextSize);
                  float pp = progress;
                  float y = mTextHeight + startY - pp * mTextHeight;

                  float width = mPaint.measureText(mNewText.charAt(i) + "");
                  canvas.drawText(mNewText.charAt(i) + "", 0, 1, offset + (gapList.get(i) - width) / 2, y, mPaint);
           }
           offset += gapList.get(i);
          }
     }
}

在onDraw方法中,判断旧字符串中字符是否需要移动,需要移动就根据在新、旧字符串中的位置(宽度)计算需要移动多少,但是并不是一次onDraw移动到位,而是多次onDraw移动完,达到动画效果;不需要移动的就慢慢淡化;新字符串中判断出那些字符是可以替换的,不可替换的就直接drawText,同时慢慢移动,多次onDraw达到动画效果,可替换的就直接不画出来;所以总的来说onDraw多次调用,在每一次调用时旧字符串会有一点移动和淡化,新字符串也会有一点移动和淡化,最终达到两者同时动画和整个动画的目的,但是字符串越长调用次数越多,好像不太合适的样子?

附上我自己写的demo地址:
https://github.com/longlong-2l/MySelfViewDemo/tree/master/app/src/main/java/com/study/longl/myselfviewdemo/Views/ByteBeatView

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自定义控件是Android开发中常见的任务之一。下面是一步一步教你如何自定义控件的简要指南: 第一步:创建一个新的Java类作为你的自定义控件。 首先,创建一个新的Java类,可以命名为你想要的控件名称。这个类应该继承自Android框架中的现有控件,例如View、TextView等。例如,如果你想要创建一个自定义按钮,可以创建一个名为CustomButton的类,并让它继承自Button类。 第二步:实现构造函数和属性。 在你的自定义控件类中,你可以实现构造函数和属性,以便对控件进行初始化和设置。你可以定义自己的属性,例如颜色、大小等,以及相应的getter和setter方法。 第三步:重写绘制方法。 要自定义控件的外观,你需要重写它的绘制方法。最常用的方法是重写`onDraw()`方法,在其中使用Canvas绘制你想要的形状、文本等。 第四步:处理用户交互。 如果你的自定义控件需要与用户进行交互,你可以重写相应的触摸事件(例如`onTouchEvent()`)或点击事件(例如`setOnClickListener()`)来处理用户操作。 第五步:在布局文件中使用自定义控件。 完成以上步骤后,你可以在布局文件中使用你的自定义控件了。只需在布局文件中添加一个与你的控件类名相对应的XML标签,并设置相应的属性。 这只是一个简要的指南,帮助你开始自定义控件的过程。在实际开发中,你可能需要更多的步骤和细节来完成你的自定义控件。你可以参考Android官方文档或其他教程来获取更多信息和示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值