发红包android

马上春节,写个应景的控件

发红包android_第1张图片       

思路分析

1.红包沿着不同的轨迹由上往下运动
2.当手指捕获到一个红包,红包停止原先的运动,可以随着手指的滑动做跟手操作
3.当手指动作停止后,红包放大
4.通过滑动刮开红包,看到期待已久的money 

大体知识点概况

1.属性动画,实现红包按照贝塞尔曲线运动和放大效果
2.实现一个可移动的view,可以参考我的另一篇博客http://blog.csdn.net/xuan_xiaofeng/article/details/50463595
3.图片的结合模式,主要是实现刮开红包
4.自定义控件的相关知识 

实战

1.先来做个红包。继承view,做点初始化的工作
private void init() {
    mPath = new Path();
    mRandom = new Random();

    initPaint();
    initMoneyPaint();

    mText = moneys[mRandom.nextInt(moneys.length)];

    //获取字体的宽高
    moneyPaint.getTextBounds(mText, 0, mText.length(), mTextBound);
}

private void initPaint() {
    mPaint = new Paint();
    mPaint.setColor(Color.parseColor("#c0c0c0"));
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    /**
     * 设置接合处的形态
     */
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    /**
     * 抗抖动
     */
    mPaint.setDither(true);
    mPaint.setAntiAlias(true);
    mPaint.setStrokeWidth(PAINT_WIDTH);
}

/**
 * money画笔
 */
private void initMoneyPaint() {
    moneyPaint = new Paint();
    moneyPaint.setColor(Color.RED);
    moneyPaint.setAntiAlias(true);
    moneyPaint.setTextSize(30);
    mTextBound = new Rect();
    moneyPaint.getTextBounds(moneys[0], 0, moneys[0].length(), mTextBound);
}

2.创建一个画布,就是一个绘制一个红包的图片,依据手指在控件上的滑动路径,除去图片的结合部分
发红包android_第2张图片
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);

Bitmap bitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.red_packet));

mCanvas.drawBitmap(bitmap, null, new RectF(0, 0, width, height), null);

//设置图片的结合方式
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
mCanvas.drawPath(mPath, mPaint);
canvas.drawBitmap(mBitmap, 0, 0, null);

3.重写onTouchEvent方法记录手指的擦除路径以及实现跟手操作
public boolean onTouchEvent(MotionEvent event) {
    x = (int) event.getX();
    y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //路径的初始化位置
            mPath.moveTo(x, y);
            break;
        case MotionEvent.ACTION_MOVE:
            if (movable) {
                // 跟手滑效果
                setX(x + getLeft() + getTranslationX() - getWidth() / 2);
                setY(y + getTop() + getTranslationY() - getHeight() / 2);
            } else if (Math.abs(x - mLastX) > DEFAULT_PATH_INSTANCE || Math.abs(y - mLastY) > DEFAULT_PATH_INSTANCE) {
                // 记录手指擦除路径
                mPath.lineTo(x, y);
                invalidate();
            }
        case MotionEvent.ACTION_UP:
            MyAsyncTask task = new MyAsyncTask();
            task.execute();
            break;
    }

    //记录上次位置
    mLastX = x;
    mLastY = y;
    return true;
}

4.附上完整的代码
public class RedPacketView extends ImageView {
    private Paint mPaint, moneyPaint;
    private Path mPath;
    private Canvas mCanvas;
    private Bitmap mBitmap;
    private int x, y, mLastX, mLastY;
    public boolean movable = true;
    public boolean isTouch = false;
    private String[] moneys = new String[]{"¥5", "¥10", "¥20", "¥50"};
    private Rect mTextBound;
    private String mText;
    private Random mRandom;
    private boolean isComplete = false;

    /**
     * 笔触的宽度
     */
    private static final float PAINT_WIDTH = 20;
    /**
     * 默认绘制的最小距离
     */
    private static final float DEFAULT_PATH_INSTANCE = 5;

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

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

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

    private void init() {
        mPath = new Path();
        mRandom = new Random();

        initPaint();
        initMoneyPaint();

        //随机产生一个面值
        mText = moneys[mRandom.nextInt(moneys.length)];

        //获取字体的宽高
        moneyPaint.getTextBounds(mText, 0, mText.length(), mTextBound);
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#c0c0c0"));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        /**
         * 设置接合处的形态
         */
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        /**
         * 抗抖动
         */
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(PAINT_WIDTH);
    }

    /**
     * money画笔
     */
    private void initMoneyPaint() {
        moneyPaint = new Paint();
        moneyPaint.setColor(Color.RED);
        moneyPaint.setAntiAlias(true);
        moneyPaint.setTextSize(30);
        mTextBound = new Rect();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        try {
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mBitmap);

            Bitmap bitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.red_packet));

            mCanvas.drawBitmap(bitmap, null, new RectF(0, 0, width, height), null);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        try {
            canvas.drawText(mText, getWidth() / 2 - mTextBound.width() / 2, getHeight() / 2 + mTextBound.height() / 2, moneyPaint);

            if (isComplete) return;

            //设置图片的结合方式
            mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
            mCanvas.drawPath(mPath, mPaint);

            canvas.drawBitmap(mBitmap, 0, 0, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        x = (int) event.getX();
        y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //路径的初始化位置
                mPath.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                if (movable) {
                    // 跟手滑效果
                    setX(x + getLeft() + getTranslationX() - getWidth() / 2);
                    setY(y + getTop() + getTranslationY() - getHeight() / 2);
                } else if (Math.abs(x - mLastX) > DEFAULT_PATH_INSTANCE || Math.abs(y - mLastY) > DEFAULT_PATH_INSTANCE) {
                    // 记录手指擦除路径
                    mPath.lineTo(x, y);
                    invalidate();
                }
            case MotionEvent.ACTION_UP:
                MyAsyncTask task = new MyAsyncTask();
                task.execute();
                break;
        }

        //记录上次位置
        mLastX = x;
        mLastY = y;
        return true;
    }

    /**
     * 查看目前的红包的擦除比例,实现完全擦除
     */
    class MyAsyncTask extends AsyncTask{
        @Override
        protected Object doInBackground(Object[] params) {
            clearOverPercent();
            return null;
        }

        private void clearOverPercent()
        {
            int[] mPixels;

            int w = getWidth();
            int h = getHeight();

            float wipeArea = 0;
            float totalArea = w * h;

            Bitmap bitmap = Bitmap.createBitmap(mBitmap);

            mPixels = new int[w * h];

            //拿到所有像素信息
            bitmap.getPixels(mPixels, 0, w, 0, 0, w, h);

            //获取擦除部分的面积
            int index = 0;
            for (int i = 0; i < w; i++) {
                for (int j = 0; j < h; j++) {
                    if (mPixels[index] == 0) {
                        wipeArea++;
                    }
                    index++;
                }
            }

            int percent = (int) (wipeArea / totalArea * 100);
            if (percent > 70) {
                isComplete = true;
                postInvalidate();
            }
        }
    };
}

5.实现发红包的父容器LaunchRedPacketLayout。重点说下贝塞尔曲线动画部分的实现。实现的过程用到四个点,分别是起点,随机点1,随机点2,终点。起点为控件的底部中点,终点为控件顶部的任意点即(x=n, y=0)。随机点为控件内部任意点,当然为了更好的效果,点位分布均匀为佳。发红包android_第3张图片

6.有了四个点后,根据贝塞尔曲线的公式新建一个估值器,以便于计算红包当前的位置
/**
 * 估值器
 */
static class BSEEvaluator implements TypeEvaluator {
    private PointF pointF1;
    private PointF pointF2;

    public BSEEvaluator(PointF pointF1, PointF pointF2) {
        this.pointF1 = pointF1;
        this.pointF2 = pointF2;
    }

    @Override
    public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
        PointF pointF = new PointF();

        float lFraction = 1 - fraction;

        pointF.x = (float) (startValue.x * Math.pow(lFraction, 3) +
                3 * pointF1.x * fraction * Math.pow(lFraction, 2) +
                3 * pointF2.x * Math.pow(lFraction, 2) * fraction +
                endValue.x * Math.pow(fraction, 3));
        pointF.y = (float) (startValue.y * Math.pow(lFraction, 3) +
                3 * pointF1.y * fraction * Math.pow(lFraction, 2) +
                3 * pointF2.y * Math.pow(fraction, 2) * lFraction +
                endValue.y * Math.pow(fraction, 3));

        return pointF;
    }
}

7.设置属性动画的监听器,不断将新的位置设置给红包,让红包动起来

private ValueAnimator getBSEValueAnimator(View target) {
    //贝赛尔估值器
    BSEEvaluator evaluator = new BSEEvaluator(getPoint(), getPoint());
    ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - dWidth) / 2, mHeight - dHeight), new PointF(random.nextInt(mWidth), 0));
    animator.addUpdateListener(new BSEListenr(target));
    animator.setTarget(target);
    animator.setDuration(3000);
    return animator;
}

private class BSEListenr implements ValueAnimator.AnimatorUpdateListener {

    private View target;

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

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        //这里获取到贝塞尔曲线计算出来的的xy值
        PointF pointF = (PointF) animation.getAnimatedValue();
        target.setX(pointF.x);
        target.setY(pointF.y);
    }
}

8.提供发射红包的入口方法

/**
 * 发射多个红包
 *
 * @param numb
 */
public void launch(int numb) throws Exception {
    for (int i = 0; i < numb; i++)
        launch();
}

/**
 * 发射红包
 */
public void launch() throws Exception {
    final RedPacketView imageView = new RedPacketView(getContext());
    imageView.setImageDrawable(drawable);

    //设置位置
    LayoutParams layoutParams = new LayoutParams(dWidth, dHeight);
    layoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE);
    layoutParams.addRule(CENTER_HORIZONTAL, TRUE);
    imageView.setLayoutParams(layoutParams);

    final Animator set = addAnimatior(imageView);

    imageView.setOnTouchListener(new OnTouchListener() {
        public boolean onTouch(View v, MotionEvent event) {
            x = (int) imageView.getX();
            y = (int) imageView.getY();

            if (!imageView.isTouch) {
                imageView.isTouch = true;
                set.end();
            }

            if (MotionEvent.ACTION_UP == event.getAction()) {
                if (imageView.movable) {
                    ObjectAnimator.ofFloat(imageView, View.ALPHA, 1f).start();
                    AnimatorSet setDown = new AnimatorSet();
                    setDown.playTogether(
                            ObjectAnimator.ofFloat(imageView, "scaleX", 0.8f, 1.5f),
                            ObjectAnimator.ofFloat(imageView, "scaleY", 0.8f, 1.5f)
                    );
                    setDown.start();

                    imageView.movable = false;
                }
            }

            return false;
        }
    });

    addView(imageView);
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);

            // 动画结束移除view
            if (imageView.isTouch) {
                imageView.setX(x);
                imageView.setY(y);
            } else {
                removeView(imageView);
            }
        }
    });
    set.start();
}


9.附上完整代码

public class LaunchRedPacketLayout extends RelativeLayout {
    private Drawable drawable;
    private int dWidth;
    private int dHeight;
    private int mWidth;
    private int mHeight;
    int x, y;

    /**
     * 插值器组
     */
    private Interpolator[] interpolatorsArray;

    private Random random;

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

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

    private void init() {
        drawable = getResources().getDrawable(R.drawable.red_packet);
        dWidth = drawable.getIntrinsicWidth();
        dHeight = drawable.getIntrinsicHeight();

        random = new Random();

        interpolatorsArray = new Interpolator[4];
        interpolatorsArray[0] = new LinearInterpolator();
        interpolatorsArray[1] = new AccelerateInterpolator();
        interpolatorsArray[2] = new DecelerateInterpolator();
        interpolatorsArray[3] = new AccelerateDecelerateInterpolator();

        post(new Runnable() {
            @Override
            public void run() {
                mHeight = getMeasuredHeight();
                mWidth = getMeasuredWidth();

                int curWidth = dWidth;
                dWidth = mWidth / 5;
                dHeight = dHeight * dWidth / curWidth;
            }
        });
    }

    /**
     * 发射多个红包
     *
     * @param numb
     */
    public void launch(int numb) throws Exception {
        for (int i = 0; i < numb; i++)
            launch();
    }

    /**
     * 发射红包
     */
    public void launch() throws Exception {
        final RedPacketView imageView = new RedPacketView(getContext());
        imageView.setImageDrawable(drawable);

        //设置位置
        LayoutParams layoutParams = new LayoutParams(dWidth, dHeight);
        layoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE);
        layoutParams.addRule(CENTER_HORIZONTAL, TRUE);
        imageView.setLayoutParams(layoutParams);

        final Animator set = addAnimatior(imageView);

        imageView.setOnTouchListener(new OnTouchListener() {
            public boolean onTouch(View v, MotionEvent event) {
                x = (int) imageView.getX();
                y = (int) imageView.getY();

                if (!imageView.isTouch) {
                    imageView.isTouch = true;
                    set.end();
                }

                if (MotionEvent.ACTION_UP == event.getAction()) {
                    if (imageView.movable) {
                        ObjectAnimator.ofFloat(imageView, View.ALPHA, 1f).start();
                        AnimatorSet setDown = new AnimatorSet();
                        setDown.playTogether(
                                ObjectAnimator.ofFloat(imageView, "scaleX", 0.8f, 1.5f),
                                ObjectAnimator.ofFloat(imageView, "scaleY", 0.8f, 1.5f)
                        );
                        setDown.start();

                        imageView.movable = false;
                    }
                }

                return false;
            }
        });

        addView(imageView);
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);

                // 动画结束移除view
                if (imageView.isTouch) {
                    imageView.setX(x);
                    imageView.setY(y);
                } else {
                    removeView(imageView);
                }
            }
        });
        set.start();
    }

    /**
     * 设置动画
     *
     * @param target
     */
    private Animator addAnimatior(View target) throws Exception {
        AnimatorSet set = new AnimatorSet();
        AnimatorSet enterSet = getEnterSet(target);

        ValueAnimator bezierValueAnimator = getBSEValueAnimator(target);
        set.playSequentially(enterSet, bezierValueAnimator);
        set.setInterpolator(interpolatorsArray[random.nextInt(4)]);
        set.setTarget(target);
        return set;
    }

    private class BSEListenr implements ValueAnimator.AnimatorUpdateListener {

        private View target;

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

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //这里获取到贝塞尔曲线计算出来的的x y值
            PointF pointF = (PointF) animation.getAnimatedValue();
            target.setX(pointF.x);
            target.setY(pointF.y);
        }
    }

    /**
     * 设置贝赛尔曲线动画
     *
     * @param target
     * @return
     */
    private ValueAnimator getBSEValueAnimator(View target) {
        //贝赛尔估值器
        BSEEvaluator evaluator = new BSEEvaluator(getPoint(), getPoint());
        ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - dWidth) / 2, mHeight - dHeight), new PointF(random.nextInt(mWidth), 0));
        animator.addUpdateListener(new BSEListenr(target));
        animator.setTarget(target);
        animator.setDuration(3000);
        return animator;
    }

    private PointF getPoint() {
        PointF pointF = new PointF();
        pointF.x = random.nextInt(mWidth);
        pointF.y = random.nextInt(mHeight - dHeight);
        return pointF;
    }

    /**
     * 估值器
     */
    static class BSEEvaluator implements TypeEvaluator {
        private PointF pointF1;
        private PointF pointF2;

        public BSEEvaluator(PointF pointF1, PointF pointF2) {
            this.pointF1 = pointF1;
            this.pointF2 = pointF2;
        }

        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            PointF pointF = new PointF();

            float lFraction = 1 - fraction;

            pointF.x = (float) (startValue.x * Math.pow(lFraction, 3) +
                    3 * pointF1.x * fraction * Math.pow(lFraction, 2) +
                    3 * pointF2.x * Math.pow(lFraction, 2) * fraction +
                    endValue.x * Math.pow(fraction, 3));
            pointF.y = (float) (startValue.y * Math.pow(lFraction, 3) +
                    3 * pointF1.y * fraction * Math.pow(lFraction, 2) +
                    3 * pointF2.y * Math.pow(fraction, 2) * lFraction +
                    endValue.y * Math.pow(fraction, 3));

            return pointF;
        }
    }

    /**
     * 入场动画
     *
     * @param target
     * @return
     */
    private AnimatorSet getEnterSet(View target) {
        try {
            AnimatorSet enterSet = new AnimatorSet();

            enterSet.playTogether(
                    ObjectAnimator.ofFloat(target, View.ALPHA, 0, 1f),
                    ObjectAnimator.ofFloat(target, View.SCALE_X, 0.1f, 0.8f),
                    ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.1f, 0.8f)
            );
            enterSet.setDuration(500);
            enterSet.setInterpolator(new LinearInterpolator());
            enterSet.setTarget(target);

            return enterSet;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

10.试下




    

    

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private LaunchRedPacketLayout launchRedPacketLayout;
    private Button launchBtn, reStartBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        launchRedPacketLayout = (LaunchRedPacketLayout) findViewById(R.id.launchRedPacket);
        launchBtn = (Button) findViewById(R.id.launchBtn);
        reStartBtn = (Button) findViewById(R.id.reStart);

        launchBtn.setOnClickListener(this);
        reStartBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        try {
            switch (v.getId()) {
                case R.id.reStart:
                    startActivity(new Intent(this, MainActivity.class));
                    finish();
                    overridePendingTransition(0, 0);
                    break;
                case R.id.launchBtn:
                    launchRedPacketLayout.launch(3);
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结

1.发射过多红包会引起页面卡顿,需要优化
2.代码结构还可以优化下
3.欢迎大家评论交流 

十分感谢 程序亦非猿,hongyang 大神的博客


源码地址 https://github.com/wolow3/LaunchRedPacket


你可能感兴趣的:(android,自定义view)