千古八方的博客

  • 程序开发
    • Android
    • Spring Boot
  • 数据私有化
    • NAS
    • 私有云
    • 内网穿透
    • 服务器
  • 留言
  • 搞事
进学
为学无间断,如流水行云,日进而不已也
  1. 首页
  2. Android
  3. 正文

Android widget - 直播右下角点击刷礼物特效

2022年6月22日 336点热度 1人点赞 2条评论
目录
Widget部件介绍
使用说明及Demo
FavorLayout.java 源码文件
widget 链接集合
Widget部件介绍

Android  widget  -  直播右下角点击刷礼物特效,自动以及手动点击锚点视图,添加礼物。新添加的礼物沿着贝塞尔曲线,随机选择加速、减速加速器进行运动。

 

软件架构

Android 礼物动画特效

 

使用说明及Demo

1.  主Activity布局XML里添加FavorLayout以及锚点View   

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">

    <org.tcshare.widgets.FavorLayout
        android:id="@+id/flavorLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="bottom">
    </org.tcshare.widgets.FavorLayout>

    <TextView
        android:id="@+id/ancher"
        android:layout_width="160dp"
        android:layout_height="60dp"
        android:layout_gravity="bottom|right"
        android:layout_marginBottom="42dp"
        android:layout_marginRight="42dp"
        android:background="#99999999"
        android:text="锚点视图,浮动效果从该View中心点开始"/>
</FrameLayout>

2. 主Activity 完整演示代码如下

public class TCLiveLikeActivity extends AppCompatActivity {

    private FavorLayout favor;
    private final Handler handler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if(msg.what == 1){
                handler.sendEmptyMessageDelayed(1, 200);
                favor.addFavor();
            }
        }
    };

    @Override
    protected void onDestroy() {
        handler.removeMessages(1);
        super.onDestroy();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tc_live_like);

        favor = findViewById(R.id.flavorLayout);
        Resources res = getResources();
        List<Drawable> items = new ArrayList<Drawable>(){
            {
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_1, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_2, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_3, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_4, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_5, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_6, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_7, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_8, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_9, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_10, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.gift_11, null));
            }
        };
        // 使用自定义的效果图标,默认使用♥形状
        favor.setFavors(items);
        // 设置效果图标,左右飘动的范围,以及终止点的范围
        favor.setFavorWidthHeight(100, 400);
        // 设置AnchorView,效果图标会从该AnchorView的中心点飘出
        favor.setAnchor(findViewById(R.id.ancher));


        // 自动添加效果图标
        handler.sendEmptyMessageDelayed(1, 200);

        // 手动点击添加
        findViewById(R.id.ancher).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                favor.addFavor();
            }
        });
    }
}

 

 

FavorLayout.java 源码文件

package org.tcshare.widgets;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;

import androidx.core.content.res.ResourcesCompat;

import org.tcshare.androidutils.R;
import org.tcshare.utils.DensityUtil;
import org.tcshare.utils.RandomUtils;

import java.util.ArrayList;
import java.util.List;

/**
 *
 */
public class FavorLayout extends RelativeLayout {
    private static final String TAG = FavorLayout.class.getSimpleName();

    private int iHeight = 120;
    private int iWidth = 120;
    private int mHeight;
    private int mWidth;

    private LayoutParams lp;
    private List<Drawable> loves;

    private final List<Interpolator> interpolates = new ArrayList<Interpolator>() {
        {
            add(new LinearInterpolator());
            //add(new AccelerateInterpolator());
            add(new DecelerateInterpolator());
            add(new AccelerateDecelerateInterpolator());
        }
    };
    private PointF startPoint = new PointF();
    private PointF anchorPoint;
    private View anchorView;
    private int favorWidth = -1, favorHeight = -1;
    private boolean stop = false;


    public FavorLayout(Context context) {
        super(context);
        init();
    }

    public FavorLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        startPoint.set((mWidth - iWidth) / 2f, mHeight - iHeight);
    }

    private void init() {
        //底部 并且 水平居中
        lp = new LayoutParams(iWidth, iHeight);
        lp.addRule(CENTER_HORIZONTAL, TRUE); //这里的TRUE 要注意 不是true
        lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);

        Resources res = getResources();
        //初始化显示的图片
        loves = new ArrayList<Drawable>() {
            {
                add(ResourcesCompat.getDrawable(res, R.mipmap.love_a, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.love_b, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.love_c, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.love_d, null));
                add(ResourcesCompat.getDrawable(res, R.mipmap.love_e, null));
            }

        };

    }

    public void setAnchor(final View view) {
        anchorView = view;
        resetAnchorPoint();
    }

    private void resetAnchorPoint() {
        if (anchorView != null) {
            anchorView.post(new Runnable() {
                @Override
                public void run() {
                    int[] outLocation = new int[2];
                    anchorView.getLocationOnScreen(outLocation);
                    float x = outLocation[0] + (anchorView.getWidth() - iWidth) / 2f;
                    float y = outLocation[1] - (anchorView.getHeight() - iHeight) / 2f;
                    anchorPoint = new PointF(x, y);
                }
            });
        }
    }

    /**
     * 点赞
     * 对外暴露的方法
     */
    public void addFavor() {
        if (stop) {
            return;
        }
        ImageView imageView = new ImageView(getContext());
        // 随机选一个
        imageView.setImageDrawable(RandomUtils.getRandomElement(loves));

        if (anchorPoint == null) {
            imageView.setLayoutParams(lp);
        } else {
            imageView.setX(anchorPoint.x - getX());
            imageView.setY(anchorPoint.y - getY());
        }

        addView(imageView);
        Log.d(TAG, "addFavor: " + "add后子view数:" + getChildCount());

        Animator set = getAnimator(imageView);
        set.addListener(new AnimEndListener(imageView));
        set.start();

    }

    /**
     * 设置动画
     */
    private Animator getAnimator(View target) {
        AnimatorSet set = getEnterAnimator(target);

        ValueAnimator bezierValueAnimator = getBezierValueAnimator(target);

        AnimatorSet finalSet = new AnimatorSet();
        finalSet.playSequentially(set);
        finalSet.playSequentially(set, bezierValueAnimator);
        finalSet.setInterpolator(RandomUtils.getRandomElement(interpolates));//实现随机变速
        finalSet.setTarget(target);
        return finalSet;
    }


    /**
     * 设置初始动画
     * 渐变 并且横纵向放大
     */
    private AnimatorSet getEnterAnimator(final View target) {

        ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
        AnimatorSet enter = new AnimatorSet();
        enter.setDuration(500);
        enter.setInterpolator(new LinearInterpolator());
        enter.playTogether(alpha, scaleX, scaleY);
        enter.setTarget(target);
        return enter;
    }

    public void setFavorWidthHeight(int width, int height) {
        this.favorWidth = DensityUtil.dp2px(getContext(), width);
        this.favorHeight = DensityUtil.dp2px(getContext(), height);
    }

    private PointF getPointLow() {
        PointF pointF = new PointF();
        if (anchorView != null && favorWidth != -1 && favorHeight != -1 && anchorPoint != null) {
            // 中心点
            float x = anchorPoint.x - getX();
            float y = anchorPoint.y - getY();
            pointF.x = x - favorWidth / 2f + RandomUtils.getRandomInt(favorWidth);
            pointF.y = y - favorHeight / 4f - RandomUtils.getRandomInt(favorHeight / 4);

        } else {
            //减去100 是为了控制 x轴活动范围
            pointF.x = RandomUtils.getRandomInt(mWidth - 100);
            //再Y轴上 为了确保第二个控制点 在第一个点之上,我把Y分成了上下两半
            pointF.y = RandomUtils.getRandomInt(mHeight - 100) / 2f;
        }

        return pointF;
    }

    private PointF getPointHeight() {
        PointF pointF = new PointF();
        if (anchorView != null && favorWidth != -1 && favorHeight != -1 && anchorPoint != null) {
            // 中心点
            float x = anchorPoint.x - getX();
            float y = anchorPoint.y - getY();
            pointF.x = x - favorWidth / 2f + RandomUtils.getRandomInt(favorWidth);
            pointF.y = y - favorHeight / 2f - RandomUtils.getRandomInt(favorHeight / 4);

        } else {
            pointF.x = RandomUtils.getRandomInt(mWidth - 100);
            pointF.y = RandomUtils.getRandomInt(mHeight - 100);
        }

        return pointF;
    }

    private PointF getEndPoint() {
        PointF pointF = new PointF();
        if (anchorView != null && favorWidth != -1 && favorHeight != -1 && anchorPoint != null) {
            // 中心点
            float x = anchorPoint.x - getX();
            float y = anchorPoint.y - getY();
            pointF.x = x - favorWidth / 2f + RandomUtils.getRandomInt(favorWidth);
            pointF.y = y - favorHeight;

        } else {
            pointF.set(RandomUtils.getRandomInt(getWidth()), 0);
        }

        return pointF;
    }

    /**
     * 获取贝塞尔曲线动画
     */
    private ValueAnimator getBezierValueAnimator(View target) {

        //初始化一个BezierEvaluator
        BezierEvaluator evaluator = new BezierEvaluator(getPointLow(), getPointHeight());

        // 起点固定,终点随机
        ValueAnimator animator = ValueAnimator.ofObject(evaluator,
                anchorPoint == null ? startPoint : new PointF(anchorPoint.x - getX(), anchorPoint.y - getY()),
                getEndPoint());
        animator.addUpdateListener(new BezierListener(target));
        animator.setTarget(target);
        animator.setDuration(3000);
        return animator;
    }


    /**
     * 设置点赞效果集合
     *
     * @param items 浮动的图片
     */
    public void setFavors(List<Drawable> items) {
        loves.clear();
        loves.addAll(items);
        if (items.size() == 0) {
            throw new UnsupportedOperationException("点赞效果图片不能为空");
        }

        this.iWidth = items.get(0).getIntrinsicWidth();
        this.iHeight = items.get(0).getIntrinsicHeight();
        startPoint = new PointF((mWidth - iWidth) / 2f, mHeight - iHeight);
        resetAnchorPoint();
    }

    public void setStat(boolean stop) {
        this.stop = stop;
    }


    private class AnimEndListener extends AnimatorListenerAdapter {
        private final View target;

        public AnimEndListener(View target) {
            this.target = target;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            //因为不停的add 导致子view数量只增不减,所以在view动画结束后remove掉
            removeView((target));
            Log.v(TAG, "removeView后子view数:" + getChildCount());
        }
    }


}

widget 链接集合

  • Android widget - 完全可配的炫酷仪表盘
  • Android widget - 支持实际距离的雷达扫描控件
  • Android widget - 直播右下角点击刷礼物特效
标签: Android Widgets
最后更新:2022年6月22日

千古八方

物格而后知至,知至而后意诚,意诚而后心正

点赞
< 上一篇
下一篇 >

文章评论

  • Avatar photo
    disco

    有demo链接吗?贝塞尔曲线这个关键类没有写出来~

    2022年12月5日
    回复
    • Avatar photo
      千古八方

      @disco 贝塞尔这个类在这里 https://gitee.com/aityu/AndroidUtils/blob/master/androidutils/src/main/java-utils/org/tcshare/widgets/BezierEvaluator.java

      完整的demo及库 在这里 https://gitee.com/aityu/AndroidUtils

      2022年12月15日
      回复
  • razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
    取消回复

    千古八方

    物格而后知至,知至而后意诚,意诚而后心正

    最新 热点 随机
    最新 热点 随机
    火狐下远离内容农场的方法! 中国城市生活成本查询 Debian11 系统备份与灾备盘制作 Debian logwatch 自定义配置文件,根据规则忽略掉某些日志内容 Ampache 5.0 报错 The root Ampache folder has changed to ./public socat 正反向传输文件
    自建NAS,如何备份数据? 自建NAS,是否需要RAID磁盘阵列? 自建 NAS,系统安全防护配置(二)扫描探测篇 自建NAS,选机械硬盘还是固态硬盘? Haproxy搭配NPS内网穿透,获取客户端的源IP 自建NAS,动态IPv6,DDNS解决方案
    分类
    • Android
    • Linux
    • NAS
    • Spring Boot
    • Windows
    • 内网穿透
    • 回忆
    • 基于NEAT的瞎几把寻思算法笔记
    • 散篇
    • 普通人的致富之路在哪里
    • 服务器
    • 未分类
    • 私有云
    标签聚合
    服务器 安全维护 Android Widgets 内网穿透 ubuntu NAS Android Utils Android系统编译 linux 我开源的APP Spring Boot Windows 散篇 私有云 Nextcloud TrueNAS 群晖NAS NPS
    工具/友链

    资源导航 – NAS、私有云存储
    在线工具 – MD5、SHA、BASE64、URL编解码
    在线工具 – 贷款计算器
    在线工具 – IPv4/IPv6地址查询

    千古八方 在 B站

    千古八方 在 知乎

    我的微信小程序:
    微信小程序-城市生活成本查询
    城市生活成本查询
    库房物资流水记账簿
    最近评论
    Avatar photo
    千古八方 发布于 1 个月前(12月28日) 重置命令是Nextcloud提供的,和哪个平台没关系。 如果报错,一般是路径或权限的问题导致的。 ...
    Avatar photo
    aron 发布于 1 个月前(12月24日) 博主你好 我的nextcloud管理员密码过期了 用了occ命令重置但是报错,能请教下是什么原因...
    Avatar photo
    千古八方 发布于 2 个月前(12月15日) 贝塞尔这个类在这里 https://gitee.com/aityu/AndroidUtils/blo...
    Avatar photo
    disco 发布于 2 个月前(12月05日) 有demo链接吗?贝塞尔曲线这个关键类没有写出来~
    Avatar photo
    HJM 发布于 3 个月前(10月20日) 必须点赞!很有效!

    COPYRIGHT © 2022 千古八方的博客. ALL RIGHTS RESERVED.

    京ICP备14020471号