Skip to content

属性动画 仿网易彩票奖池数字效果 仿bilibili弹幕

Notifications You must be signed in to change notification settings

rjpacket/PropertyAnim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Sep 7, 2017
33bda30 · Sep 7, 2017

History

6 Commits
Sep 7, 2017
Sep 4, 2017
Sep 4, 2017
Sep 5, 2017
Sep 4, 2017
Sep 4, 2017
Sep 4, 2017
Sep 4, 2017
Sep 4, 2017

Repository files navigation

一、先看网易中奖奖池的效果

wangyi.gif

二、思路

明显的动画效果,而且是随着时间的推移,前面的数字暂停下来。数字的滚动用线程?!当然可以实现。但是前面的数字停下,这种效果不好处理。我开始想的是从最大的数字开始跑,一直减到0,在跑的过程中比对数字的位数,高位的就截取最终的结果,低位的显示当前的计数。同样的很麻烦,而且控制不了显示的位数,万一显示上亿,GG了。 换思路,我发现网易的动画里每个位置上的数字都是很随机的出现,不是规律的递减。那就获取最终结果的位数,然后线程里随机0到9:

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < size; i++) {
            sb.append((int) Math.ceil(Math.random() * 9));
        }
        return sb.toString();

如果size是5,返回的一直是一个五位数,当很快速的刷新的时候,效果就很动画了。 上述代码实际测试可以实现。但是要想实现前面的数字暂停下来的效果,又没有了思路。

三、巧合

在搜索的时候,发现属性动画里有一个参数fraction,一直是从0到1(左右包含)的变化,感觉很有戏。因为这个暂停的数字看作进度,也是从[0,1]区间里变化。

3.1 定义估值器

    public class NumberEvaluator implements TypeEvaluator<String> {
    
        @Override
        public String evaluate(float fraction, String startValue, String endValue) {
            int size = endValue.length();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < size; i++) {
                if(i * 1.0 / size > fraction) {
                    sb.append((int) Math.ceil(Math.random() * 9));
                }else{
                    sb.append(String.valueOf(endValue).charAt(i));
                }
            }
            return sb.toString();
        }
    }

这个类就是传入一个开始值和一个结束值,然后返回无数的中间值,这个fraction默认0到1变化(不知道可不可以改变)。我们遍历显示的位数,如果它与整体的比值大于fraction,说明进度还没有到,如果小于,说明已经过了这个进度,画个图示意一下:

test.png

就是随着时间的流逝,返回最终值得前几位和随机值的组合。

四、代码实现

        ValueAnimator numberAnim = ValueAnimator.ofObject(new NumberEvaluator(), "0", "676767676767");
        numberAnim.setDuration(3000);
        numberAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                String number = (String) animation.getAnimatedValue();
                tvNumber.setText(number);
            }
        });
        numberAnim.start();
        numberAnim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                tvNumber.setText("676767676767");
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

new一个定义的估值器扔进去,起始值不关心,传空,传一个最终的显示值,然后动画里不断的监听拿到中间值设置进去,动画结束,把最终的值设置上,结束。

五、最终效果

number.gif

附上简书地址,有兴趣可以肛一波。

一、先看掌阅动画效果

掌阅动画.gif

二、分析

从动画上看,可以拆解分为三部分进行:

  1. 书壳的翻转动画;
  2. 书扉页的放大动画;
  3. 书壳的放大动画。(这个动画和扉页是同步进行的,但是仔细看,可以看到,书壳的高度变化速率和扉页是一样的,但是宽度的速率要低于扉页)

三、代码实现

我们选择用属性动画实现,那么首先要确定哪些属性是变化的。首先扉页放大的过程中,宽高是变化的,随着宽高变化,位置也需要调整,至少需要记录left和top。那就是至少需要四个值。但是宽可以通过

宽度 = right - left 高度 = bottom - top

索性我们建立一个对象来存储每一本书的(left, top, right, bottom)。

public class BookValue {
    private int left;
    private int top;
    private int right;
    private int bottom;
}

这样我们问题就变成了,如果知道起始和终点的BookValue,那么就可以计算出中间如何一个位置的BookValue。有没有很熟悉?没错就是属性动画的自定义估值器。

public class BookEvaluator implements TypeEvaluator<BookValue> {
    @Override
    public BookValue evaluate(float fraction, BookValue startValue, BookValue endValue) {
        int startLeft = startValue.getLeft();
        int startTop = startValue.getTop();
        int startRight = startValue.getRight();
        int startBottom = startValue.getBottom();

        int endLeft = endValue.getLeft();
        int endTop = endValue.getTop();
        int endRight = endValue.getRight();
        int endBottom = endValue.getBottom();

        BookValue bookValue = new BookValue();
        bookValue.setLeft((int) (startLeft - fraction * (startLeft - endLeft)));
        bookValue.setTop((int) (startTop - fraction * (startTop - endTop)));
        bookValue.setRight((int) (startRight - fraction * (startRight - endRight)));
        bookValue.setBottom((int) (startBottom - fraction * (startBottom - endBottom)));

        return bookValue;
    }
}

新建一个估值器,传进去开始和结束的BookValue,计算返回中间的BookValue。计算不说了,那么怎么确定开始和结束位置呢,结束位置很好确定,就是整个屏幕。(实际操作中发现需要减去状态栏的高度,如果追求精度的话需要考虑,我们这里没有考虑) 那么起始位置怎么确定呢?我们点击的时候是可以拿到当前这个View的,这个View的属性里面就带了需要的位置信息。

convertView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ViewGroup window = (ViewGroup) getWindow().getDecorView();
        BookValue startValue = new BookValue(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
        BookValue endValue = new BookValue(window.getLeft(), window.getTop(), window.getRight(), window.getBottom());

        BookView bookView = new BookView(mContext);
        window.addView(bookView);
        bookView.startAnim(startValue, endValue);
    }
});            

获取屏幕的DecorView,可以说这个view的位置就是结束位置。onClick里面的view就是点击的开始位置。点击的view里面有两个子View,一个是书壳一个是书扉页,我们可以把两个view组合成一个自定义的ViewGroup:

public class BookView extends FrameLayout{

    private TextView tvBookName;
    private TextView tvBookAuthor;

    public BookView(@NonNull Context context) {
        super(context);
        initView(context);
    }

    public BookView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public BookView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    public void initView(Context context) {
        LayoutInflater.from(context).inflate(R.layout.layout_book_view, this);
        setBackgroundColor(Color.TRANSPARENT);
        tvBookName = (TextView) findViewById(R.id.tv_book_name);
        tvBookAuthor = (TextView) findViewById(R.id.tv_book_author);
    }
}

BookView继承自Framelayout,再看它的布局:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_book_author"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="作者:\n小\n黑"
        android:gravity="center"
        android:background="#6d4c7c"
        android:textColor="#fff"
        />

    <TextView
        android:id="@+id/tv_book_name"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="android\n入\n门\n到\n放\n弃"
        android:gravity="center"
        android:background="#4d3b3b"
        android:textColor="#fff"
        />
</merge>

只有两个TextView,因为BookView是Framelayout,所以书壳在扉页的下面。细心的人可以看到BookView我们设置背景透明的,方便动画实现,省去了还要计算外层布局的位置,直接让它透明的充满全屏。 我们来看BookView里面的实现:

    /**
     * 开启动画
     */
    public void startAnim(BookValue startValue, BookValue endValue) {
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue);
        valueAnimator.setDuration(3000);
        valueAnimator.addUpdateListener(this);
        valueAnimator.addListener(this);

        tvBookName.setPivotX(0);
        tvBookName.setPivotY(500);
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180);
        objectAnimator.setDuration(1000);
        objectAnimator.setStartDelay(300);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(valueAnimator, objectAnimator);
        animatorSet.start();
    }

暴露一个开启动画的方法,动画上面知道肯定是两个(实际上是三个,这里简化了,所以效果也有点粗糙):

ValueAnimator valueAnimator = ValueAnimator.ofObject(new BookEvaluator(), startValue, endValue);

新建一个估值器,直接扔进属性动画里,这里不需要指定动画的作用对象,因为它实际就是开启了一个计算,不断的回调计算结果。那么回调去哪了呢?后面再说。再看第二个动画:

  tvBookName.setPivotX(0);
  tvBookName.setPivotY(500);
  ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvBookName, "rotationY", 0, -180);
  objectAnimator.setDuration(1000);
  objectAnimator.setStartDelay(300);

这个动画是作用于书壳上的,首先要设定锚点,不然它会默认中间翻转。属性动画设置rotationY属性变化,谁的属性?第一个参数tvBookName。怎么变化?从0到-180,也就是逆时针。 锚点的PivotX等于0很好理解,就是绕y轴翻转,但是PivotY怎么理解呢?我也不清楚,但是设置了一个很大的值,它和屏幕会有一个夹角,很接近掌阅的效果。 还有一个延迟300ms,书壳是先随着扉页一起放大,再翻转,效果上就很真实了。

上面有说计算结果回调去哪了,有重写的方法回调:

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        BookValue midBookValue = (BookValue) animation.getAnimatedValue();

        tvBookName.setX(midBookValue.getLeft());
        tvBookName.setY(midBookValue.getTop());
        ViewGroup.LayoutParams layoutParams = tvBookName.getLayoutParams();
        layoutParams.width = midBookValue.getRight() - midBookValue.getLeft();
        layoutParams.height = midBookValue.getBottom() - midBookValue.getTop();
        tvBookName.setLayoutParams(layoutParams);

        tvBookAuthor.setX(midBookValue.getLeft());
        tvBookAuthor.setY(midBookValue.getTop());
        ViewGroup.LayoutParams layoutParams1 = tvBookAuthor.getLayoutParams();
        layoutParams1.width = midBookValue.getRight() - midBookValue.getLeft();
        layoutParams1.height = midBookValue.getBottom() - midBookValue.getTop();
        tvBookAuthor.setLayoutParams(layoutParams1);
    }

首先获取midBookValue,这个值是不断的返回的,所以下面的方法要不断的修改作用对象的属性,就形成了动画的视觉效果。那么我们需要修改哪些属性呢? 首先修改X与Y,这个书壳就会产生偏移(这里不能去设置left和top,会有坑,一直显示在屏幕的原点位置。至于原因,还没发现)。 然后修改书壳的宽高,是通过获取LayoutParams修改width和height实现的。 书扉页也需要同样的操作。 以上就是全部的思路。

四、最终效果

电子书动画.gif

这样看起来有点粗糙,是因为书壳和扉页宽度的变化是一致的,我们可以将书壳的宽度做一下限制,或者将他们的动画分开来写。

附上简书地址,有兴趣可以肛一波。

About

属性动画 仿网易彩票奖池数字效果 仿bilibili弹幕

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages