美文网首页
Android onLayout()续

Android onLayout()续

作者: 就爱烫卷发 | 来源:发表于2019-08-04 01:11 被阅读0次

    我站在巨人的肩膀上

           关于layout只是水平或者垂直摆放子控件的话好像根本没什么特别的东西,于是思考摆出一个圆形菜单,研究几天还是不太满意自己的结果,于是查阅了前人的写法果然受益匪浅。首先贴上鸿洋的博客:http://blog.csdn.net/lmj623565791/article/details/43131133

    正文

           刚开始设计的时候想的是如何做个圆形的菜单,继承ViewGroup 重写layout方法,按照位置来摆放就ok,计算一下弧度角度,问题应该都不大,初步实现的时候确实问题不是特别的大。

    1. 保证是一个圆形的View()
    2. 计算多少个Item之间的弧度差。
    3. 获取到子View的中心点,通过中心点以及宽高来判断自己的layout 位置。
      onLayout()代码:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int parentHeight = getMeasuredHeight();
        int parentWidth = getMeasuredWidth();
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();
            int length = Math.round(parentWidth / 2 - childWidth / 2);
            int left = parentWidth / 2 + (int) Math.round(length * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
                    * childWidth);
            int top = parentWidth
                    / 2
                    + (int) Math.round(length
                    * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
                    * childWidth);
            childView.layout(left, top, left + childWidth, top + childHeight);
            mStartAngle += mpadding;
    
        }
    }
    

           在onMeasure()中进行了圆形的设置简单的写法就是:

        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        width = height = Math.min(width, height);
        setMeasuredDimension(width, height);
    

           讲道理这里应该没有任何问题,根据前面的onlayout()第一篇,肯定难道不大,但是,我在使用的时候翻车了,当view写在ConstraintLayout中的时候,我只有一个View就是自定义的View,宽高写match_parent,然后通过onMeasure()来设置宽高相等的时候发现就怎么搞都不好使,还是占满了全屏。查阅资料之后,根布局换成LinearLayout(逃避的办法),又有说用使用0dp 表示 match_constraint 即可,换上之后 然而并没有什么用。此处埋点有坑。换成LinearLayout继续写。下面开始数学计算,讲解一下这个圆形的layout具体的摆放过程。首先圆点的问题这个很容易算,View的一半,第二算一下每个子view摆放的位置。一般来进行摆放我们可以搞两个同心圆,外面的圆就是这个View的大小,里面的圆的线路就是子View摆放的位置的中心。

    角度计算.png
           已知A,B 坐标,加AB半径,再通过每个之间的间距的弧度,直接可以推算出第二个点,第三个点......以及后面点的位置.好吧其实画这个图不是来算每个子项的位置的,而是为了后面滑动准备的。代码
     int left = parentWidth / 2 + (int) Math.round(length *    Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
                    * childWidth);
    

           这个其实是算距离子View 左边的位置,之前说过位置的摆放是左上右下的来的,这里先通过子View的圆心距离父View的圆心减去子View的半径,就可以算出来子View距离圆心的左边,以此推算出上面(Top),然后调用child.layout()方法进行摆放。这也就完成了初步的计算与布局。(这里后来又想了一下继承View 直接通过onDraw()来实现子菜单好像也是可以,这个只要算出子View的圆心一会也能直接画出来,但是从可塑性上来看,还是继承ViewGroup来比较好)。
           下面开始搞滑动。还是上面那个图,B点是我们按下的点,然后滑动到了C点,刚开始我计算的时候走进的误区就是通过B,C两点来计算偏移量(角度),那个复杂的,越算越怀疑人生,涉及到角度,涉及到象限,什么时候加,什么时候减,麻烦的一腿,后来看鸿洋的博客,直接顺了两个方法:

      private float getAngle(float xTouch, float yTouch)
        {
        double x = xTouch - (getMeasuredWidth() / 2d);
        double y = yTouch - (getMeasuredWidth() / 2d);
        return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
    }
    

    方法1,计算角度,所有的角度分为两块,都是跟圆心A点之间的角度来算角度差。
    方法2,算象限:

       private int getQuadrant(float x, float y)
      {
         int tmpX = (int) (x - getMeasuredWidth() / 2);
          int tmpY = (int) (y - getMeasuredWidth() / 2);
          if (tmpX >= 0)
         {
            return tmpY >= 0 ? 4 : 1;
        } else
        {
            return tmpY >= 0 ? 3 : 2;
        }
    
    }
    

    两个方法之后就是onMove()中的代码:

              float start = getAngle(lastX, lastY);
                /**
                 * 获得当前的角度
                 */
                float end = getAngle(x, y);
    
                // Log.e("TAG", "start = " + start + " , end =" + end);
                // 如果是一、四象限,则直接end-start,角度值都是正值
                if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4)
                {
                    mStartAngle += end - start;
               
                } else
                // 二、三象限,色角度值是付值
                {
                    mStartAngle += start - end;
                   
                }
                // 重新布局
                requestLayout();
    
                lastX = x;
                lastY = y;
    
                break;
    

           正常情况之前的View进行状态改变我们调用invalidate()方法,但是现在是 ViewGroup 咋可以换一个方法来整。当然后期还有其他的方法来做。原理其实很简单,我们不需要去确切的计算每一个子View的距离,只要保证我们的起点第一个子View的位置,然后通过View之间的角度来动态算出后面子View的位置。这样就能整出可以跟着手指滑动的菜单View了。

    背景没有的图.png
           到这里onLayout()就算是告一段落了,但是呢鸿洋大神还在这个View中加了惯性滑动,他的计算方法就是记录按下的时间与抬起的时间,计算出这个时间段走过的多少角度,然后给自己设定的一个值进行比较,再来算出后续惯性滑动的距离是多少。
    再次给链接:http://blog.csdn.net/lmj623565791/article/details/43131133。思路就是那个思路。至于后续添加子View,按键事件,都跟onLayout()一中一样。
    但是我这用的另外一个东西来进行处理。

    VelocityTracker

           这个是个速度检测器,使用起来也很简单。首先在down中进行注册,move的时候进行事件监听,up的时候看一下滑动了偏移量,这个是靠系统来计算的,然后自己定义一个标准让他来继续惯性就完事。
    使用代码Down:

          case MotionEvent.ACTION_DOWN:
                //初始化速度追踪
                if (velocityTracker == null) {
                    velocityTracker = VelocityTracker.obtain();
                } else {
                    velocityTracker.clear();
                }
    

    Move ---> velocityTracker.addMovement(event);
    Up

          velocityTracker.computeCurrentVelocity(1000);  此处单位为1S =1000ms
                velocity = velocityTracker.getYVelocity();//获取的是Y轴的单位偏移量,同样有获取X的方法
                float minVelocity =   ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity();//可以帮他当成一个对比的量
                if (Math.abs(velocity) > minVelocity) {//因为滑动有上下,即出现正负的情况。
                    continueScroll = true;
                    continueScroll();
                } else {
                    velocityTracker.recycle();
                    velocityTracker = null;
                }
    

    最后继续惯性滑动的代码:

      private void continueScroll() {
         new Thread(new Runnable() {
            @Override
            public void run() {
                float velocityAbs = 0;//速度绝对值
                if (velocity > 0 && continueScroll) {
                    velocity -= 300;
                    mStartAngle += 20;
                    velocityAbs = velocity;
                } else if (velocity < 0 && continueScroll) {
                    velocity += 300;
                    mStartAngle -= 20;
                    velocityAbs = velocity;
                }
    
                handler.sendEmptyMessage(0);
                if (continueScroll && Math.abs(velocityAbs) > 300) {
                    post(this);
                } else {
                    continueScroll = false;
                }
            }
        }).start();
    

           这里给出的300 是自己实际测试出来的单位数据,不是特别的严谨。但是可以完成比较合适的惯性效果,当然要是实现更复杂的惯性,比如越来越慢,最后来个类似弹性的东西,可以在这计算的时候花点时间。最后记得在主线程中调用requestLayout()
    最后未改善的代码:
    public class CircleView extends ViewGroup {

    String TAG = "CircleView";
    
    Context mContext;
    
    private double mStartAngle = 0;
    float lastX = 0;
    float lastY = 0 ;
    private double offsetAngle;
    
    int[] items;
    
    public CircleView(Context context) {
        this(context, null);
    }
    
    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
    }
    
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     //   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        Log.e(TAG, "onMeasure: "+width+"-->"+height );
        int size = Math.min(width, height);
        Log.e(TAG, "onMeasure: "+size);
        setMeasuredDimension(size, size);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    
        Log.d(TAG, "onMeasure:-- " + getMeasuredHeight() + "---->height" + getMinimumWidth());
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
        Log.d(TAG, "onLayout: "+mStartAngle);
        int parentHeight = getMeasuredHeight();
        int parentWidth = getMeasuredWidth();
    
    
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();
            int length = Math.round(parentWidth / 2 - childWidth / 2);
            int left = parentWidth / 2 + (int) Math.round(length * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
                    * childWidth);
            int top = parentWidth
                    / 2
                    + (int) Math.round(length
                    * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
                    * childWidth);
            childView.layout(left, top, left + childWidth, top + childHeight);
            mStartAngle += offsetAngle;
    
        }
    
    }
    public void bindView(int[] items) {
        this.items = items;
        offsetAngle = mStartAngle = 360 / items.length;
        int count = items.length;
        for (int i = 0; i < count; i++) {
            ImageView imageView = new ImageView(mContext);
            imageView.setImageResource(items[i]);
            this.addView(imageView);
        }
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x  = event.getX();
        float y =  event.getY();
    
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //初始化速度追踪
                if (velocityTracker == null) {
                    velocityTracker = VelocityTracker.obtain();
                } else {
                    velocityTracker.clear();
                }
                continueScroll = false;
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                velocityTracker.addMovement(event);
                Log.d(TAG, "onTouchEvent: "+"MOVE");
                /**
                 * 获得开始的角度
                 */
                float start = getAngle(lastX, lastY);
                /**
                 * 获得当前的角度
                 */
                float end = getAngle(x, y);
    
                // Log.e("TAG", "start = " + start + " , end =" + end);
                // 如果是一、四象限,则直接end-start,角度值都是正值
                if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4)
                {
                    mStartAngle += end - start;
                    // mTmpAngle += end - start;
                } else
                // 二、三象限,色角度值是付值
                {
                    mStartAngle += start - end;
                    //  mTmpAngle += start - end;
                }
                // 重新布局
                requestLayout();
    
    
                lastX = x;
                lastY = y;
    
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.computeCurrentVelocity(2000);
                velocity = velocityTracker.getYVelocity();
                float minVelocity = ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity();
                if (Math.abs(velocity) > minVelocity) {
                    continueScroll = true;
                    continueScroll();
                } else {
                    velocityTracker.recycle();
                    velocityTracker = null;
                }
                break;
        }
        return true;
    }
    
    
    
    private int getQuadrant(float x, float y)
    {
        int tmpX = (int) (x - getMeasuredWidth() / 2);
        int tmpY = (int) (y - getMeasuredWidth() / 2);
        if (tmpX >= 0)
        {
            return tmpY >= 0 ? 4 : 1;
        } else
        {
            return tmpY >= 0 ? 3 : 2;
        }
    
    }
    
    
    /**
     * 根据触摸的位置,计算角度
     *
     * @param xTouch
     * @param yTouch
     * @return
     */
    private float getAngle(float xTouch, float yTouch)
    {
        double x = xTouch - (getMeasuredWidth() / 2d);
        double y = yTouch - (getMeasuredWidth() / 2d);
        return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
    }
    
    
    private VelocityTracker velocityTracker;//速度监测
    private float velocity;//当前滑动速度
    private float a = 1000000;//加速度
    
    boolean continueScroll = false;
    
    
    
    
    Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            requestLayout();
            return false;
        }
    });
    
    private void continueScroll() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                float velocityAbs = 0;//速度绝对值
                if (velocity > 0 && continueScroll) {
                    velocity -= 300;
                    mStartAngle += 20;
                    velocityAbs = velocity;
                } else if (velocity < 0 && continueScroll) {
                    velocity += 300;
                    mStartAngle -= 20;
                    velocityAbs = velocity;
                }
    
                handler.sendEmptyMessage(0);
                if (continueScroll && Math.abs(velocityAbs) > 300) {
                    post(this);
                } else {
                    continueScroll = false;
                }
            }
        }).start();
    }
    }
    

    相关文章

      网友评论

          本文标题:Android onLayout()续

          本文链接:https://www.haomeiwen.com/subject/qalarctx.html