鹅厂系列三 : 仿QQ消息拖动小球

未来会怎样,我不知道,我只是想为了比今天好

老规矩,看看效果
效果1效果2效果3

嗯,前面自定义了两个视图容器,今天这个是自定义View,开始自定义前,我们应该理清自己的思路,怎么来做这个东西.用我们的QQ,我们会发现,它可以拖动,有两个圆,中间像个橡皮泥连着,所以我是这么想的,我们可以在它初始状态下,画两个圆,一个固定不动,一个可以拖动,且上面还要画个数字.嗯,我们先来完成这个.

public class DragBall extends View {

    private Paint mPaint;
    private PointF mCicleCenterPoint;
    private PointF mDragCicleCenter;
    private float mRadius;
    private float mDragRadius;
    private Paint mTextPaint;
    private int mStatusBarHeight;

    private int mRang;
    private float saveRdius = 0;
    private int mNumber = 0;

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

    public DragBall(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragBall(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);  //填充
        mPaint.setDither(true);             //防抖动

        mRadius = dp2px(context, 9);        //半径9dp
        mDragRadius = mRadius;

        saveRdius = mRadius;

        mTextPaint = new Paint();
        mTextPaint.setTextAlign(Paint.Align.CENTER);        //字居中对齐
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(mRadius * 1.2f);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setDither(true);


        mStatusBarHeight = getStatusBarHeight();

        Log.d("DragBall", "mStatusBarHeight:" + mStatusBarHeight);

    }


    public void setCenterPoint(float x, float y) {


        mCicleCenterPoint = new PointF(x, y);

        mDragCicleCenter = new PointF(x, y);
    }


    public void setDragRang(int rang) {  //拖动范围
        mRang = rang;
    }

    public void setNumber(int number) {
        mNumber = number;

    }

    public int getStatusBarHeight() {//获取状态栏高度
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    public int dp2px(Context context, int dp) {

        return (int) (context.getResources().getDisplayMetrics().density * dp);
    }

    @Override
    protected void onDraw(Canvas canvas) {


        canvas.drawCircle(mCicleCenterPoint.x, mCicleCenterPoint.y, mRadius, mPaint); //画固定圆

        canvas.drawCircle(mDragCicleCenter.x, mDragCicleCenter.y, mDragRadius, mPaint);//画拖动圆

        canvas.drawText("" + mNumber, mDragCicleCenter.x, mDragCicleCenter.y + mDragRadius / 2, mTextPaint); //画文字

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float delX;
        float dely;
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                break;

            case MotionEvent.ACTION_MOVE:

                mDragCicleCenter.x = event.getRawX();         //手机坐标
                mDragCicleCenter.y = event.getRawY();

                break;

            case MotionEvent.ACTION_UP:

                mDragCicleCenter.x = mCicleCenterPoint.x;
                mDragCicleCenter.y = mCicleCenterPoint.y;              //回到初始位置,固定圆不动

                break;
        }

        invalidate();           //刷新界面 调onDraw
        return true;
    }
}

效果
继承view都要我们重写onDraw方法,里面画板canvas可以画许多东西,这里就不细讲了,我也细讲不了.自我感觉还没理解到那个程度,但基本的我们还是会用的,我们通过触摸事件不断更新拖动圆的圆心坐标和不断重绘invalidate(),达到拖动的效果,但是,发现了吧,这感觉圆心不在我们点的那个位置啊,这是因为画板画的时候,算了状态栏高度,而我们的点是没有算状态栏高度的,即我们本来画(100,100)这个点,画板给我们画到了(100,100+状态栏高度),所以我们要把我们的画板上移状态栏高度再进行绘画.

  @Override
    protected void onDraw(Canvas canvas) {

        canvas.save();
        canvas.translate(0,-mStatusBarHeight);

        canvas.drawCircle(mCicleCenterPoint.x, mCicleCenterPoint.y, mRadius, mPaint); //画固定圆

        canvas.drawCircle(mDragCicleCenter.x, mDragCicleCenter.y, mDragRadius, mPaint);//画拖动圆

        canvas.drawText("" + mNumber, mDragCicleCenter.x, mDragCicleCenter.y + mDragRadius / 2, mTextPaint); //画文字

        canvas.restore();
    }

处理后
这样我们的圆就指哪画哪了是吧,这里canvas.save()和canvas.restore(),即保存和恢复,为什么要这样呢,因为你想,onDraw执行了多少次,如果你不保存好状态,也不恢复的话,画板会移到哪里去..这也说明了,画是一层一层画上去的,不是只在一层上,每次画就画在对应的那个层上.不然如果在一个层的话,又恢复了,那不等于没移对吧.然后,画完这两个圆后,我们应该去画我们中间那个东西了,那个东西其实是画路径画出来的,我们来看个图加深下理解原理

画板canva有个方法drawPath,即画路径,而Path路径,可以走直线,也可以走贝塞尔曲线,什么是贝塞尔曲线呢,来看个图
贝塞尔这具体是什么算法我们不要管.,我们调用path.quadTo方法就好了,它前两个参数是p1控制点的横纵坐标,后两个参数是p2终点的横纵坐标,这时你会说,起点呢,起点就是path出发的那个点,我们new Path的时候,一般要执行个方法path.moveTo就是站到那个点去,如果不掉,默认最开始起点为(0,0),然后是lineTo(直走)还是quadTo(贝塞尔曲线走)随便你,每条路径的终点即为下一路径的起点.所以来到我画的那张图,其实中间那个橡皮泥一样的东西就是path先moveTo A 然后贝塞尔到C再直线到D,再贝塞尔到B,然后path关闭,path会形成回路,即它的终点,直线到它最开始的那个起点A点,这样ACDB连接起来就是中间那个东西了,我们来看下代码吧

 @Override
    protected void onDraw(Canvas canvas) {

        canvas.save();
        canvas.translate(0,-mStatusBarHeight);

        Path path = new Path();
        path.moveTo(mPointA.x, mPointA.y);  //到A点

        path.quadTo(mPointE.x, mPointE.y, mPointC.x, mPointC.y); //从A走贝塞尔曲线到C点

        path.lineTo(mPointD.x, mPointD.y);                       //从C走直线到D点

        path.quadTo(mPointE.x, mPointE.y, mPointB.x, mPointB.y); //从D点走贝塞尔曲线到B点

        path.close();                                             //关闭路径,形成回路最后终点B点直线到起点A点

        canvas.drawPath(path, mPaint);                           //画出路径

        canvas.drawCircle(mCicleCenterPoint.x, mCicleCenterPoint.y, mRadius, mPaint); //画固定圆

        canvas.drawCircle(mDragCicleCenter.x, mDragCicleCenter.y, mDragRadius, mPaint);//画拖动圆

        canvas.drawText("" + mNumber, mDragCicleCenter.x, mDragCicleCenter.y + mDragRadius / 2, mTextPaint); //画文字

        canvas.restore();
    }


/**
     * 计算图中的ABCDE点,在触摸move的时候调用
     */
    private void ComputerFivePoint() {

        mPointE.x = mDragCicleCenter.x / 2 - mCicleCenterPoint.x / 2 + mCicleCenterPoint.x;
        mPointE.y = mDragCicleCenter.y / 2 - mCicleCenterPoint.y / 2 + mCicleCenterPoint.y;

        double tan = (mDragCicleCenter.x - mCicleCenterPoint.x) / (mDragCicleCenter.y - mCicleCenterPoint.y);
        double atan = Math.atan(tan);  //得到角度
        mPointA.x = (float) (mCicleCenterPoint.x + Math.cos(atan) * mRadius);
        mPointA.y = (float) (mCicleCenterPoint.y - Math.sin(atan) * mRadius);
        mPointB.x = (float) (mCicleCenterPoint.x - Math.cos(atan) * mRadius);
        mPointB.y = (float) (mCicleCenterPoint.y + Math.sin(atan) * mRadius);


        mPointC.x = (float) (mDragCicleCenter.x + Math.cos(atan) * mDragRadius);
        mPointC.y = (float) (mDragCicleCenter.y - Math.sin(atan) * mDragRadius);
        mPointD.x = (float) (mDragCicleCenter.x - Math.cos(atan) * mDragRadius);
        mPointD.y = (float) (mDragCicleCenter.y + Math.sin(atan) * mDragRadius);

        invalidate();
    }

效果

好了,做到这里就差不多了,接下来的就是设置范围,然后根据范围把固定圆的半径根据拉出距离和半径比缩小,再就是拖出范围,不画固定圆和中间那段路径,再就是抬起判断,如果在距离内,回到初始位置,如果一直都没出过范围就执行回弹动画,超出距离,消失,都不画.嗯,就是一堆判断考验细心耐心,还是有点注释,加上我前面讲的.还是自己看吧.

public class DragBall extends View {

    private Paint mPaint;
    private PointF mCicleCenterPoint;
    private PointF mDragCicleCenter;
    private float mRadius;
    private float mDragRadius;
    private Paint mTextPaint;
    private int mStatusBarHeight;
    private PointF mPointE;
    private PointF mPointA;
    private PointF mPointB;
    private PointF mPointC;
    private PointF mPointD;
    private int mRang;
    private boolean isOutOfRang = false;
    private float saveRdius = 0;
    private boolean isDisappear = false;
    private int mNumber = 0;

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

    public DragBall(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragBall(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);  //填充
        mPaint.setDither(true);             //防抖动


        mRadius = dp2px(context,9);
        mDragRadius = mRadius;

        saveRdius = mRadius;

        mTextPaint = new Paint();
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(mRadius * 1.2f);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setDither(true);


        mStatusBarHeight = getStatusBarHeight();

        Log.d("DragBall", "mStatusBarHeight:" + mStatusBarHeight);


    }


    public void setCenterPoint(float x, float y) {


        mCicleCenterPoint = new PointF(x, y);

        mDragCicleCenter = new PointF(x, y);

        mPointA = new PointF(x + mRadius, y);
        mPointB = new PointF(x - mRadius, y);

        mPointC = new PointF(x + mRadius, y);
        mPointD = new PointF(x - mRadius, y);

        mPointE = new PointF(x, y);
    }


    public void setDragRang(int rang) {  //拖动范围
        mRang = rang;
    }

    public void setNumber(int number) {
        mNumber = number;

    }

    @Override
    protected void onDraw(Canvas canvas) {

        canvas.save();
        canvas.translate(0, -mStatusBarHeight);


        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(mCicleCenterPoint.x, mCicleCenterPoint.y, mRang, mPaint);
        mPaint.setStyle(Paint.Style.FILL);

        if (!isOutOfRang) {

            Path path = new Path();
            path.moveTo(mPointA.x, mPointA.y);  //到A点

            path.quadTo(mPointE.x, mPointE.y, mPointC.x, mPointC.y); //从A走贝塞尔曲线到C点

            path.lineTo(mPointD.x, mPointD.y);                       //从C走直线到D点

            path.quadTo(mPointE.x, mPointE.y, mPointB.x, mPointB.y); //从D点走贝塞尔曲线到B点

            path.close();                                             //关闭路径,形成回路最后终点B点直线到起点A点

            canvas.drawPath(path, mPaint);                           //画出路径

            canvas.drawCircle(mCicleCenterPoint.x, mCicleCenterPoint.y, mRadius, mPaint); //画固定圆
        }

        if (!isDisappear) {


            canvas.drawCircle(mDragCicleCenter.x, mDragCicleCenter.y, mDragRadius, mPaint);//画拖动圆



            canvas.drawText("" + mNumber, mDragCicleCenter.x, mDragCicleCenter.y + mDragRadius / 2, mTextPaint); //画文字
        }

        canvas.restore();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float delX;
        float dely;
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                break;

            case MotionEvent.ACTION_MOVE:

                mDragCicleCenter.x = event.getRawX();         //手机坐标
                mDragCicleCenter.y = event.getRawY();
                ComputerFivePoint();                            //计算ABCD坐标

                delX = Math.abs(mDragCicleCenter.x - mCicleCenterPoint.x);
                dely = Math.abs(mDragCicleCenter.y - mCicleCenterPoint.y);

                if (Math.sqrt(delX * delX + dely * dely) <= mRang) {         //拖动距离
                    mRadius = (float) (saveRdius - Math.sqrt(delX * delX + dely * dely) / mRang * (mRadius / 7 * 6));           //固定圆半径缩小
                } else {
                    isOutOfRang = true;
                }

                break;

            case MotionEvent.ACTION_UP:

                if (isOutOfRang) {

                    delX = Math.abs(mDragCicleCenter.x - mCicleCenterPoint.x);
                    dely = Math.abs(mDragCicleCenter.y - mCicleCenterPoint.y);
                    if (Math.sqrt(delX * delX + dely * dely) < mRang) {
                        isOutOfRang = false;
                        mDragCicleCenter.x = mCicleCenterPoint.x;
                        mDragCicleCenter.y = mCicleCenterPoint.y;
                        ComputerFivePoint();
                    } else {
                        isDisappear = true;
                        invalidate();

                        if (monDragBallListener != null) {
                            monDragBallListener.onDisappear(this, mDragCicleCenter.x, mDragCicleCenter.y); //消失的时候
                        }
                    }

                } else {

                    ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
                    animator.setInterpolator(new OvershootInterpolator(5.0f));

                    final PointF startPoint = new PointF(mDragCicleCenter.x,mDragCicleCenter.y);
                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {

                            float fraction = animation.getAnimatedFraction();  //百分比

                            mDragCicleCenter.x = startPoint.x + (mCicleCenterPoint.x - startPoint.x) * fraction;
                            mDragCicleCenter.y = startPoint.y + (mCicleCenterPoint.y - startPoint.y) * fraction;
                            Log.d("DragBall", "fraction:" + fraction);

                            ComputerFivePoint();
                        }
                    });

                    animator.setDuration(200);
                    animator.start();

                   animator.addListener(new Animator.AnimatorListener() {
                       @Override
                       public void onAnimationStart(Animator animation) {

                       }

                       @Override
                       public void onAnimationEnd(Animator animation) {
                           if (monDragBallListener != null){
                               monDragBallListener.onReset();
                           }
                       }

                       @Override
                       public void onAnimationCancel(Animator animation) {

                       }

                       @Override
                       public void onAnimationRepeat(Animator animation) {

                       }
                   });
                }

                mRadius = saveRdius;          //固定圆半径还原


                break;
        }

        return true;
    }

    /**
     * 计算图中的ABCDE点
     */
    private void ComputerFivePoint() {

        mPointE.x = mDragCicleCenter.x / 2 - mCicleCenterPoint.x / 2 + mCicleCenterPoint.x;
        mPointE.y = mDragCicleCenter.y / 2 - mCicleCenterPoint.y / 2 + mCicleCenterPoint.y;

        double tan = (mDragCicleCenter.x - mCicleCenterPoint.x) / (mDragCicleCenter.y - mCicleCenterPoint.y);
        double atan = Math.atan(tan);  //得到角度
        mPointA.x = (float) (mCicleCenterPoint.x + Math.cos(atan) * mRadius);
        mPointA.y = (float) (mCicleCenterPoint.y - Math.sin(atan) * mRadius);
        mPointB.x = (float) (mCicleCenterPoint.x - Math.cos(atan) * mRadius);
        mPointB.y = (float) (mCicleCenterPoint.y + Math.sin(atan) * mRadius);


        mPointC.x = (float) (mDragCicleCenter.x + Math.cos(atan) * mDragRadius);
        mPointC.y = (float) (mDragCicleCenter.y - Math.sin(atan) * mDragRadius);
        mPointD.x = (float) (mDragCicleCenter.x - Math.cos(atan) * mDragRadius);
        mPointD.y = (float) (mDragCicleCenter.y + Math.sin(atan) * mDragRadius);

        invalidate();
    }


    public int getStatusBarHeight() {//获取状态栏高度
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }


    public interface onDragBallListener {
        void onDisappear(DragBall dragBall, float x, float y);
        void onReset();
    }

    private onDragBallListener monDragBallListener;

    public void setonDragBallListener(onDragBallListener onDragBallListener) {
        monDragBallListener = onDragBallListener;
    }

    public int dp2px(Context context,int dp){

        return (int) (context.getResources().getDisplayMetrics().density * dp);
    }
}

效果这里说一下怎么把它加到listView的item中,你会发现我们把它弄在布局文件是不好的,反正我没搞成功.其实QQ应该(我说的是应该)也是先在布局那布局一个和这个差不多的textView,然后当我们触摸它的时候,通过touch事件的ActionDown获得那个位置,接着再通过WindowManger添加我们这个view去那个位置,然后把textView隐藏掉,并把touch事件传递给我们的view处理,这样就能满屏幕拖动了.

效果

public class MainActivity extends Activity {

    private DragBall mDragBall;
    private WindowManager mWindowManager;
    private WindowManager.LayoutParams mParams;
    public static Handler mHandler = new Handler();
    private float mDensity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.item_list);


      mDensity = getResources().getDisplayMetrics().density;
        final TextView msg_point = (TextView) findViewById(R.id.msg_point);


        mDragBall = new DragBall(this);

        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

        mParams = new WindowManager.LayoutParams();

        mParams.format = PixelFormat.TRANSLUCENT;         //很重要!!,这个属性一定要加,不加的话,除了你添加的View能看到,其它黑乎乎一片,使透明就是你想要的效果了


        msg_point.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                if (event.getAction() == MotionEvent.ACTION_DOWN) {


                    msg_point.getParent().requestDisallowInterceptTouchEvent(true);

                    msg_point.setVisibility(View.INVISIBLE);

                    mDragBall.setCenterPoint(event.getRawX(), event.getRawY());
                    mDragBall.setDragRang(150);
                    mDragBall.setNumber(50);

                    mWindowManager.addView(mDragBall, mParams);
                }

                mDragBall.onTouchEvent(event);
                return true;
            }
        });


        mDragBall.setonDragBallListener(new DragBall.onDragBallListener() {

        @Override
        public void onDisappear(DragBall dragBall, float x, float y) {


            if (mWindowManager != null && mDragBall.getParent() != null){
                mWindowManager.removeView(mDragBall);
            }

            ImageView imageView = new ImageView(MainActivity.this);
            imageView.setImageResource(R.drawable.frame_bao);                          //帧动画
            AnimationDrawable mAnimDrawable = (AnimationDrawable) imageView
                    .getDrawable();

            final BaozhaLayout baozhaLayout= new BaozhaLayout(MainActivity.this);    //测量帧动画的自定义控件
            baozhaLayout.setCenter((int) x, (int) (y - getStatusBarHeight()));

            baozhaLayout.addView(imageView, new FrameLayout.LayoutParams(
                    FrameLayout.LayoutParams.WRAP_CONTENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT));

            mWindowManager.addView(bubbleLayout, mParams);

            mAnimDrawable.start();

            // 播放结束后,移除帧动画
            mHandler.postDelayed(new Runnable() {

                @Override
                public void run() {
                    mWindowManager.removeView(baozhaLayout);
                }
            }, 600);
        }

        @Override
        public void onReset() {
            if (mWindowManager != null && mDragBall.getParent() != null){
                mWindowManager.removeView(mDragBall);
                msg_point.setVisibility(View.VISIBLE);
            }
        }
    }

        );
    }


    public int getStatusBarHeight() {//获取状态栏高度
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }


}

我们只得到了消息小球消失时的横纵坐标,直接放帧动画的话,会填充整个屏幕的,所以来看看里面怎么写的

public class BaozhaLayout extends FrameLayout {

    public BaozhaLayout(Context context) {
        super(context);
    }
    private int mCenterX, mCenterY;
    public void setCenter(int x, int y){
        mCenterX = x;
        mCenterY = y;

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        View child = getChildAt(0);
        // 设置View到指定位置
        if (child != null && child.getVisibility() != GONE) {
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();
            child.layout((int)(mCenterX - width / 2.0f), (int)(mCenterY - height / 2.0f)  //确定了帧动画的位置和宽高
                    , (int)(mCenterX + width / 2.0f), (int)(mCenterY + height / 2.0f));
        }
    }

其实挺简单的,对吧,怕不贴出来被打啊.经过了控件测量就不会填充屏幕了.嗯,差不多了,就这样吧,加上我说的,和那些注释应该好理解吧.有问题可以问我,有兴趣的可以下载下来看看,额,上传的代码资源还没有审核通过,通过后,我会把单独的这个和整合了侧拉菜单侧拉删除的下载链接发上来.
单独:http://download.youkuaiyun.com/detail/z8z87878/9559500
整合前面两个控件http://download.youkuaiyun.com/detail/z8z87878/9559501

<script type="text/javascript"> $(function () { $('pre.prettyprint code').each(function () { var lines = $(this).text().split('\n').length; var $numbering = $('<ul/>').addClass('pre-numbering').hide(); $(this).addClass('has-numbering').parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($('<li/>').text(i)); }; $numbering.fadeIn(1700); }); }); </script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值