Android onLayout()续

我站在巨人的肩膀上

       关于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();
}
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容