自定义百分比布局RelatvieLayout

PercentRelativeLayout.png

自定义布局系列之自定义RelativeLayout

众所周知 , Android碎片化非常严重 , 全球那么多机型 , 数以千计的分辨率 , 给我们的开发带来的不小的难度 , 那么机型都需要配置 , 谷歌也做了一些努力 , 退出了与分辨率无关的dp , sp等等单位 , 但这些单位在不同的机型上面还是显示各异 , 谷歌在15年的时候推出了百分比布局 , 可以在一定程度上 , 解决我们的布局差异问题 , 但却只有两种百分比布局 , PercentRelativeLayout 和 PercentFrameLayout 。

 下面我们就深究其理 , 写一个我们自己的百分比布局 。

在写自定义布局之前 , 我们首先要弄明白的一点是 , 怎样进行百分比布局 , 只有明白其中原理 , 代码就可以随之写出了 。我们知道 , View最重要的三个方法是onMeasure ,onLayout , onDraw 这三个方法决定了View的宽高,在什么位置 , 以及呈现的形态 , 这和我们绘画是一致的 , 都需要明确 , 画什么(确定宽高属性) , 在什么地方画 (确定绘画的位置), 怎样画(用什么颜色的画笔,尺子等)等问题 。

明确了上述问题 , 接下来就是分析我们的百分比布局 , 用到了哪些方法 。 我们的百分比布局 , 主要改变的是子控件的宽高,以及外边距的属性 , 也就是画什么 , 需要多宽多高 , 所以我们需要在onMeasure方法里面做工作 , 去改变子控件的属性值 。

我们都写过LayoutInflater.infalte()这个方法 , 将我们的xml文件布局inflate成一个View对象 , 我们在写自定义控件的时候 , 也都需要实现一个带属性参数的构造函数,PercentRelativeLayout(Context context, AttributeSet attrs) ,如果没有 , 则在xml中使用中会报错 。 在xml文件中写的View控件标签 , 最后都会inflate成一个View对象 ,而inflate里面 , 使用的pull解析 , 将xml标签解析成一个个View对象 。


    final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }

在解析View标签的时候 , 必然也会将标签的属性一并解析 , 并将标签属性设置到LayoutParams对象中 , 所以我们才可以通过子控件拿到布局属性 。


    // 拿到根布局的View
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

    ViewGroup.LayoutParams params = null;

    if (root != null) {
        if (DEBUG) {
            System.out.println("Creating params from root: " +
                    root);
        }
        // 创建一个布局参数对象
        params = root.generateLayoutParams(attrs);
        if (!attachToRoot) {
            // Set the layout params for temp if we are not
            // attaching. (If we are, we use addView, below)
            temp.setLayoutParams(params);
        }
    }

    /**
     * Returns a new set of layout parameters based on the supplied attributes set.
     *
     * @param attrs the attributes to build the layout parameters from
     *
     * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
     *         of its descendants
     */
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

知道了上面两点 , 我们就可以自定义一个我们自己的布局参数 , 通过generateLayoutParams() 返回我们的自定义布局参数对象 。之后我们就可以通过判断是否是我们自定义的布局参数 , 来进行子控件的宽高改变 。

首先我们将自定义的布局属性得到 , 并实例化自定义布局属性对象:

    
    /**
     * 自定义布局参数类 , 因为只是拓展RelativeLayout属性 , 所以继承自RelativeLayout.LayoutParams类
     * ,保留原有的RelativeLayout属性 。
     */
    static class PercentLayoutParams extends RelativeLayout.LayoutParams {

        public PercentLayoutInfo info = null;

        public PercentLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            /* 初始化布局自定义布局属性信息对象 */
            info = info != null ? info : new PercentLayoutInfo();

            /* 得到自定义布局属性值 */
            TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
            info.setPercentHeight(typedArray.getFloat(R.styleable.PercentLayout_layout_percentHeight, 0));
            info.setPercentWidth(typedArray.getFloat(R.styleable.PercentLayout_layout_percentWidth, 0));

            info.setPercentMarginOfWidth(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginOfWidth, 0));
            info.setPercentMarginOfHeight(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginOfHeight, 0));
            info.setPercentMarginLeft(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginLeft, 0));
            info.setPercentMarginTop(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginTop, 0));
            info.setPercentMarginRight(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginRight, 0));
            info.setPercentMarginBottom(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginBottom, 0));

            typedArray.recycle();
        }

    }

自己的LayoutParams类 , 是继承自RelativeLayout的 , 因为我们只是做扩展 , 所有保留原有的RelativeLayout的属性。接下来我们将PercentLayoutParams对象 ,作为返回值返给我们的generateLayoutParams方法 , 这样我们就可以在测量的时候 , 进行LayoutParams对象的判断了。

    
    /*
    * 生成布局参数对象
    * */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {

        /* 将返回的布局参数对象设置成自定义的参数对象 */
        return new PercentLayoutParams(getContext(), attrs);
    }

测量的时候 , 我们对LayoutParams对象进行判断 , 然后将我们自定义的属性 , 设置到子控件相应的属性 :


    /* 测量View的宽高 */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /* 测量RelativeLayout的宽高 */
        int viewGroupWidth = MeasureSpec.getSize(widthMeasureSpec);
        int viewGroupHeight = MeasureSpec.getSize(heightMeasureSpec);

        /* 得到RelativeLayout中的子控件个数 */
        int childCount = this.getChildCount();

        /* 循环取得子控件 */
        for (int i = 0; i < childCount; i++) {

            View child = this.getChildAt(i);
            /* 得到子控件的布局参数 */
            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();

            /* 判断LayoutParams对象是不是自定义的Params对象 */
            if (layoutParams instanceof PercentRelativeLayout.PercentLayoutParams) {

                /* 得到布局参数信息对象 */
                PercentLayoutInfo info = ((PercentLayoutParams) layoutParams).info;
                if (info != null) {
                    /* 得到自定义的宽高比 */
                    float percentWidth = info.getPercentWidth();
                    float percentHeight = info.getPercentHeight();

                    /* 如果宽高比大于0 ,则与父容器宽高进行计算 , 并将结果赋值给子控件 */
                    if (percentHeight > 0) {
                        layoutParams.height = (int) (percentHeight * viewGroupHeight);
                    }

                    if (percentWidth > 0) {
                        layoutParams.width = (int) (percentWidth * viewGroupWidth);
                    }

                    /* 设置外边距百分比 */
                    setLayoutPercentMargin(layoutParams, info, viewGroupWidth, viewGroupHeight);

                }

            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

设置外边距百分比:


    /**
     * 设置子控件外边距百分比
     * @param layoutParams 子控件的布局参数
     * @param info  自定义属性对象
     * @param parentWidth 父容器的宽度
     * @param parentHeight 父容器的高度
     */
    private void setLayoutPercentMargin(ViewGroup.LayoutParams layoutParams, PercentLayoutInfo info, int parentWidth, int parentHeight) {
        /* 得到自定义属性的值 */
        float percentMarginOfWidth = info.getPercentMarginOfWidth();
        float percentMarginOfHeight = info.getPercentMarginOfHeight();
        float percentMarginLeft = info.getPercentMarginLeft();
        float percentMarginTop = info.getPercentMarginTop();
        float percentMarginRight = info.getPercentMarginRight();
        float percentMarginBottom = info.getPercentMarginBottom();

        /* 判断子控件是否设置了外边距的参数 */
        if (layoutParams instanceof MarginLayoutParams) {

            if (percentMarginOfWidth > 0) {
                setLayoutMarginParams(layoutParams, percentMarginOfWidth, parentWidth);
            }

            if (percentMarginOfHeight > 0 ) {
                setLayoutMarginParams(layoutParams, percentMarginOfHeight, parentHeight);
            }

            if (percentMarginLeft > 0) {
                ((MarginLayoutParams) layoutParams).leftMargin = (int) (percentMarginLeft * parentWidth);
            }

            if (percentMarginTop > 0) {
                ((MarginLayoutParams) layoutParams).topMargin = (int) (percentMarginTop * parentHeight);
            }

            if (percentMarginRight > 0) {
                ((MarginLayoutParams) layoutParams).rightMargin = (int) (percentMarginRight * parentWidth);
            }

            if (percentMarginBottom > 0) {
                ((MarginLayoutParams) layoutParams).bottomMargin = (int) (percentMarginBottom * parentHeight);
            }
        }

    }

    /**
     * 设置子控件的外边距 , 根据父容器的某个宽度和高度的百分比
     * @param layoutParams 子控件的布局参数对象
     * @param percent 自定义属性百分比
     * @param parent 父容器的宽度或高度
     */
    private void setLayoutMarginParams(ViewGroup.LayoutParams layoutParams, float percent, int parent) {
        ((MarginLayoutParams) layoutParams).leftMargin = (int) (percent * parent);
        ((MarginLayoutParams) layoutParams).topMargin = (int) (percent * parent);
        ((MarginLayoutParams) layoutParams).rightMargin = (int) (percent * parent);
        ((MarginLayoutParams) layoutParams).bottomMargin = (int) (percent * parent);
    }

控件属性XML:


    <declare-styleable name="PercentLayout">
        <!-- 宽高比 -->
        <attr name="layout_percentWidth" format="float"/>
        <attr name="layout_percentHeight" format="float"></attr>

        <!-- 外边距百分比 -->

        <!-- 按照父容器的宽度来设置子控件的四个外边距的百分比 -->
        <attr name="layout_percentMarginOfWidth" format="float"></attr>
        <!-- 按照父容器的高度来设置子控件的四个外边距的百分比 -->
        <attr name="layout_percentMarginOfHeight" format="float"></attr>

        <!-- 四个外边距的百分比 -->
        <attr name="layout_percentMarginLeft" format="float"></attr>
        <attr name="layout_percentMarginRight" format="float"></attr>
        <attr name="layout_percentMarginTop" format="float"></attr>
        <attr name="layout_percentMarginBottom" format="float"></attr>

    </declare-styleable>

自定义RelativeLayout就到这里 , 其他的百分比布局 , 原理都是类似的 , 这里就不赘述了 , 如果写多个百分比布局 , 可以将那些属性处理 , 属性设置 , 抽取一个帮助类出来 。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,004评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,389评论 0 17
  • 在上一篇文章里,我总结了一下自定义控件需要了解的基础知识:View的绘制流程——《自定义控件知识储备-View的绘...
    蘑菇君的小小世界阅读 9,382评论 15 94
  • 近日,发现曾经上过的培训,没有好好上,反而对现在的项目有很大的帮助。 后来好好学的,反而说不上很有用。 可是那时候...
    SandmanLi阅读 207评论 0 0
  • 其实我一直知道的 我相信铁树开花 但从不瞥过一眼 我猜不透火山缄默 却相信就在明天 远行路上的茶寮 坐歇 续程 长...
    缺耳阅读 246评论 0 10