美文网首页自定义view相关AndroidAndroid 自定义view
Android自定义View实例-QQBubbleView实现

Android自定义View实例-QQBubbleView实现

作者: 晴明_ | 来源:发表于2016-07-27 16:37 被阅读990次

    参考项目地址 https://github.com/Yasic/QQBubbleView

    最近网上看了一个模仿QQ空间直播页面右下角的礼物冒泡特效,感觉效果不错所以参考源码实现了一下。

    本次自定义View主要运用的是属性动画的知识,不熟悉的可参考:
    1.Android属性动画完全解析(上),初识属性动画的基本用法
    2.Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
    3.Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
    4.属性动画中的插值器和估值器

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

    通过效果图,我们分解一下只看一个♥️的运动轨迹,可以发现:

    • 1.♥️运动的范围是整个屏幕(除状态栏和标题栏),并且可以重叠所以布局上面我们可以选择继承FrameLayout或RelativeLayout,因为FrameLayout复杂度比RelativeLayout低所以我选择的是FrameLayout
    • 2.冒出♥️的地方位于屏幕最下方,并且大体水平居中,所以我选的位置是(getWidth()/2, getHeight())这个点
    • 3.♥️y轴运动范围是固定的(从最上方),x轴运动范围是随机的(但都在屏幕可视范围内所以是0到getWidth()),所以y轴的偏移范围为getHeight()到0,x轴偏移范围为getWidth()/2到getWidth()* Math.random(),所以可以定义两个偏移动画:
    ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", Float.valueOf(getHeight()), 0f);
    ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofFloat(tempImageView, "translationX", getWidth() / 2f, (float) (getWidth()* Math.random()));
    
    • 4.桃心有一个慢慢变浅的效果,所以再定义一个渐变动画:
    ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);
    

    接下来我们可以开始写代码了

    首先定义一个类继承自FrameLayoutpublic class CustomView extends FrameLayout,并实现一下构造方法。
    接着对外定义一个方法,触发动画效果public void startAnimator(),每次触发方法内我们每次创建一个ImageView,背景设置为准备好的♥️图片(我直接从原项目中copy过来的),添加到布局中,并播放动画包含上述的三个动画),即:

    public void startAnimator() {
        ImageView tempImageView = new ImageView(getContext());  //创建一个ImageView
        tempImageView.setImageResource(R.mipmap.ic_favorite_red_900_24dp);  //设置背景
        addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));//添加到布局中
    
        ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);  //渐渐变淡动画
        ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofFloat(tempImageView, "translationX", getWidth() / 2, (float) (getWidth()* Math.random()));  //x轴偏移动画
        ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);  //y轴偏移动画
    
        AnimatorSet animatorSet = new AnimatorSet();  //动画集合,集中动画一起播放
        animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
        animatorSet.setDuration(5000);
        animatorSet.start();
    }
    

    好了,效果差不多实现了,我们在项目中运行一下试试吧(由于每次点击只会添加一个♥️,所以可以快速点击查看一下效果)


    效果差不多,不过♥️的运动轨迹还是一条倾斜的直线,怎么才能像原项目那样不规则的运行呢,接下来我们试着给x轴的偏移动画加个插值器试试看吧:
    public void startAnimator() {
        ImageView tempImageView = new ImageView(getContext());  //创建一个ImageView
        tempImageView.setImageResource(R.mipmap.ic_favorite_red_900_24dp);  //设置背景
        addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));//添加到布局中
    
        ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);  //渐渐变淡动画
        ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofFloat(tempImageView, "translationX", getWidth() / 2, (float) (getWidth()* Math.random()));  //x轴偏移动画
        riseTranslationXAnimator.setInterpolator(new BounceInterpolator());  //为x轴动画设置插值器
        ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);  //y轴偏移动画
    
        AnimatorSet animatorSet = new AnimatorSet();  //动画集合,集中动画一起播放
        animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
        animatorSet.setDuration(5000);
        animatorSet.start();
    }
    

    再次运行试试看看效果(系统为我们提供了好几种插值器,大家可以都试试看):



    这样♥️运行的轨迹就不规则了,但是看起来还不是那么自然,那么下面我们运用一下三阶贝塞尔曲线看看吧(原项目中x轴y轴的点都用了三阶贝塞尔曲线,这里我只针对x轴),下面我门在估值器中利用三阶贝塞尔曲线计算x轴的路径

    public void startAnimator() {
        ImageView tempImageView = new ImageView(getContext());  //创建一个ImageView
        tempImageView.setImageResource(R.mipmap.ic_favorite_red_900_24dp);  //设置背景
        addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));//添加到布局中
    
        ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);  //渐渐变淡动画
    
        BesselEvaluator besselEvaluator = new BesselEvaluator((float) (getWidth()* Math.random()), (float) (getWidth()* Math.random()));//三阶贝塞尔曲线需要四个值,起点、重点我们确定了,我们再随机取两个中间点
        ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofObject(tempImageView, "translationX", besselEvaluator, getWidth() / 2f, (float) (getWidth()* Math.random()));  //x轴偏移动画
        ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);  //y轴偏移动画
    
        AnimatorSet animatorSet = new AnimatorSet();  //动画集合,集中动画一起播放
        animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
        animatorSet.setDuration(5000);
        animatorSet.start();
    }
    
    public class BesselEvaluator implements TypeEvaluator<Float>{
        private Float secondX;
        private Float thirdX;
        public BesselEvaluator(Float sencodX, Float thirdX){
            this.secondX = sencodX;
            this.thirdX = thirdX;
        }
        @Override
        public Float evaluate(float fraction, Float start, Float end) {
            return Float.valueOf(start * (1 - fraction) * (1 - fraction) * (1 - fraction)
                    + secondX * 3 * fraction * (1 - fraction) * (1 - fraction)
                    + thirdX * 3 * (1 - fraction) * fraction * fraction
                    + end * fraction * fraction * fraction);
        }
    }
    

    我们再来看一下实现的效果吧:



    这次看起来舒服多了,接下来我们只需要为每个♥️随机设置背景图片就可以实现原项目中的效果了:



    这个自定义的View主要运用的是属性动画的知识,不涉及测量、布局、事件分发等,所以实现起来比较容易学,下面把我的代码贴出来,方便大家理解,如需可运行的代码请到原项目中获取(实现稍微有点不一样,大同小异):
    CustomView.java
    public class CustomView extends FrameLayout {
    
        private int[] imgResids = {R.mipmap.ic_favorite_indigo_900_24dp
                , R.mipmap.ic_favorite_deep_purple_900_24dp
                , R.mipmap.ic_favorite_cyan_900_24dp
                , R.mipmap.ic_favorite_deep_purple_900_24dp
                , R.mipmap.ic_favorite_light_blue_900_24dp
                , R.mipmap.ic_favorite_lime_a200_24dp
                , R.mipmap.ic_favorite_purple_900_24dp
                , R.mipmap.ic_favorite_pink_900_24dp
                , R.mipmap.ic_favorite_red_900_24dp};
    
        private int viewWidth = dp2pix(16), viewHeight = dp2pix(16);
    
        public CustomView(Context context) {
            super(context);
        }
        public CustomView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public void startAnimator() {
            ImageView tempImageView = new ImageView(getContext());
            tempImageView.setImageResource(imgResids[(int) (Math.random() * imgResids.length)]);
            addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));
            ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);
            BesselEvaluator besselEvaluator = new BesselEvaluator((float) (getWidth() * Math.random()), (float) (getWidth() * Math.random()));
            ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofObject(tempImageView, "translationX", besselEvaluator, getWidth() / 2f, (float) (getWidth() * Math.random()));
            ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
            animatorSet.setDuration(5000);
            animatorSet.start();
        }
    
        public class BesselEvaluator implements TypeEvaluator<Float> {
            private Float secondX;
            private Float thirdX;
            public BesselEvaluator(Float sencodX, Float thirdX) {
                this.secondX = sencodX;
                this.thirdX = thirdX;
            }
            @Override
            public Float evaluate(float fraction, Float start, Float end) {
                return Float.valueOf(start * (1 - fraction) * (1 - fraction) * (1 - fraction)
                        + secondX * 3 * fraction * (1 - fraction) * (1 - fraction)
                        + thirdX * 3 * (1 - fraction) * fraction * fraction
                        + end * fraction * fraction * fraction);
            }
        }
    
        private int dp2pix(int dp) {
            float scale = getResources().getDisplayMetrics().density;
            int pix = (int) (dp * scale + 0.5f);
            return pix;
        }
    }
    

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
        private CustomView mCustomView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mCustomView = (CustomView) findViewById(R.id.bubbleView);
        }
        public void clickButton(View view) {
            mCustomView.startAnimator();
        }
    }
    

    activity_main

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.zhong.qqlivebubble.MainActivity">
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:onClick="clickButton"
            android:text="开始"/>
        <com.zhong.qqlivebubble.CustomView
            android:id="@+id/bubbleView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"/>
    </RelativeLayout>
    

    相关文章

      网友评论

      • 断金岁月:导进去你参考地址中的那个项目 总是报Error:Connection refused: connect-_-
      • fendo:赞一个!!

      本文标题:Android自定义View实例-QQBubbleView实现

      本文链接:https://www.haomeiwen.com/subject/pjnnjttx.html