直播涂鴉禮物

展示一波

show.gif

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

繪制需求描述

  • 首先選中某個涂鴉禮物后,進(jìn)入涂鴉禮物繪制模式,可以在屏幕區(qū)域內(nèi)自由的繪制巧娱,手指移動的時選中的禮物圖片需要擺放在移動的軌跡上眨层。
  • 禮物圖片以正圓的形式緊密排布馒闷,但是當(dāng)前正圓不能和上一個正圓有相交重疊逛薇。
  • 涂鴉禮物可以支持切換,切換為另一種禮物后呢袱,手指移動繪制的當(dāng)前選中的禮物圖片。
  • 支持撤銷當(dāng)前軌跡繪制的禮物治专,支持清空所有繪制的禮物,關(guān)閉涂鴉禮物繪制模式喘批。


    diy_示意圖.png

方案選擇

  • 首先想到的是自定義一個view老速,重寫onTouchEvent方法额湘,在該方法中獲取用戶手指移動的坐標(biāo)點B卿吐,已知上一個禮物的坐標(biāo)A(手指down下去的坐標(biāo)就是第一個禮物的坐標(biāo)),求出A锋华、B兩點之間的間距線段AB嗡官,此時可以計算出線段AB能放幾個禮物正圓(正圓的直徑取禮物圖片寬或高的小值),在求出每個正圓的圓心坐標(biāo)毯焕,根據(jù)這些坐標(biāo)就能繪制出相應(yīng)的禮物圖片。


    坐標(biāo)計算.png
  • 另外由于我們的禮物圖片是網(wǎng)絡(luò)圖片纳猫,所以勢必需要下載婆咸,所以我們可以用LruCache來緩存下載好的bitmap,還有就是由于用戶手指一放上去移動芜辕,我們的view就需要把圖片繪制上去尚骄,但是此時可能我們的圖片還未下載完成,所以我們可以參考imageView設(shè)置一張占位圖侵续,待圖片下載成功后把占位圖替換即可倔丈。

編寫對應(yīng)的實現(xiàn)代碼

    /**
     * 禮物信息實體類
     */
    public static class GiftInfo {
        public String imageUrl;
        public int giftId;
    }

    /**
     * 每一條手勢軌跡實體類
     */
    public static class PosWrap {
        //當(dāng)前軌跡的禮物bitmap對象
        private Bitmap bitmap;
        //當(dāng)前軌跡的禮物信息
        public GiftInfo giftInfo;
        //當(dāng)前軌跡的所有禮物的位置區(qū)域
        private List<RectF> dstList = new ArrayList<>();

        //當(dāng)前軌跡的所有禮物的原始位置區(qū)域(做縮放動畫時候需要使用)
        private List<RectF> originDstList;

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

public class GiftDiyView extends View {

    private static final String TAG = "PathGiftView";
    //記錄按下去的點x軸坐標(biāo)
    private float initialTouchX;
    //記錄按下去的點y軸坐標(biāo)
    private float initialTouchY;
    //滑動閥值,手指移動的距離大于該值便是滑動
    private final int touchSlop;
    //發(fā)送使用的畫筆
    private Paint sendPaint;
    //發(fā)送的數(shù)據(jù)
    private SparseArray<PosWrap> sendDataArray;
    //序號,用來標(biāo)識一次touch事件
    private int sequenceNumber;
    //圖片的內(nèi)存緩存
    private final LruCache<String, Bitmap> lruCache;
    //占位圖
    private Bitmap placeholder;
    //當(dāng)前發(fā)送方繪制時的圖片
    private Bitmap currentBitmap;
    //當(dāng)前發(fā)送方繪制時的禮物信息
    private GiftInfo currentGift;
    //當(dāng)前正在下載圖片的集合
    private Map<String, Target<Bitmap>> targetMap;
    //強(qiáng)制指定的圖片的寬度
    private int imageWidth;
    //強(qiáng)制指定的圖片的寬度
    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();
            }
        };
    }

 /**
     * 設(shè)置當(dāng)前繪制的禮物
     */
    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的坐標(biāo)
                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軸坐標(biāo)邊界修正
     */
    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軸坐標(biāo)邊界修正
     */
    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;
        }
    }

/**
     * 通知當(dāng)前禮物有變化
     */
    private void notifyGiftCountChange() {
        if (giftDiyListener != null) {
            giftDiyListener.onGiftChange(getAllGiftCount());
        }
    }

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

    /**
     * 添加當(dāng)前軌跡的第一個點
     */
    private void initPosWrap(float x, float y) {
        if (sendDataArray.get(sequenceNumber) != null) {
            throw new UnsupportedOperationException("posWrapArray中不應(yīng)該有當(dāng)前sequenceNumber:" + sequenceNumber);
        }
        if (canDraw()) {
            //初始化當(dāng)前軌跡的禮物信息對象
            PosWrap posWrap = new PosWrap();
            posWrap.bitmap = currentBitmap;
            posWrap.giftInfo = currentGift;
            //添加當(dāng)前軌跡的第一個點的范圍
            posWrap.dstList.add(getDrawDstByPos(x, y));
            sendDataArray.put(sequenceNumber, posWrap);
            invalidate();
            notifyGiftCountChange();
        }
    }

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

    /**
     * 根據(jù)圖片的大小和坐標(biāo)點計算出禮物的顯示范圍
     */
    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);
        }
    }

    /**
     * 根據(jù)正方形范圍判斷該坐標(biāo)是否在圓內(nèi)
     */
    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) {
        //獲取當(dāng)前軌跡信息
        PosWrap posWrap = sendDataArray.get(sequenceNumber);
        //根據(jù)上一個點縮放出一個正方形
        RectF square = zoomSquare(posWrap.dstList.get(posWrap.dstList.size() - 1));
        //根據(jù)該正方形判斷當(dāng)前move的坐標(biāo)是否在圓的范圍內(nèi)
        if (inCircle(square, endX, endY)) {
            return false;
        }

        /*
         * 以下根據(jù)上一個的中心點做出的圓的圓心坐標(biāo),做一條線段到當(dāng)前坐標(biāo)
         * 然后根據(jù)該線段的長度,計算可以放的下多少個圓,并把每個圓的圓心坐標(biāo)計算出來,
         * 就得出了每個禮物的坐標(biāo)點
         */
        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);
            }
        }
    }
}

其實以上代碼就是做了方案選擇當(dāng)中的事情,接下來還有收到他人的涂鴉禮物状蜗,需要動畫展示需五,動畫要求如下:
按照用戶繪制順序逐個出現(xiàn)
縮放出現(xiàn)0%-100% ,用時200ms诗舰,
每張禮物圖片出現(xiàn)間隔60ms
禮物至多繪制100個警儒,至少繪制10個,所以動畫最長時間為6200ms眶根,最短時間為800ms
全部禮物圖片出現(xiàn)后蜀铲,停留1500ms后,縮放100%-200%并漸隱100%-0%消失属百,用時300ms记劝。
完整代碼如下:

public class GiftDiyView extends View {

    private static final String TAG = "PathGiftView";
    //記錄按下去的點x軸坐標(biāo)
    private float initialTouchX;
    //記錄按下去的點y軸坐標(biāo)
    private float initialTouchY;
    //滑動閥值,手指移動的距離大于該值便是滑動
    private final int touchSlop;
    //發(fā)送使用的畫筆
    private Paint sendPaint;
    //接收使用的畫筆
    private Paint receivePaint;
    //發(fā)送的數(shù)據(jù)
    private SparseArray<PosWrap> sendDataArray;
    //序號,用來標(biāo)識一次touch事件
    private int sequenceNumber;
    //圖片的內(nèi)存緩存
    private final LruCache<String, Bitmap> lruCache;
    //占位圖
    private Bitmap placeholder;
    //當(dāng)前發(fā)送方繪制時的圖片
    private Bitmap currentBitmap;
    //當(dāng)前發(fā)送方繪制時的禮物信息
    private GiftInfo currentGift;
    //當(dāng)前正在下載圖片的集合
    private Map<String, Target<Bitmap>> targetMap;
    //強(qiáng)制指定的圖片的寬度
    private int imageWidth;
    //強(qiáng)制指定的圖片的寬度
    private int imageHeight;
    //接收方的數(shù)據(jù)
    private List<PosWrap> receiveDataList;
    //接收到數(shù)據(jù)后的動畫是否正在執(zhí)行
    private boolean isAnimatorRunning;
    //接收方的離場動畫
    private ValueAnimator exitAnimator;
    //接收方的進(jìn)場動畫集合
    private List<ValueAnimator> enterScaleAnimators;
    //禮物監(jiān)聽器
    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 當(dāng)前繪制的禮物id
     */
    public int getCurrentGiftId() {
        return currentGift == null ? 0 : currentGift.giftId;
    }

    /**
     * 設(shè)置當(dāng)前繪制的禮物
     */
    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);
        }
    }

    /**
     * 刪除當(dāng)前繪制的禮物
     */
    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;
    }

    /**
     * 釋放繪制方后重新回調(diào)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;
    }

    /**
     * 獲取繪制的所有禮物總數(shù)
     */
    public int getTotalGiftCount() {
        int totalCount = 0;
        for (int i = 0, size = sendDataArray.size(); i < size; i++) {
            totalCount += sendDataArray.valueAt(i).getGiftSize();
        }
        return totalCount;
    }

    /**
     * 獲取每一種禮物的繪制數(shù)量
     */
    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;
    }

    /**
     * 獲取某個禮物的繪制數(shù)量
     */
    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;
    }

    /**
     * 獲取繪制的禮物坐標(biāo)信息等
     */
    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;
    }

    /**
     * 清空發(fā)送方的數(shù)據(jù)
     */
    private void clearPosWrapArray() {
        sequenceNumber = 0;
        sendDataArray.clear();
    }

    /**
     * 取消接收方的進(jìn)場動畫
     */
    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;
                //當(dāng)前軌跡要展示的禮物信息
                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);
                }
                //坐標(biāo)點等比例轉(zhuǎn)換
                float[] pos = coverPos(wrap.posList.get(data.dstIndex), data.width, data.height);
                //根據(jù)坐標(biāo)點確認(rèn)禮物的顯示范圍
                RectF dst = getDstByPos(receiveData.bitmap, pos[0], pos[1]);

                boolean isLast = false;
                //獲取當(dāng)前軌跡的下個禮物
                data.dstIndex++;
                if (data.dstIndex >= wrap.posList.size()) {
                    //當(dāng)前軌跡已沒有禮物
                    data.dstIndex = 0;
                    //獲取下一個軌跡
                    data.index++;
                    if (data.index >= data.list.size()) {
                        //沒有下一個軌跡,證明沒用任何禮物了
                        isLast = true;
                    }
                }
                //添加該禮物的范圍
                receiveData.dstList.add(dst);
                //添加該禮物的原始范圍
                receiveData.originDstList.add(new RectF(dst));
                //開啟該禮物的進(jìn)場動畫
                startEnterScaleAnimator(dst, isLast);
                if (isLast) {
                    //結(jié)束
                    loggerD("結(jié)束了");
                } else {
                    //60ms后進(jìn)行下一個禮物的進(jìn)場
                    sendNextMessage(data, 60L);
                }
            } else if (msg.what == EXIT_MSG) {
                //所有的禮物一起執(zhí)行離場動畫
                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};
    }

    /**
     * 坐標(biāo)點比例轉(zhuǎn)換
     * 根據(jù)發(fā)送方的寬高結(jié)合接收方自己的寬高對發(fā)送方發(fā)來的坐標(biāo)點進(jìn)行等比例轉(zhuǎn)換
     */
    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);
    }

    /**
     * 每個禮物執(zhí)行的進(jìn)場動畫(縮放出現(xiàn)0%-100%)
     * 每個禮物出現(xiàn)間隔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后需執(zhí)行離場動畫
                    handler.sendEmptyMessageDelayed(EXIT_MSG, 1500L);
                }
            }
        });
        animator.start();
    }

    /**
     * 所有的禮物一起執(zhí)行離場動畫(縮放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;
                //通知接收方禮物展示結(jié)束
                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的坐標(biāo)
                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軸坐標(biāo)邊界修正
     */
    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軸坐標(biāo)邊界修正
     */
    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;
        }
    }

    /**
     * 通知當(dāng)前禮物有變化
     */
    private void notifyGiftCountChange() {
        if (giftDiyListener != null) {
            giftDiyListener.onGiftChange(getAllGiftCount());
        }
    }

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

    /**
     * 添加當(dāng)前軌跡的第一個點
     */
    private void initPosWrap(float x, float y) {
        if (sendDataArray.get(sequenceNumber) != null) {
            throw new UnsupportedOperationException("posWrapArray中不應(yīng)該有當(dāng)前sequenceNumber:" + sequenceNumber);
        }
        if (canDraw()) {
            //初始化當(dāng)前軌跡的禮物信息對象
            PosWrap posWrap = new PosWrap();
            posWrap.bitmap = currentBitmap;
            posWrap.giftInfo = currentGift;
            //添加當(dāng)前軌跡的第一個點的范圍
            posWrap.dstList.add(getDrawDstByPos(x, y));
            sendDataArray.put(sequenceNumber, posWrap);
            invalidate();
            notifyGiftCountChange();
        }
    }

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

    /**
     * 根據(jù)圖片的大小和坐標(biāo)點計算出禮物的顯示范圍
     */
    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);
        }
    }

    /**
     * 根據(jù)正方形范圍判斷該坐標(biāo)是否在圓內(nèi)
     */
    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) {
        //獲取當(dāng)前軌跡信息
        PosWrap posWrap = sendDataArray.get(sequenceNumber);
        //根據(jù)上一個點縮放出一個正方形
        RectF square = zoomSquare(posWrap.dstList.get(posWrap.dstList.size() - 1));
        //根據(jù)該正方形判斷當(dāng)前move的坐標(biāo)是否在圓的范圍內(nèi)
        if (inCircle(square, endX, endY)) {
            return false;
        }

        /*
         * 以下根據(jù)上一個的中心點做出的圓的圓心坐標(biāo),做一條線段到當(dāng)前坐標(biāo)
         * 然后根據(jù)該線段的長度,計算可以放的下多少個圓,并把每個圓的圓心坐標(biāo)計算出來,
         * 就得出了每個禮物的坐標(biāo)點
         */
        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進(jìn)行升序排序的,而我們的sequenceNumber只進(jìn)行++處理,
         * 所以可以和ArrayList一樣保證遍歷的時候是按照添加時的順序進(jìn)行遍歷
         */
        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);
            }
        }
    }

    /**
     * 取消加載圖片的任務(wù)
     */
    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<>();
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市族扰,隨后出現(xiàn)的幾起案子厌丑,更是在濱河造成了極大的恐慌,老刑警劉巖渔呵,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怒竿,死亡現(xiàn)場離奇詭異,居然都是意外死亡扩氢,警方通過查閱死者的電腦和手機(jī)耕驰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來录豺,“玉大人朦肘,你說我怎么就攤上這事饭弓。” “怎么了媒抠?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵弟断,是天一觀的道長。 經(jīng)常有香客問我趴生,道長阀趴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任苍匆,我火速辦了婚禮舍咖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锉桑。我一直安慰自己,他們只是感情好窍株,可當(dāng)我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布民轴。 她就那樣靜靜地躺著,像睡著了一般球订。 火紅的嫁衣襯著肌膚如雪后裸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天冒滩,我揣著相機(jī)與錄音微驶,去河邊找鬼。 笑死开睡,一個胖子當(dāng)著我的面吹牛因苹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播篇恒,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼扶檐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胁艰?” 一聲冷哼從身側(cè)響起款筑,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腾么,沒想到半個月后奈梳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡解虱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年攘须,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饭寺。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡阻课,死狀恐怖叫挟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情限煞,我是刑警寧澤抹恳,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站署驻,受9級特大地震影響奋献,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜旺上,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一瓶蚂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宣吱,春花似錦窃这、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疤坝,卻和暖如春兆解,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跑揉。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工锅睛, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人历谍。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓现拒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親望侈。 傳聞我的和親對象是個殘疾皇子具练,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容

  • ios 繪畫之涂鴉,貼圖,馬賽克,高斯筆涂鴉 https://github.com/wangjinshan/IJS...
    874b526fa570閱讀 3,246評論 17 10
  • 前言: 現(xiàn)在直播APP中還有一種特別常用的禮物 —— 手繪禮物,接下來我們來談?wù)勅绾螌崿F(xiàn)這個好玩的自定義贈送禮...
    Bepawbikvy閱讀 1,047評論 0 2
  • 用兩張圖告訴你甜无,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料扛点? 從這篇文章中你...
    hw1212閱讀 12,732評論 2 59
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭岂丘,有人歡樂有人憂愁陵究,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,536評論 28 53
  • 信任包括信任自己和信任他人 很多時候奥帘,很多事情铜邮,失敗、遺憾、錯過松蒜,源于不自信扔茅,不信任他人 覺得自己做不成,別人做不...
    吳氵晃閱讀 6,190評論 4 8