直播涂鸦礼物

展示一波

show.gif

gif链接:https://upload-images.jianshu.io/upload_images/7588686-58365907312b7d23.gif?imageMogr2/auto-orient/strip

绘制需求描述

  • 首先选中某个涂鸦礼物后,进入涂鸦礼物绘制模式,可以在屏幕区域内自由的绘制,手指移动的时选中的礼物图片需要摆放在移动的轨迹上。
  • 礼物图片以正圆的形式紧密排布,但是当前正圆不能和上一个正圆有相交重叠。
  • 涂鸦礼物可以支持切换,切换为另一种礼物后,手指移动绘制的当前选中的礼物图片。
  • 支持撤销当前轨迹绘制的礼物,支持清空所有绘制的礼物,关闭涂鸦礼物绘制模式。


    diy_示意图.png

方案选择

  • 首先想到的是自定义一个view,重写onTouchEvent方法,在该方法中获取用户手指移动的坐标点B,已知上一个礼物的坐标A(手指down下去的坐标就是第一个礼物的坐标),求出A、B两点之间的间距线段AB,此时可以计算出线段AB能放几个礼物正圆(正圆的直径取礼物图片宽或高的小值),在求出每个正圆的圆心坐标,根据这些坐标就能绘制出相应的礼物图片。


    坐标计算.png
  • 另外由于我们的礼物图片是网络图片,所以势必需要下载,所以我们可以用LruCache来缓存下载好的bitmap,还有就是由于用户手指一放上去移动,我们的view就需要把图片绘制上去,但是此时可能我们的图片还未下载完成,所以我们可以参考imageView设置一张占位图,待图片下载成功后把占位图替换即可。

编写对应的实现代码

    /**
     * 礼物信息实体类
     */
    public static class GiftInfo {
        public String imageUrl;
        public int giftId;
    }

    /**
     * 每一条手势轨迹实体类
     */
    public static class PosWrap {
        //当前轨迹的礼物bitmap对象
        private Bitmap bitmap;
        //当前轨迹的礼物信息
        public GiftInfo giftInfo;
        //当前轨迹的所有礼物的位置区域
        private List<RectF> dstList = new ArrayList<>();

        //当前轨迹的所有礼物的原始位置区域(做缩放动画时候需要使用)
        private List<RectF> originDstList;

        public int getGiftSize() {
            return dstList.size();
        }
    }

public class GiftDiyView extends View {

    private static final String TAG = "PathGiftView";
    //记录按下去的点x轴坐标
    private float initialTouchX;
    //记录按下去的点y轴坐标
    private float initialTouchY;
    //滑动阀值,手指移动的距离大于该值便是滑动
    private final int touchSlop;
    //发送使用的画笔
    private Paint sendPaint;
    //发送的数据
    private SparseArray<PosWrap> sendDataArray;
    //序号,用来标识一次touch事件
    private int sequenceNumber;
    //图片的内存缓存
    private final LruCache<String, Bitmap> lruCache;
    //占位图
    private Bitmap placeholder;
    //当前发送方绘制时的图片
    private Bitmap currentBitmap;
    //当前发送方绘制时的礼物信息
    private GiftInfo currentGift;
    //当前正在下载图片的集合
    private Map<String, Target<Bitmap>> targetMap;
    //强制指定的图片的宽度
    private int imageWidth;
    //强制指定的图片的宽度
    private int imageHeight;

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

    public GiftDiyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GiftDiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        sendPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        sendDataArray = new SparseArray<>();

        targetMap = new HashMap<>();
        imageWidth = dp2px(52);
        imageHeight = dp2px(40);
        placeholder = ImageUtils.getBitmap(R.drawable.ic_gift_default, imageWidth, imageHeight);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        long max = Runtime.getRuntime().maxMemory();
        int cacheSize = (int) (max / 8);
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
    }

 /**
     * 设置当前绘制的礼物
     */
    public void setGiftInfo(int giftId, @NonNull String imageUrl) {
        if (currentGift != null && giftId == currentGift.giftId) {
            return;
        }
        currentGift = new GiftInfo();
        currentGift.giftId = giftId;
        currentGift.imageUrl = imageUrl;
        Bitmap bitmap;
        currentBitmap = (bitmap = lruCache.get(imageUrl)) == null ? placeholder : bitmap;
        if (bitmap == null) {
            loadBitmap(imageUrl);
        }
    }

/**
     * 加载图片
     */
    private void loadBitmap(@NonNull final String imageUrl) {
        if (targetMap.containsKey(imageUrl)) {
            //该图片正在加载
            return;
        }
        targetMap.put(imageUrl, GlideApp.with(this)
                .asBitmap()
                .load(imageUrl)
                .diskCacheStrategy(DiskCacheStrategy.DATA)
                .override(imageWidth, imageHeight)
                .into(new SimpleTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                        targetMap.remove(imageUrl);
                        lruCache.put(imageUrl, resource);
                        //通知图片加载成功
                        notifyBitmapUpdate(imageUrl);
                        loggerD("onResourceReady: 图片加载成功 imageUrl: " + imageUrl);
                    }

                    @Override
                    public void onLoadFailed(@Nullable Drawable errorDrawable) {
                        super.onLoadFailed(errorDrawable);
                        targetMap.remove(imageUrl);
                        loggerE("onLoadFailed: 图片加载失败");
                    }
                }));
    }

    /**
     * 图片加载成功,需要从placeholder切换为正确的图片
     */
    private void notifyBitmapUpdate(@NonNull String imageUrl) {
        Bitmap bitmap = Objects.requireNonNull(lruCache.get(imageUrl), "请检查代码逻辑,通知bitmap更新中不允许bitmap为null");

        boolean invalidate = false;
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            if (updateBitmap(posWrap, imageUrl, bitmap)) {
                invalidate = true;
            }
        }

        if (currentGift != null && imageUrl.equals(currentGift.imageUrl)) {
            currentBitmap = bitmap;
        }
        loggerD("notifyBitmapUpdate  invalidate: " + invalidate);
        if (invalidate) {
            invalidate();
        }
    }

    /**
     * 判断该图片是否需要更新
     */
    private boolean updateBitmap(@NonNull PosWrap posWrap, @NonNull String imageUrl, @NonNull Bitmap bitmap) {
        if (posWrap.giftInfo.imageUrl.equals(imageUrl) && posWrap.bitmap == placeholder) {
            posWrap.bitmap = bitmap;
            return true;
        }
        return false;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (currentGift == null) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录down的坐标
                initialTouchX = getValidX(event);
                initialTouchY = getValidY(event);

                //序号++
                sequenceNumber++;
                initPosWrap(initialTouchX, initialTouchY);
                break;
            case MotionEvent.ACTION_MOVE:
                float x = getValidX(event);
                float y = getValidY(event);
                float dx = x - initialTouchX;
                float dy = y - initialTouchY;
                if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
                    if (canDraw() && addPosByCircle(x, y)) {
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    /**
     * x轴坐标边界修正
     */
    private float getValidX(@NonNull MotionEvent event) {
        float x = event.getX();
        int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
        float min, max;
        if (x < (min = value)) {
            return min;
        } else if (x > (max = getWidth() - value)) {
            return max;
        } else {
            return x;
        }
    }

    /**
     * y轴坐标边界修正
     */
    private float getValidY(@NonNull MotionEvent event) {
        float y = event.getY();
        int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
        float min, max;
        if (y < (min = value)) {
            return min;
        } else if (y > (max = getHeight() - value)) {
            return max;
        } else {
            return y;
        }
    }

/**
     * 通知当前礼物有变化
     */
    private void notifyGiftCountChange() {
        if (giftDiyListener != null) {
            giftDiyListener.onGiftChange(getAllGiftCount());
        }
    }

    /**
     * 判断是否能绘制礼物
     */
    private boolean canDraw() {
        return giftDiyListener == null || giftDiyListener.canDraw(currentGift.giftId);
    }

    /**
     * 添加当前轨迹的第一个点
     */
    private void initPosWrap(float x, float y) {
        if (sendDataArray.get(sequenceNumber) != null) {
            throw new UnsupportedOperationException("posWrapArray中不应该有当前sequenceNumber:" + sequenceNumber);
        }
        if (canDraw()) {
            //初始化当前轨迹的礼物信息对象
            PosWrap posWrap = new PosWrap();
            posWrap.bitmap = currentBitmap;
            posWrap.giftInfo = currentGift;
            //添加当前轨迹的第一个点的范围
            posWrap.dstList.add(getDrawDstByPos(x, y));
            sendDataArray.put(sequenceNumber, posWrap);
            invalidate();
            notifyGiftCountChange();
        }
    }

    private RectF getDrawDstByPos(float x, float y) {
        return getDstByPos(currentBitmap, x, y);
    }

    /**
     * 根据图片的大小和坐标点计算出礼物的显示范围
     */
    private RectF getDstByPos(Bitmap bitmap, float x, float y) {
        float left = x - (bitmap.getWidth() >> 1);
        float top = y - (bitmap.getHeight() >> 1);
        float right = left + bitmap.getWidth();
        float bottom = top + bitmap.getHeight();
        return new RectF(left, top, right, bottom);
    }

    /**
     * 缩放成正方形
     */
    private RectF zoomSquare(@NonNull RectF src) {
        float sub = src.width() - src.height();
        if (sub > 0) {
            return new RectF(src.left + sub / 2, src.top, src.right - sub / 2, src.bottom);
        } else {
            return new RectF(src.left, src.top + Math.abs(sub) / 2, src.right, src.bottom - Math.abs(sub) / 2);
        }
    }

    /**
     * 根据正方形范围判断该坐标是否在圆内
     */
    private boolean inCircle(@NonNull RectF square, float x, float y) {
        float dx = Math.abs(square.centerX() - x);
        float dy = Math.abs(square.centerY() - y);
        return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) < square.width() / 2;
    }

    private boolean addPosByCircle(float endX, float endY) {
        //获取当前轨迹信息
        PosWrap posWrap = sendDataArray.get(sequenceNumber);
        //根据上一个点缩放出一个正方形
        RectF square = zoomSquare(posWrap.dstList.get(posWrap.dstList.size() - 1));
        //根据该正方形判断当前move的坐标是否在圆的范围内
        if (inCircle(square, endX, endY)) {
            return false;
        }

        /*
         * 以下根据上一个的中心点做出的圆的圆心坐标,做一条线段到当前坐标
         * 然后根据该线段的长度,计算可以放的下多少个圆,并把每个圆的圆心坐标计算出来,
         * 就得出了每个礼物的坐标点
         */
        float startX = square.centerX();
        float startY = square.centerY();
        float dx = endX - startX;
        float dy = endY - startY;

        int numX = dx > 0 ? 1 : -1;
        int numY = dy > 0 ? 1 : -1;

        float absDx = Math.abs(dx);
        float absDy = Math.abs(dy);

        int size = (int) (Math.sqrt(Math.pow(absDx, 2) + Math.pow(absDy, 2)) / square.width());
        if (size <= 0) {
            return false;
        }

        float fx, fy;
        if (absDx == 0) {
            fx = 0;
            fy = 1;
        } else if (absDy == 0) {
            fx = 1;
            fy = 0;
        } else {
            double degree = Math.atan(absDx / absDy);
            fx = (float) Math.sin(degree);
            fy = (float) Math.cos(degree);
        }

        boolean invalid = false;
        for (int i = 1; i <= size; i++) {
            float x = startX + fx * square.width() * i * numX;
            float y = startY + fy * square.width() * i * numY;
            if (!canDraw()) {
                break;
            }
            invalid = true;
            posWrap.dstList.add(getDrawDstByPos(x, y));
        }
        if (invalid) {
            notifyGiftCountChange();
        }
        return invalid;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        /*
         * 绘制手势轨迹
         */
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            Bitmap bitmap = posWrap.bitmap;
            for (RectF dst : posWrap.dstList) {
                canvas.drawBitmap(bitmap, null, dst, sendPaint);
            }
        }
    }
}

其实以上代码就是做了方案选择当中的事情,接下来还有收到他人的涂鸦礼物,需要动画展示,动画要求如下:
按照用户绘制顺序逐个出现
缩放出现0%-100% ,用时200ms,
每张礼物图片出现间隔60ms
礼物至多绘制100个,至少绘制10个,所以动画最长时间为6200ms,最短时间为800ms
全部礼物图片出现后,停留1500ms后,缩放100%-200%并渐隐100%-0%消失,用时300ms。
完整代码如下:

public class GiftDiyView extends View {

    private static final String TAG = "PathGiftView";
    //记录按下去的点x轴坐标
    private float initialTouchX;
    //记录按下去的点y轴坐标
    private float initialTouchY;
    //滑动阀值,手指移动的距离大于该值便是滑动
    private final int touchSlop;
    //发送使用的画笔
    private Paint sendPaint;
    //接收使用的画笔
    private Paint receivePaint;
    //发送的数据
    private SparseArray<PosWrap> sendDataArray;
    //序号,用来标识一次touch事件
    private int sequenceNumber;
    //图片的内存缓存
    private final LruCache<String, Bitmap> lruCache;
    //占位图
    private Bitmap placeholder;
    //当前发送方绘制时的图片
    private Bitmap currentBitmap;
    //当前发送方绘制时的礼物信息
    private GiftInfo currentGift;
    //当前正在下载图片的集合
    private Map<String, Target<Bitmap>> targetMap;
    //强制指定的图片的宽度
    private int imageWidth;
    //强制指定的图片的宽度
    private int imageHeight;
    //接收方的数据
    private List<PosWrap> receiveDataList;
    //接收到数据后的动画是否正在执行
    private boolean isAnimatorRunning;
    //接收方的离场动画
    private ValueAnimator exitAnimator;
    //接收方的进场动画集合
    private List<ValueAnimator> enterScaleAnimators;
    //礼物监听器
    private GiftDiyListener giftDiyListener;

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

    public GiftDiyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GiftDiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        sendPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        receivePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

        sendDataArray = new SparseArray<>();

        targetMap = new HashMap<>();
        imageWidth = dp2px(52);
        imageHeight = dp2px(40);
        placeholder = ImageUtils.getBitmap(R.drawable.ic_gift_default, imageWidth, imageHeight);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        long max = Runtime.getRuntime().maxMemory();
        int cacheSize = (int) (max / 8);
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };

        receiveDataList = new ArrayList<>();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width, height;
        width = widthMode == MeasureSpec.EXACTLY ? widthSize : ViewGroup.LayoutParams.MATCH_PARENT;
        height = heightMode == MeasureSpec.EXACTLY ? heightSize : ViewGroup.LayoutParams.MATCH_PARENT;
        setMeasuredDimension(width, height);
    }

    /**
     * @return 当前绘制的礼物id
     */
    public int getCurrentGiftId() {
        return currentGift == null ? 0 : currentGift.giftId;
    }

    /**
     * 设置当前绘制的礼物
     */
    public void setGiftInfo(int giftId, @NonNull String imageUrl) {
        if (currentGift != null && giftId == currentGift.giftId) {
            return;
        }
        currentGift = new GiftInfo();
        currentGift.giftId = giftId;
        currentGift.imageUrl = imageUrl;
        Bitmap bitmap;
        currentBitmap = (bitmap = lruCache.get(imageUrl)) == null ? placeholder : bitmap;
        if (bitmap == null) {
            loadBitmap(imageUrl);
        }
    }

    /**
     * 删除当前绘制的礼物
     */
    public void deleteCurrentGift() {
        int size = sendDataArray.size();
        if (size > 0) {
            sendDataArray.removeAt(size - 1);
            invalidate();
            notifyGiftCountChange();
        }
    }

    /**
     * 清空绘制的礼物
     */
    public void clearMoveGift() {
        if (sendDataArray.size() > 0) {
            clearPosWrapArray();
            invalidate();
            notifyGiftCountChange();
        }
    }

    /**
     * 释放绘制方
     */
    private void releaseTouchDraw() {
        clearPosWrapArray();
        currentGift = null;
        currentBitmap = null;
    }

    /**
     * 释放绘制方后重新回调onDraw
     */
    public void closeTouchDraw() {
        releaseTouchDraw();
        invalidate();
    }

    /**
     * 释放所有
     */
    public void release() {
        releaseTouchDraw();
        clearExitAnimator();
        handler.removeCallbacksAndMessages(null);
        clearEnterScaleAnimators();
        isAnimatorRunning = false;
        receiveDataList.clear();
        clearTarget();
        lruCache.evictAll();
        giftDiyListener = null;
        placeholder = null;
    }

    /**
     * 获取绘制的所有礼物总数
     */
    public int getTotalGiftCount() {
        int totalCount = 0;
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            totalCount += sendDataArray.valueAt(i).getGiftSize();
        }
        return totalCount;
    }

    /**
     * 获取每一种礼物的绘制数量
     */
    public SparseIntArray getAllGiftCount() {
        SparseIntArray giftCounts = new SparseIntArray();
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            int newCount = posWrap.getGiftSize() + giftCounts.get(posWrap.giftInfo.giftId);
            giftCounts.put(posWrap.giftInfo.giftId, newCount);
        }
        return giftCounts;
    }

    /**
     * 获取某个礼物的绘制数量
     */
    public int getGiftCountByGiftId(long giftId) {
        int count = 0;
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            if (posWrap.giftInfo.giftId == giftId) {
                count += posWrap.getGiftSize();
            }
        }
        return count;
    }

    /**
     * 获取绘制的礼物坐标信息等
     */
    public List<PosWrap> getPosData() {
        int size = sendDataArray.size();
        List<PosWrap> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            list.add(posWrap);
        }
        return list;
    }

    /**
     * 接收到他人的diy礼物
     */
    public void receiveDiyGift(@NonNull MessageData data) {
        if (isAnimatorRunning || data.list.isEmpty() || data.list.get(0).posList.isEmpty() || !receiveDataList.isEmpty()) {
            return;
        }
        isAnimatorRunning = true;
        sendNextMessage(data, 0L);
    }

    public void setGiftDiyListener(@Nullable GiftDiyListener giftDiyListener) {
        this.giftDiyListener = giftDiyListener;
    }

    /**
     * 清空发送方的数据
     */
    private void clearPosWrapArray() {
        sequenceNumber = 0;
        sendDataArray.clear();
    }

    /**
     * 取消接收方的进场动画
     */
    private void clearEnterScaleAnimators() {
        if (enterScaleAnimators != null) {
            Iterator<ValueAnimator> iterator = enterScaleAnimators.iterator();
            while (iterator.hasNext()) {
                ValueAnimator animator = iterator.next();
                animator.removeAllUpdateListeners();
                animator.removeAllListeners();
                animator.cancel();
                iterator.remove();
            }
            enterScaleAnimators = null;
        }
    }

    /**
     * 取消接收方的离场动画
     */
    private void clearExitAnimator() {
        if (exitAnimator != null) {
            exitAnimator.removeAllUpdateListeners();
            exitAnimator.removeAllListeners();
            exitAnimator.cancel();
            exitAnimator = null;
        }
    }

    /**
     * 接收方展示下一个礼物图片的消息
     */
    private static final int NEXT_MSG = 1;
    /**
     * 接收方所有礼物退场的消息
     */
    private static final int EXIT_MSG = 2;

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (msg.what == NEXT_MSG) {
                MessageData data = (MessageData) msg.obj;
                //当前轨迹要展示的礼物信息
                ReceivePosWrap wrap = data.list.get(data.index);
                PosWrap receiveData;
                if (receiveDataList.size() <= data.index) {
                    receiveData = new PosWrap();
                    receiveData.giftInfo = wrap.giftInfo;
                    //该集合用来保存礼物的原始位置和范围,退出动画的时候需要用到原始的范围
                    receiveData.originDstList = new ArrayList<>();
                    Bitmap bitmap;
                    receiveData.bitmap = (bitmap = lruCache.get(receiveData.giftInfo.imageUrl)) == null ? placeholder : bitmap;
                    if (bitmap == null) {
                        loadBitmap(receiveData.giftInfo.imageUrl);
                    }
                    receiveDataList.add(receiveData);
                } else {
                    receiveData = receiveDataList.get(data.index);
                }
                //坐标点等比例转换
                float[] pos = coverPos(wrap.posList.get(data.dstIndex), data.width, data.height);
                //根据坐标点确认礼物的显示范围
                RectF dst = getDstByPos(receiveData.bitmap, pos[0], pos[1]);

                boolean isLast = false;
                //获取当前轨迹的下个礼物
                data.dstIndex++;
                if (data.dstIndex >= wrap.posList.size()) {
                    //当前轨迹已没有礼物
                    data.dstIndex = 0;
                    //获取下一个轨迹
                    data.index++;
                    if (data.index >= data.list.size()) {
                        //没有下一个轨迹,证明没用任何礼物了
                        isLast = true;
                    }
                }
                //添加该礼物的范围
                receiveData.dstList.add(dst);
                //添加该礼物的原始范围
                receiveData.originDstList.add(new RectF(dst));
                //开启该礼物的进场动画
                startEnterScaleAnimator(dst, isLast);
                if (isLast) {
                    //结束
                    loggerD("结束了");
                } else {
                    //60ms后进行下一个礼物的进场
                    sendNextMessage(data, 60L);
                }
            } else if (msg.what == EXIT_MSG) {
                //所有的礼物一起执行离场动画
                startExitAnimator();
            }
        }
    };

    private Activity findActivity(Context context) {
        if (context instanceof Activity) {
            return (Activity) context;
        } else if (context instanceof ContextWrapper) {
            return findActivity(((ContextWrapper) context).getBaseContext());
        } else {
            return null;
        }
    }

    private int getValidScreenHeight() {
        Activity activity = findActivity(getContext());
        int navBarHeight;
        if (activity == null) {
            navBarHeight = 0;
        } else {
            navBarHeight = BarUtils.isNavBarVisible(activity) ? BarUtils.getNavBarHeight() : 0;
        }
        return ScreenUtils.getScreenHeight() - navBarHeight;
    }

    public int[] getViewSize() {
        int w = getWidth() == 0 ? ScreenUtils.getScreenWidth() : getWidth();
        int h = getHeight() == 0 ? getValidScreenHeight() : getHeight();
        return new int[]{w, h};
    }

    /**
     * 坐标点比例转换
     * 根据发送方的宽高结合接收方自己的宽高对发送方发来的坐标点进行等比例转换
     */
    private float[] coverPos(float[] pos, int width, int height) {
        int[] size = getViewSize();
        float x = pos[0] * size[0] / width;
        float y = pos[1] * size[1] / height;
        return new float[]{x, y};
    }

    private void sendNextMessage(@NonNull MessageData data, long delayMillis) {
        Message message = handler.obtainMessage(NEXT_MSG);
        message.obj = data;
        handler.sendMessageDelayed(message, delayMillis);
    }

    /**
     * 每个礼物执行的进场动画(缩放出现0%-100%)
     * 每个礼物出现间隔60ms
     */
    private void startEnterScaleAnimator(final RectF dst, final boolean isLast) {
        if (enterScaleAnimators == null) {
            enterScaleAnimators = new ArrayList<>();
        }
        final float left = dst.left;
        final float top = dst.top;
        final float right = dst.right;
        final float bottom = dst.bottom;
        final float centerX = dst.centerX();
        final float centerY = dst.centerY();

        ValueAnimator animator;
        enterScaleAnimators.add(animator = ValueAnimator.ofFloat(0f, 1f).setDuration(200L));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float) animation.getAnimatedValue();
                dst.set(animatedValue * left + (1 - animatedValue) * centerX,
                        animatedValue * top + (1 - animatedValue) * centerY,
                        animatedValue * right + (1 - animatedValue) * centerX,
                        animatedValue * bottom + (1 - animatedValue) * centerY);
                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                enterScaleAnimators.remove(((ValueAnimator) animation));
                loggerD("enterScaleAnimators.size: " + enterScaleAnimators.size());
                if (isLast) {
                    //若是最后一个礼物,停留1500ms后需执行离场动画
                    handler.sendEmptyMessageDelayed(EXIT_MSG, 1500L);
                }
            }
        });
        animator.start();
    }

    /**
     * 所有的礼物一起执行离场动画(缩放100%-200%并渐隐100%-0%消失)
     */
    private void startExitAnimator() {
        exitAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(300L);
        exitAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float) animation.getAnimatedValue();
                for (PosWrap receiveData : receiveDataList) {
                    for (int i = 0, size = receiveData.dstList.size(); i < size; i++) {
                        RectF originDst = receiveData.originDstList.get(i);
                        RectF dst = receiveData.dstList.get(i);
                        float targetLeft = originDst.left - originDst.width() / 2;
                        float targetTop = originDst.top - originDst.height() / 2;
                        float targetRight = originDst.right + originDst.width() / 2;
                        float targetBottom = originDst.bottom + originDst.height() / 2;
                        dst.set(animatedValue * targetLeft + (1 - animatedValue) * originDst.left,
                                animatedValue * targetTop + (1 - animatedValue) * originDst.top,
                                animatedValue * targetRight + (1 - animatedValue) * originDst.right,
                                animatedValue * targetBottom + (1 - animatedValue) * originDst.bottom);
                    }
                }
                receivePaint.setAlpha((int) ((1 - animatedValue) * 255));
                invalidate();
            }
        });
        exitAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                exitAnimator = null;
                receiveDataList.clear();
                receivePaint.setAlpha(255);
                isAnimatorRunning = false;
                //通知接收方礼物展示结束
                if (giftDiyListener != null) {
                    giftDiyListener.receiveDiyGiftEnd();
                }
            }
        });
        exitAnimator.start();
    }

    /**
     * 加载图片
     */
    private void loadBitmap(@NonNull final String imageUrl) {
        if (targetMap.containsKey(imageUrl)) {
            //该图片正在加载
            return;
        }
        targetMap.put(imageUrl, GlideApp.with(this)
                .asBitmap()
                .load(imageUrl)
                .diskCacheStrategy(DiskCacheStrategy.DATA)
                .override(imageWidth, imageHeight)
                .into(new SimpleTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                        targetMap.remove(imageUrl);
                        lruCache.put(imageUrl, resource);
                        //通知图片加载成功
                        notifyBitmapUpdate(imageUrl);
                        loggerD("onResourceReady: 图片加载成功 imageUrl: " + imageUrl);
                    }

                    @Override
                    public void onLoadFailed(@Nullable Drawable errorDrawable) {
                        super.onLoadFailed(errorDrawable);
                        targetMap.remove(imageUrl);
                        loggerE("onLoadFailed: 图片加载失败");
                    }
                }));
    }

    /**
     * 图片加载成功,需要从placeholder切换为正确的图片
     */
    private void notifyBitmapUpdate(@NonNull String imageUrl) {
        Bitmap bitmap = Objects.requireNonNull(lruCache.get(imageUrl), "请检查代码逻辑,通知bitmap更新中不允许bitmap为null");

        boolean invalidate = false;
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            if (updateBitmap(posWrap, imageUrl, bitmap)) {
                invalidate = true;
            }
        }

        for (PosWrap posWrap : receiveDataList) {
            if (updateBitmap(posWrap, imageUrl, bitmap)) {
                invalidate = true;
            }
        }
        if (currentGift != null && imageUrl.equals(currentGift.imageUrl)) {
            currentBitmap = bitmap;
        }
        loggerD("notifyBitmapUpdate  invalidate: " + invalidate);
        if (invalidate) {
            invalidate();
        }
    }

    /**
     * 判断该图片是否需要更新
     */
    private boolean updateBitmap(@NonNull PosWrap posWrap, @NonNull String imageUrl, @NonNull Bitmap bitmap) {
        if (posWrap.giftInfo.imageUrl.equals(imageUrl) && posWrap.bitmap == placeholder) {
            posWrap.bitmap = bitmap;
            return true;
        }
        return false;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (currentGift == null) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录down的坐标
                initialTouchX = getValidX(event);
                initialTouchY = getValidY(event);

                //序号++
                sequenceNumber++;
                initPosWrap(initialTouchX, initialTouchY);
                break;
            case MotionEvent.ACTION_MOVE:
                float x = getValidX(event);
                float y = getValidY(event);
                float dx = x - initialTouchX;
                float dy = y - initialTouchY;
                if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
                    if (canDraw() && addPosByCircle(x, y)) {
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    /**
     * x轴坐标边界修正
     */
    private float getValidX(@NonNull MotionEvent event) {
        float x = event.getX();
        int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
        float min, max;
        if (x < (min = value)) {
            return min;
        } else if (x > (max = getWidth() - value)) {
            return max;
        } else {
            return x;
        }
    }

    /**
     * y轴坐标边界修正
     */
    private float getValidY(@NonNull MotionEvent event) {
        float y = event.getY();
        int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
        float min, max;
        if (y < (min = value)) {
            return min;
        } else if (y > (max = getHeight() - value)) {
            return max;
        } else {
            return y;
        }
    }

    /**
     * 通知当前礼物有变化
     */
    private void notifyGiftCountChange() {
        if (giftDiyListener != null) {
            giftDiyListener.onGiftChange(getAllGiftCount());
        }
    }

    /**
     * 判断是否能绘制礼物
     */
    private boolean canDraw() {
        return giftDiyListener == null || giftDiyListener.canDraw(currentGift.giftId);
    }

    /**
     * 添加当前轨迹的第一个点
     */
    private void initPosWrap(float x, float y) {
        if (sendDataArray.get(sequenceNumber) != null) {
            throw new UnsupportedOperationException("posWrapArray中不应该有当前sequenceNumber:" + sequenceNumber);
        }
        if (canDraw()) {
            //初始化当前轨迹的礼物信息对象
            PosWrap posWrap = new PosWrap();
            posWrap.bitmap = currentBitmap;
            posWrap.giftInfo = currentGift;
            //添加当前轨迹的第一个点的范围
            posWrap.dstList.add(getDrawDstByPos(x, y));
            sendDataArray.put(sequenceNumber, posWrap);
            invalidate();
            notifyGiftCountChange();
        }
    }

    private RectF getDrawDstByPos(float x, float y) {
        return getDstByPos(currentBitmap, x, y);
    }

    /**
     * 根据图片的大小和坐标点计算出礼物的显示范围
     */
    private RectF getDstByPos(Bitmap bitmap, float x, float y) {
        float left = x - (bitmap.getWidth() >> 1);
        float top = y - (bitmap.getHeight() >> 1);
        float right = left + bitmap.getWidth();
        float bottom = top + bitmap.getHeight();
        return new RectF(left, top, right, bottom);
    }

    /**
     * 缩放成正方形
     */
    private RectF zoomSquare(@NonNull RectF src) {
        float sub = src.width() - src.height();
        if (sub > 0) {
            return new RectF(src.left + sub / 2, src.top, src.right - sub / 2, src.bottom);
        } else {
            return new RectF(src.left, src.top + Math.abs(sub) / 2, src.right, src.bottom - Math.abs(sub) / 2);
        }
    }

    /**
     * 根据正方形范围判断该坐标是否在圆内
     */
    private boolean inCircle(@NonNull RectF square, float x, float y) {
        float dx = Math.abs(square.centerX() - x);
        float dy = Math.abs(square.centerY() - y);
        return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) < square.width() / 2;
    }

    private boolean addPosByCircle(float endX, float endY) {
        //获取当前轨迹信息
        PosWrap posWrap = sendDataArray.get(sequenceNumber);
        //根据上一个点缩放出一个正方形
        RectF square = zoomSquare(posWrap.dstList.get(posWrap.dstList.size() - 1));
        //根据该正方形判断当前move的坐标是否在圆的范围内
        if (inCircle(square, endX, endY)) {
            return false;
        }

        /*
         * 以下根据上一个的中心点做出的圆的圆心坐标,做一条线段到当前坐标
         * 然后根据该线段的长度,计算可以放的下多少个圆,并把每个圆的圆心坐标计算出来,
         * 就得出了每个礼物的坐标点
         */
        float startX = square.centerX();
        float startY = square.centerY();
        float dx = endX - startX;
        float dy = endY - startY;

        int numX = dx > 0 ? 1 : -1;
        int numY = dy > 0 ? 1 : -1;

        float absDx = Math.abs(dx);
        float absDy = Math.abs(dy);

        int size = (int) (Math.sqrt(Math.pow(absDx, 2) + Math.pow(absDy, 2)) / square.width());
        if (size <= 0) {
            return false;
        }

        float fx, fy;
        if (absDx == 0) {
            fx = 0;
            fy = 1;
        } else if (absDy == 0) {
            fx = 1;
            fy = 0;
        } else {
            double degree = Math.atan(absDx / absDy);
            fx = (float) Math.sin(degree);
            fy = (float) Math.cos(degree);
        }

        boolean invalid = false;
        for (int i = 1; i <= size; i++) {
            float x = startX + fx * square.width() * i * numX;
            float y = startY + fy * square.width() * i * numY;
            if (!canDraw()) {
                break;
            }
            invalid = true;
            posWrap.dstList.add(getDrawDstByPos(x, y));
        }
        if (invalid) {
            notifyGiftCountChange();
        }
        return invalid;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        /*
         * 绘制接收方的礼物
         */
        for (PosWrap receiveData : receiveDataList) {
            Bitmap bitmap = receiveData.bitmap;
            for (RectF dst : receiveData.dstList) {
                canvas.drawBitmap(bitmap, null, dst, receivePaint);
            }
        }

        /*
         * 绘制手势轨迹
         * 因SparseArray是按照key进行升序排序的,而我们的sequenceNumber只进行++处理,
         * 所以可以和ArrayList一样保证遍历的时候是按照添加时的顺序进行遍历
         */
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            PosWrap posWrap = sendDataArray.valueAt(i);
            Bitmap bitmap = posWrap.bitmap;
            for (RectF dst : posWrap.dstList) {
                canvas.drawBitmap(bitmap, null, dst, sendPaint);
            }
        }
    }

    /**
     * 取消加载图片的任务
     */
    private void clearTarget() {
        Iterator<Map.Entry<String, Target<Bitmap>>> iterator = targetMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Target<Bitmap>> entry = iterator.next();
            GlideApp.with(this).clear(entry.getValue());
            iterator.remove();
        }
    }

    private int dp2px(float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, getResources().getDisplayMetrics());
    }

    private static void loggerE(String message) {
        if (BaseApplication.isDebug()) {
            Log.e(TAG, message);
        }
    }

    private static void loggerD(String message) {
        if (BaseApplication.isDebug()) {
            Log.d(TAG, message);
        }
    }

    public interface GiftDiyListener {
        void onGiftChange(@NonNull SparseIntArray giftCounts);

        boolean canDraw(int giftId);

        void receiveDiyGiftEnd();
    }

    public static class PosWrap {
        private Bitmap bitmap;
        public GiftInfo giftInfo;
        private List<RectF> dstList = new ArrayList<>();

        private List<RectF> originDstList;

        public int getGiftSize() {
            return dstList.size();
        }

        public float[] covertPos() {
            int size = dstList.size();
            if (size == 0) {
                return null;
            }
            float[] pos = new float[size * 2];
            int index = 0;
            for (RectF rectF : dstList) {
                pos[index] = rectF.centerX();
                index++;
                pos[index] = rectF.centerY();
                index++;
            }
            return pos;
        }
    }

    public static class GiftInfo {
        public String imageUrl;
        public int giftId;
    }

    public static class MessageData {
        private int index;
        private int dstIndex;
        public List<ReceivePosWrap> list = new ArrayList<>();
        public int width;
        public int height;
    }

    public static class ReceivePosWrap {
        public GiftInfo giftInfo = new GiftInfo();
        public List<float[]> posList = new ArrayList<>();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • ios 绘画之涂鸦,贴图,马赛克,高斯笔涂鸦 https://github.com/wangjinshan/IJS...
    874b526fa570阅读 3,209评论 17 10
  • 前言: 现在直播APP中还有一种特别常用的礼物 —— 手绘礼物,接下来我们来谈谈如何实现这个好玩的自定义赠送礼...
    Bepawbikvy阅读 1,011评论 0 2
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,693评论 2 59
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,521评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,181评论 4 8