美文网首页Android自定义View程序员
自定义布局(行星轨迹)

自定义布局(行星轨迹)

作者: 马也程序猴 | 来源:发表于2018-05-04 13:55 被阅读19次

    写在前头

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

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

    效果预期

    预期效果.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源码,有兴趣的朋友可以多多交流,感谢阅读。

    这也是第一次在简书发文章,挺喜欢简书里代码的排版,之后有空会把以前的文章修改修改搬到简书来。

    相关文章

      网友评论

        本文标题:自定义布局(行星轨迹)

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