自定义布局(行星轨迹)

写在前头

关于自定义控件的内容不在这片文章里讨论,之前有写过关于自定义控件的内容,感兴趣的朋友可以了解一下。自定义控件

最近看着自己项目里的首页,总感觉实现的方式太暴力了,看着不太舒服,而且拓展性也不高,所以就决定把首页封装成一个布局,话不多说,立马动手。

效果预期

预期效果.png

简单的分析一下这个目标效果,最直观的首先是两个圆形,并且外侧的圆心在内侧圆的边上,两个圆有不同的内容并且内容不同,那么整理一下自定义布局的几点需求。

  • 包含两个大布局
  • 两布局大小需要适配布局
  • 两布局layout有所关联
  • 布局内容在圆形以内
  • 外侧圆的移动轨迹是内侧圆的边

初步构建

首先目标是是自定义布局,所以创建一个ViewGroup的子类。接下来要做的就是根据主要需求写代码了。

1.包含两大布局

关于自定义布局中能够放置多少子控件,考虑到这次布局的特点,我选择把子控件数量设定在2个,不能多也不能少,当然如果在实际使用中不合适的话,还需要再做调整。
对子控件的数量判断我选在onLayout()中进行,onMeasure()的调用太频繁,onDraw()的调用对于判断来说又有些晚了。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (getChildCount() != 2){
        try {
            throw new Exception("SatelliteLayout中需要包含两个子控件");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.子控件大小适配

当SatelliteLayout的大小确定后,需要根据它的大小获取子布局的大小。虽然对于布局来说更重要的是根据子布局大小来确定自身大小,但我认为这个更能对于SatelliteLayout来说同样重要,所以我设定了一个权重值ballWeight,通过它来确定内外侧子布局在SatelliteLayout大小固定时对自身大小的测量规则。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    //获取SatelliteLayout宽高度
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(widthMeasureSpec);

    int size = widthSize >= heightSize ? heightSize : widthSize;

    float baseWeight = 1 / (ballWeight + 1);
    Log.e("基础权重", "" + baseWeight);

    sRadius = size / 2 * baseWeight;
    bRadius = size / 2 * baseWeight * ballWeight;

    Log.e("大圆半径", "" + bRadius);
    Log.e("小圆半径", "" + sRadius);

    //计算出内侧圆形位置
    bCenterPoint.set(bRadius, bRadius);
    bCenterPoint.offset(sRadius, sRadius);
}

在SatelliteLayout中,内侧圆的位置是静止的,所以获取到内侧圆圆形的位置是测量大小以及布局内外侧两圆的关键点。在拥有了圆形位置,半径大小,剩下的就是外侧圆的偏移角度,有了以上的要素,布局SatelliteLayout可以说是手到擒来了。

3.两布局的layout有所关联,外侧圆的移动轨迹,布局内容在圆形以内

因为这三点都与layout()关系密切,所以把以上两点放在一起研究。在测量出内侧圆的圆心位置以及内外圆半径以后,剩下的需求其实就是中学数学的知识了,没错,就是三角函数ヾ(o・ω・)ノ

那么大家先来看看我画的简图


SatelliteLayout示意图.png

我画了好久才成这样,实在是比敲代码还难...

黑色的圆指代内侧圆,蓝色的指代外侧圆,在内侧圆圆形确定了的情况下,外侧圆的圆形位置(x,y值)就可以根据外侧圆的偏移角度计算出来,以图片为例:

内侧圆心x坐标:x(内)
内侧圆半径:r(内)
外侧圆半径:r(外)

外侧圆y坐标:r(外) + r(内) - x(内) * cos(α)
外侧圆x坐标:r(内) + x(内) * sin(α)

布局根据左上右下位置的不同,在圆心x,y坐标的基础上对自身的半径做加减处理,布局的位置就能够确定出来了,将角度设置为变量,改变角度就能够改变外侧圆的位置。

布局内容在圆形以内
关于这一点,在圆内取的布局内容必须完全包裹在园中,并且布局边界应该规则整齐,因此在取最大的范围则在圆内按45°,135°,225°,315°这4个角度上的接触点为布局内容的边界点绘制出一个正方形,作为内容的放置区域。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (getChildCount() != 2){
        try {
            throw new Exception("SatelliteLayout中需要包含两个子控件");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return;
    }

    View centerLayout = getChildAt(0);
    View sideLayout = getChildAt(1);

    //圆内最大布局,布局与圆边界接触点角度为45°,获取一个正方形的布局
    if (centerLayout != null) {
        int left = (int) (sRadius + bRadius - (bRadius * Math.sin(45 * Math.PI / 180)));
        int right = (int) (sRadius + bRadius + Math.sin(45 * Math.PI / 180) * bRadius);

        centerLayout.layout(left, left, right, right);
    }

    if (sideLayout != null) {
        int left = (int) (sRadius + bRadius + bRadius * Math.sin(angle * Math.PI / 180) - (sRadius * Math.sin(45 * Math.PI / 180)));
        int top = (int) (sRadius + bRadius - (bRadius * Math.cos(angle * Math.PI / 180) + (sRadius * Math.sin(45 * Math.PI / 180))));
        int right = (int) (left + 2 * (sRadius * Math.sin(45 * Math.PI / 180)));
        int bottom = (int) (top + 2 * (sRadius * Math.sin(45 * Math.PI / 180)));

        sideLayout.layout(left, top, right, bottom);
    }
}

背景着色

至此,SatelliteLayout的主要内容已经完成,剩余的就是一些简单的颜色绘制之类,在onDraw()中绘制一些指定的内容,这部分比较简单,就直接上代码了。

@Override
protected void onDraw(Canvas canvas) {
    bPaint.setStyle(Paint.Style.FILL);
    bPaint.setColor(bBallColor);

    lPaint.setStyle(Paint.Style.FILL);
    lPaint.setColor(sBallColor);

    canvas.drawCircle(bCenterPoint.x, bCenterPoint.y, bRadius, bPaint);

    float xOffset = (float) (Math.sin(angle * Math.PI / 180) * bRadius);
    Log.e("X轴偏移", "xOffset:" + xOffset + "--X:" + (bCenterPoint.x + xOffset));
    float yOffset = (float) (Math.cos(angle * Math.PI / 180) * bRadius);
    Log.e("Y轴偏移", "yOffset:" + yOffset + "--Y:" + (bCenterPoint.y - yOffset));

    canvas.drawCircle(bCenterPoint.x + xOffset, bCenterPoint.y - yOffset, sRadius, lPaint);
}

总结

SatelliteLayout目前的功能比较简单,可以说主要的难度在于计算而不是写代码( • ̀ω•́ )✧,当然,编码的重点还是在于思路,这个控件的代码我已经上传到github上了,SatelliteLayout的Github源码,有兴趣的朋友可以多多交流,感谢阅读。

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

推荐阅读更多精彩内容