| 编辑推荐: | 
								   
										
                                    | 本文来自于程序员大咖,本文属于基础文章,详细的介绍了仿照qq小红点做出的各种拉伸效果中涉及的几个知识点,对于初学者会有很大的帮助。 | 
								   
							 
						 | 
                           
                         
                            
                          一直觉得 QQ 的小红点非常具有创新,新颖。要是自己也能实现类似的效果,那怎一个爽字了得。 
                          先来看看它的最终效果: 
                            
                          
                          效果图具有哪些效果: 
                          1.在拉伸范围内的拉伸效果 
                          2.未拉出拉伸范围释放后的效果 
                          3.拉出拉伸范围再拉回的释放后的效果 
                          4.拉出拉伸范围释放后的爆炸效果 
                          涉及的相关知识点: 
                          1.onLayout 视图位置 
                           2.saveLayer 图层相关知识 
                           3.Path 的贝赛尔曲线 
                           4.手势监听 
                           5.ValueAnimator 属性动画 
                          
                            拉伸效果 
                          我们先来讲解第一个知识点,onLayout 方法: 
                          方法预览: 
                    
                           
                              |   onLayout(boolean 
                                  changed,  
                                  int left,  
                                  int top,  
                                  int right,  
                                  int bottom)  | 
                           
                         
                          
                            我记得我第一次接触这个方法的时候对后面两个参数是理解错了,还纠结了很久。先来看看一张示意图就一目了然了: 
                             
                            
                          
                            那么我们可以得出: 
                        
                           
                              |    
                                  right = left + view.getWidth();  
                                  bottom = top + view.getHeight();  | 
                           
                         
                          
                            注意: right 不要理解成视图控件右边距离屏幕右边的距离;bottom 不要理解成视图控件底部距离屏幕底部的距离。 
                          1、在屏幕中心绘制小圆点 
                          先来啾啾效果图,非常简单: 
                             
                            
                        
                           
                            |  
 public class QQ_RedPoint extends View {    private Paint mPaint;   //画笔    
    private int mRadius;    
    private PointF mCenterPoint;    
public QQ_RedPoint(Context context) {
        this(context, null);    }    
public QQ_RedPoint(Context context, AttributeSet attrs) {
        this(context, attrs, 0);    } 
public QQ_RedPoint(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);        
        mPaint = new Paint();        
        mPaint.setColor(Color.RED);        
        mPaint.setAntiAlias(true);        
        mPaint.setStyle(Paint.Style.FILL);        
        mRadius = 20;        
        mCenterPoint = new PointF();    }   
      @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        
      super.onSizeChanged(w, h, oldw, oldh);        
      mCenterPoint.x = w / 2;        
      mCenterPoint.y = h / 2;    }    
      @Override    protected void onDraw(Canvas canvas) {        
      super.onDraw(canvas);        
      canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);    }}  | 
                           
                         
                          
                            2、小圆点的拉伸效果 
                          先来看看拉伸的效果图: 
                               
                          这里就要讲解第二个知识点,Path 路径贝塞尔曲线。 
                          拉伸的效果图右三部分组成: 
                          1.中心小圆 
                           2.跟手指移动的小圆 
                           3.两个圆之间使用贝塞尔曲线填充 
                          
                            我们把拼接过程放大来看看: 
                            
                          咦,这个形状好熟悉啊,明明我在什么地方见过。怎么越看越觉得像女生用的姨妈巾呢?原来,QQ 这么有深意。 
                          中间圆的效果已经实现了,接着实现跟手指移动的小圆效果: 
                              
                          为了实现手指触摸屏幕跟随手指移动的小圆效果,重写 onTouchEvent 方法(事件不往父控件传递): 
                        
                           
                              |    
                                  @Override public boolean onTouchEvent(MotionEvent 
                                  event) { 
                                  switch (event.getAction()) { 
                                  case MotionEvent.ACTION_DOWN: {  
                                  mTouch = true; }  
                                  break;  
                                  case MotionEvent.ACTION_UP: {  
                                  mTouch = false; } }  
                                  mCurPoint.set(event.getX(), event.getY());  
                                  postInvalidate();  
                                  return true; }  | 
                           
                         
                          
                            注意:onTouchEvent 方法的返回值为 true,若为 false 捕获不到 ACTION_DOWN 
                            以后的手指状态。 
                          接着实现贝塞尔曲线填充效果,这也是本篇的难点,后面的实现就轻松。 
                              
                          Ps 技术很菜,希望绘制的草图能够帮助到您。 
                          从上效果图中分析可得: 
                          贝塞尔曲线 P1P2,起点 P1,控制点 C1C2 的中点 Q0,结束点 P2 
                          那么我们所需要的就是求到 P1 , P2 , Q0 点的坐标系,Q0 的坐标很容易得到,那么我们怎么来求 
                            P1 , P2 坐标呢?下面我画出了怎么求 P1 , P2 坐标的示意图: 
                            
                            
                            
                          根据示意图得到: 
                          
                             
                              |    
                                  P1x = x0 + r * sina  
                                  P1y = y0 - r * cosa  | 
                             
                           
                          
                            进一步推得,需要求得 P1 的坐标,需要知道 a 的角度。根据数学公式: tan(a) = dy / 
                            dx 。dx,dy 为两小圆横纵坐标差值。所以推得 a = arctan(dy / dx) 。同理可以求得 
                            P2 , P3 , P4 坐标。 
                          代码实现: 
                          P1 , P2 , P3 , P4 的坐标为: 
                          
                           
                              |    
                                  float x = mCurPoint.x;  
                                  float y = mCurPoint.y;  
                                  float startX = mCenterPoint.x;  
                                  float startY = mCenterPoint.y;  
                                  float dx = x - startX;  
                                  float dy = y - startY;  
                                  double a = Math.atan(dy / dx);  
                                  float offsetX = (float) (mRadius * Math.sin(a)); 
                                   
                                  float offsetY = (float) (mRadius * Math.cos(a)); 
                                   
                                   
                                  // 根据角度计算四边形的四个点  
                                  float p1x = startX + offsetX;  
                                  float p1y = startY - offsetY;  
                                  float p2x = x + offsetX;  
                                  float p2y = y - offsetY;  
                                  float p3x = startX - offsetX;  
                                  float p3y = startY + offsetY;  
                                  float p4x = x - offsetX;  
                                  float p4y = y + offsetY;  | 
                           
                         
                          
                            两小圆圆心连线中点 Q0 的坐标(本赛尔曲线控制点坐标): 
                          
                           
                              |    
                                  float controlX = (startX + x) / 2;  
                                  float controlY = (startY + y) / 2;  | 
                           
                         
                          
                            效果中 Path 的路径区域是个封闭的区域: 
                          
                             
                              |    
                                  mPath.reset();  
                                  mPath.moveTo(p1x, p1y);  
                                  mPath.quadTo(controlX, controlY, p2x, p2y); 
                                   
                                  mPath.lineTo(p4x, p4y);  
                                  mPath.quadTo(controlX, controlY, p3x, p3y); 
                                   
                                  mPath.lineTo(p1x, p1y);  
                                  mPath.close();路径绘制完毕,我们来看看 onDraw 方法的绘制:  
                                   
                                  @Override  
                                  protected void onDraw(Canvas canvas) {  
                                  super.onDraw(canvas);  
                                  canvas.saveLayer(new RectF(0, 0, getWidth(), 
                                  getHeight()), mPaint, Canvas.ALL_SAVE_FLAG); 
                                   
                                  canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 
                                  mRadius, mPaint);  
                                  if (mTouch) {  
                                  calculatePath();  
                                  canvas.drawCircle(mCurPoint.x, mCurPoint.y, 
                                  mRadius, mPaint);  
                                  canvas.drawPath(mPath, mPaint); }  
                                  canvas.restore();  
                                  super.dispatchDraw(canvas);//绘出该控件的所有子控件 }  | 
                           
                         
                            
                          
                            注意:我们在 onTouchEvent 方法中,我们并没有对多点触摸进行处理。如果你感兴趣,请继续关注我的博客。 
                          在 onTouchEvent 方法中调用的是 postInvalidate() 从新绘制,从新绘制有两个方法:postInvalidate 
                            ,invadite 。 
                          invadite 必须在 UI 线程中调用,而 postInvalidate 内部是由Handler的消息机制实现的,可以在任何线程中调用,效率没有 
                            invadite 高 。 
                          拉伸范围内释放效果 
                          在拉伸范围内手指释放后的效果: 
                              
                          1.初始位置只显示 TextView 控件。替换掉了以前的小圆点。 
                           2.点击 TextView 所在区域才能移动 TextView 
                            。 
                           3.拖动 TextView 且与中心小圆点以贝塞尔曲线连接形成闭合的路径。 
                           4.距离的拉伸,小圆的半径逐渐减少。 
                           5.拉伸一定的范围内,释放手指,按着原来的路径返回,且运动到中心点有反弹效果。 
                           6.我们挨着来实现以上效果。 
                          显示TextView 
                          当前控件继承 ViewGroup ,我这里继承的是 FrameLayout 。我们在初始化的时候添加 
                            TextView 控件: 
                   
                           
                            |  
  private void init() { mPaint = new Paint(); mPaint.setColor(Color.RED); 
                                  mPaint. 
                                setAntiAlias(true); 
                                  mPaint.setStyle 
                                (Paint.Style.FILL); 
                                  mRadius = 20; 
                                 mCenterPoint = 
                                  new PointF(); mCurPoint 
                                 = new PointF(); 
                                  mPath = new Path();  
                                mDragTextView = 
                                  new TextView(getContext()); LayoutParams lp 
                                  = new LayoutParams(ViewGroup.LayoutParams. 
                                WRAP_CONTENT, ViewGroup.LayoutParams. 
                                WRAP_CONTENT); mDragTextView.setLayout 
                                Params(lp); mDragTextView.setPadding 
                                (10, 10, 10, 10); 
                                  mDragTextView.set 
                                BackgroundResource 
                                (R.drawable.tv_bg); 
                                  mDragTextView. 
                                setText("99+"); 
                                  addView(mDragTextView); }  | 
                           
                         
                          
                            在 FrameLayout 中添加了 mDragTextView 控件,并对 mDragTextView 
                            控件做了一些基础的设置。对应的 tv_bg 资源文件: 
                    
                           
                              |   <?xml 
                                  version="1.0" encoding="utf-8"?> 
                                  <shape xmlns:android="http://schemas.android. 
                                com/apk/res/android"> 
                                   
                                  <corners android:radius="10dp"/> 
                                   
                                  <solid android:color="#ff0000"/> 
                                   
                                  <stroke android:color="#0f000000" 
                                 
                                android:width="1dp"/> 
                                  </shape>  | 
                           
                         
                          
                            我们重写 dispatchDraw 方法(view 重写 onDraw 方法 ,viewgroup 
                            重写 dispatchDraw 方法): 
                 
                           
                              |    
                                  @Override  
                                  protected void dispatchDraw(Canvas canvas) { 
                                   
                                  canvas.saveLayer(new RectF(0, 0, getWidth(), 
                                  getHeight()), mPaint, Canvas.ALL_SAVE_FLAG); 
                                   
                                  canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 
                                  mRadius, mPaint);  
                                  canvas.restore();  
                                  super.dispatchDraw(canvas); }  | 
                           
                         
                          
                            效果图: 
                             
                          这里我们需要注意 super.dispatchDraw(canvas); 的位置,放在最后与放在最前效果是不一样的。 
                      
                           
                              |    
                                  @Override  
                                  protected void dispatchDraw(Canvas canvas) { 
                                  //....绘制操作  
                                  super.dispatchDraw(canvas);  
                                  //绘制自身然后绘制子元素 可以理解子控件覆盖在父控件绘制之上 }  | 
                           
                         
                          
                            与 
                       
                           
                              |    
                                  @Override  
                                  protected void dispatchDraw(Canvas canvas) { 
                                   
                                  super.dispatchDraw(canvas);  
                                  //....绘制操作 //绘制子控件然后绘制自身 可以理解成父控件绘制覆盖子控件的绘制 
                                  }  | 
                           
                         
                          
                            例,我这里调整一下 super.dispatchDraw(canvas) 的位置: 
                      
                           
                              |    
                                  @Override  
                                  protected void dispatchDraw(Canvas canvas) { 
                                   
                                  super.dispatchDraw(canvas);  
                                  mPaint.setColor(Color.GREEN);//主要是为了区分红色  
                                  canvas.saveLayer(new RectF(0, 0, getWidth(), 
                                  getHeight()), mPaint, Canvas.ALL_SAVE_FLAG); 
                                   
                                  canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 
                                  mRadius, mPaint);  
                                  canvas.restore(); }  | 
                           
                         
                          
                            效果图: 
                          
                          点击TextView拖动效果 
                          点击 TextView 才能拖动文本,说明要触摸到 TextView 的矩形区域。可以通过: 
                      
                           
                              |    
                                  int x= (int) event.getX();  
                                  int y= (int) event.getY();  
                                   
                                  if(x>=mDragTextView.getLeft()&&x< 
                                =mDragTextView.getRight()&&y<=mDrag 
                                TextView.getBottom() 
                                   
                                  &&y>=mDragTextView.getTop()){  
                                  mTouch = true; }  | 
                           
                         
                          
                            也可以通过: 
                     
                           
                              |    
                                  Rect rect = new Rect();  
                                  rect.left = mDragTextView.getLeft();  
                                  rect.top = mDragTextView.getTop();  
                                  rect.right = mDragTextView.getWidth() + rect.left; 
                                   
                                  rect.bottom = mDragTextView.getHeight() + rect.top; 
                                   
                                  if (rect.contains((int) event.getX(), (int) 
                                  event.getY())) { mTouch = true; }  | 
                           
                         
                          
                            获取到所点击区域在 TextView 的矩形之内。 
                          绘制贝塞尔曲线,形成闭合的路径 
                          我们已经求出了各个点的坐标,连接形成闭合的路径。 so easy … 
                        
                           
                              |    
                                  private void calculatePath() {  
                                  float x = mCurPoint.x;  
                                  float y = mCurPoint.y;  
                                  float startX = mCenterPoint.x;  
                                  float startY = mCenterPoint.y;  
                                  float dx = x - startX;  
                                  float dy = y - startY;  
                                  double a = Math.atan(dy / dx);  
                                  float offsetX = (float) (mRadius * Math.sin(a)); 
                                   
                                  float offsetY = (float) (mRadius * Math.cos(a)); 
                                   
                                   
                                  // 根据角度计算四边形的四个点  
                                  float p1x = startX + offsetX;  
                                  float p1y = startY - offsetY;  
                                  float p2x = x + offsetX;  
                                  float p2y = y - offsetY;  
                                  float p3x = startX - offsetX;  
                                  float p3y = startY + offsetY;  
                                  float p4x = x - offsetX;  
                                  float p4y = y + offsetY;  
                                  float controlX = (startX + x) / 2;  
                                  float controlY = (startY + y) / 2;  
                                  mPath.reset();  
                                  mPath.moveTo(p1x, p1y);  
                                  mPath.quadTo(controlX, controlY, p2x, p2y); 
                                   
                                  mPath.lineTo(p4x, p4y);  
                                  mPath.quadTo(controlX, controlY, p3x, p3y); 
                                   
                                  mPath.lineTo(p1x, p1y);  
                                  mPath.close(); }  | 
                           
                         
                          
                            啾啾效果图: 
                             
                          在拉伸的过程当中,小球的大小是没有变化的。 
                          越拉伸,小球越小 
                          我们可以根据拉伸的距离动态改变小球的半径,来达到小球变小的效果。 
                          1、计算中心小球与文本的距离(三角函数): 
                      
                           
                              |    
                                  float distance = (float) Math.sqrt(Math.pow(dx, 
                                  2) + Math.pow(dy, 2));  | 
                           
                         
                          
                            2、距离越大,小球半径越小: 
                 
                           
                              |    
                                  int radius = DEFAULT_RADIUS - (int) (distance 
                                  / 18); //18 根据拉伸情况  
                                  if (radius < 8) { //拉伸一定值 固定到最小值 radius = 
                                  8; }  | 
                           
                         
                          
                            然后把效果绘制到画布上面: 
              
                           
                              |    
                                  protected void dispatchDraw(Canvas canvas) { 
                                   
                                  canvas.saveLayer(new RectF(0, 0, getWidth(), 
                                  getHeight()),  
                                  mPaint, Canvas.ALL_SAVE_FLAG);  
                                  if (mTouch) {  
                                  calculatePath();  
                                  canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 
                                  mRadius, mPaint);  
                                  canvas.drawCircle(mCurPoint.x, mCurPoint.y, 
                                  mRadius, mPaint);  
                                  canvas.drawPath(mPath, mPaint);//将textview的中心放在当前手指位置 
                                   
                                  mDragTextView.setX(mCurPoint.x - mDragTextView.getWidth() 
                                  / 2);  
                                  mDragTextView.setY(mCurPoint.y - mDragTextView.getHeight() 
                                  / 2);  
                                  }else {  
                                  mDragTextView.setX(mCenterPoint.x - mDragTextView.getWidth() 
                                  / 2);  
                                  mDragTextView.setY(mCenterPoint.y - mDragTextView.getHeight() 
                                  / 2);  
                                   
                                  }  
                                  canvas.restore();  
                                  super.dispatchDraw(canvas); }  | 
                           
                         
                          
                            看看效果: 
                             
                          拉伸范围内,释放手指后的运动效果 
                          手指释放,在 onTouchEvent方法 MotionEvent.ACTION_UP 中进行处理。 
                          1、判定当前是否拖动文本: 
        
                           
                              |    
                                  if (rect.contains((int) event.getRawX(), (int) 
                                  event.getRawY())) {  
                                  mTouch = true;  
                                  mTouchText = true;  
                                  } else {  
                                  mTouchText = false;  
                                  }  | 
                           
                         
                          
                            2、在 MotionEvent.ACTION_UP 中开启释放的动画: 
                   
                           
                              |    
                                  case MotionEvent.ACTION_UP:  
                                  mTouch = false;  
                                  if (mTouchText) {  
                                  startReleaseAnimator();  
                                  }  
                                  break;  | 
                           
                         
                          
                            3、释放动画效果: 
                   
                           
                              |    
                                  private Animator getReleaseAnimator() {  
                                  final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 
                                  0.0f);  
                                  animator.setDuration(500);  
                                  animator.setRepeatMode(ValueAnimator.RESTART); 
                                   
                                  animator.addUpdateListener(new MyAnimatorUpdateListener(this) 
                                  {  
                                  @Override  
                                  public void onAnimationUpdate(ValueAnimator 
                                  animation) {  
                                  mReleaseValue = (float) animation.getAnimatedValue(); 
                                   
                                  postInvalidate();  
                                  }  
                                  } 
                                  );  
                                  animator.setInterpolator(new OvershootInterpolator()); 
                                   
                                  return animator; }  | 
                           
                         
                          
                            非常经典的属性动画系列讲解。 
                          animator.setInterpolator(new OvershootInterpolator()); 
                            设置了插值器,OvershootInterpolator 向前甩一定值后再回到原来位置,就可以实现反弹的效果。 
                          通过 (float) animation.getAnimatedValue() 获取动画运到到某一时刻的属性值,然后刷新界面: 
                          1、根据属性值来计算文本的位置: 
                          首先获取文本距离中心小圆的横纵坐标差值: 
                
                           
                              |    
                                  float dx = mCurPoint.x - mCenterPoint.x;  
                                  float dy = mCurPoint.y - mCenterPoint.y;  | 
                           
                         
                          
                            文本的位置: 
                 
                           
                              |    
                                  float x = mCurPoint.x - dx * (1.0f - mReleaseValue); 
                                   
                                  float y = mCurPoint.y - dy * (1.0f - mReleaseValue);  | 
                           
                         
                          
                            dx (1.0f - mReleaseValue) , dy (1.0f - mReleaseValue) 
                            表示在 x 轴,y 轴上的运动距离,根据当前的位置 - 运到的距离 = 文本的位置 
                          获取到文本的位置坐标,又知道中心点坐标,根据上面的公式绘制出闭合的贝塞尔曲线,就很容易了。 
                          2、释放动画过程中,防止多次拖动文本: 
                  
                           
                              |    
                                  animator.addListener(new AnimatorListenerAdapter() 
                                  {  
                                  @Override  
                                  public void onAnimationEnd(Animator animation) 
                                  {  
                                  super.onAnimationEnd(animation);  
                                  mMoreDragText = true; }  
                                  @Override  
                                  public void onAnimationStart(Animator animation) 
                                  {  
                                  super.onAnimationStart(animation);  
                                  mMoreDragText = false;  
                                  }  
                                  } 
                                  );  | 
                           
                         
                          
                            拉伸范围外的效果 
                          拉伸到一定范围外,然后再拉回来释放手指,会发现文本回到了中心并回弹效果;拉伸到范围外释放手指,会出现爆炸效果。 
                               
                          1.拉伸到范围外再拉回释放效果 
                            2.拉伸到范围外释放爆炸效果 
                           3.拉伸到范围外再拉回释放效果 
                          只要有一次拉伸到范围外,再拉回来释放,就不会再绘制中心小圆以及贝塞尔曲线的闭合路径。所以这里需要一个布尔值的标识,只要小圆半径减少到一定值就把标识设置为 
                            true 
                     
                           
                              |    
                                  if (mRadius == 8) {  
                                  mOnlyOneMoreThan = true;  
                                  }  | 
                           
                         
                          
                            在 dispatchDraw 方法里面绘制文本的位置: 
                        
                           
                              |    
                                  mDragTextView.setX(mCenterPoint.x - mDragTextView.getWidth() 
                                  / 2);  
                                  mDragTextView.setY(mCenterPoint.y - mDragTextView.getHeight() 
                                  / 2);  | 
                           
                         
                          
                            拉伸到范围外释放爆炸效果 
                          爆炸效果,是用一张张图片实现的。我们需要添加一个 ImageView 控件来单独播放爆炸的图片,具体步骤如下: 
                          1、新增图片数组: 
                    
                           
                              |    
                                  private int[] mExplodeImages = new int[]{  
                                  R.mipmap.idp,  
                                  R.mipmap.idq,  
                                  R.mipmap.idr,  
                                  R.mipmap.ids,  
                                  R.mipmap.idt}; //爆炸的图片集合  | 
                           
                         
                          
                            2、新增 ImageView 用于播放爆炸效果: 
                    
                           
                              |    
                                  mExplodeImage = new ImageView(getContext()); 
                                   
                                  mExplodeImage.setLayoutParams(lp);  
                                  mExplodeImage.setImageResource(R.mipmap.idp); 
                                   
                                  mExplodeImage.setVisibility(View.INVISIBLE); 
                                   
                                  addView(mExplodeImage);  | 
                           
                         
                          3、范围外,手指离开,播放爆炸效果: 
                          
                             
                              |    
                                  ValueAnimator animator = ValueAnimator. 
                                ofInt(0, mExplodeImages.length 
                                  - 1);  
                                  animator.setInterpolator(new LinearIn 
                                terpolator());  
                                  animator.setDuration(1000);  
                                  animator.addUpdateListener(new MyAnimatorUpdateListener(this) 
                                  {  
                                  @Override  
                                  public void onAnimationUpdate(Value 
                                Animator animation) 
                                  {  
                                  mExplodeImage.setBackgroundResource 
                                (mExplodeImages[(int) 
                                  animation. 
                                getAnimatedValue()]); 
                                   
                                  }  
                                  } 
                                  );  
                                  return animator;  
                                  }  | 
                             
                           
                          mExplodeImage 的位置应该是手指离开的位置: 
                    
                           
                              |    
                                  private void layoutExplodeImage() {  
                                  mExplodeImage.setX(mCurPoint.x - mDragTextView.getWidth() 
                                  / 2);  
                                  mExplodeImage.setY(mCurPoint.y - mDragTextView.getHeight() 
                                  / 2); }  | 
                           
                         
                             |