立即春节,写个应景的控件
思路分析
大体知识点概况
实战
继承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.创建一个画布,就是一个绘制一个红包的图片。根据手指在控件上的滑动路径,除去图片的结合部分
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);
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; }
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)。
随机点为控件内部随意点。当然为了更好的效果。点位分布均匀为佳。
/** * 估值器 */ static class BSEEvaluator implements TypeEvaluator<PointF> { 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(); }
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<PointF> { 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.试下
<?xml version="1.0" encoding="utf-8"?
> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="sample.MainActivity" tools:showIn="@layout/activity_main"> <com.empty.launchredpacket.LaunchRedPacketLayout android:id="@+id/launchRedPacket" android:layout_width="match_parent" android:background="#faecec" android:layout_height="400dp" /> <Button android:id="@+id/launchBtn" android:layout_width="match_parent" android:layout_height="40dp" android:layout_margin="5dp" android:background="@color/colorPrimary" android:text="发射" android:textColor="@android:color/white" /> <Button android:id="@+id/reStart" android:layout_width="match_parent" android:layout_height="40dp" android:layout_margin="5dp" android:background="@color/colorPrimary" android:text="又一次開始" android:textColor="@android:color/white" /> </LinearLayout>
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(); } } }
总结
十分感谢 程序亦非猿,hongyang 大神的博客
源代码地址 https://github.com/wolow3/LaunchRedPacket