MaterialDesign-LinearLayoutCompat探究及源码分析

简述

谷歌Material Design推出了许多非常好用的兼容性控件,尤其是在appcompat-V7里面有很多为兼容而生的控件,这样就可以做到高低版本和不同的ROM之间体验一致!还可以配合appcompat的主题使用达到体验一致性。例如:

1、android.support.v7.app.AlertDialog

2、进度条样式设置 style="@style/Widget.AppCompat.ProgressBar.Horizontal"

3、SwipeRefreshLayout下拉刷新

4、PopupWindow、ListPopupWindow、PopupMenu、Button、EditText等等

5、android.support.v7.widget.LinearLayoutCompat

这里主要来探究一下LinearLayoutCompat:

以前要在LinearLayout布局之间的子View之间添加分割线,还需要自己去自定义控件进行添加或者就是在子View之间写很多个分割线View;LinearLayoutCompat的出现轻松解决了LinearLayout添加分割线的问题,我们从以下两个方面来对LinearLayoutCompat进行介绍:

1、LinearLayoutCompat的使用

2、 LinearLayoutCompat的源码分析

LinearLayoutCompat的使用

LinearLayoutCompat位于support-v7包中,LinearLayoutCompat其实就是LinerLayout组件的升级,为了兼容低版本,使用前提:

1、需要引入 compile 'com.android.support:appcompat-v7:26.1.0'

2、使用LinearLayoutCompat需要自定义命名空间xmlns:app=”http://schemas.android.com/apk/res-auto”

这样就可以使用如下LinearLayoutCompat特有的功能了:

app:divider=”@drawable/line”给分隔线设置自定义的drawable,这里你需要在drawable在定义shape资源,否则将没有效果。

app:dividerPadding 给分隔线设置距离左右边距的距离。

app:showDividers="beginning|middle|end"属性。

beginning,middle,end属性值分别指明将在何处添加分割线。

beginning表示从该LinearLayoutCompat布局的最顶一个子view的顶部开始。

middle表示在此LinearLayoutCompat布局内的子view之间添加。

end表示在此LinearLayoutCompat最后一个子view的底部添加分割线。

none表示不设置间隔线。

示例代码:


    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:background="@color/white"

    android:orientation="vertical"

    app:showDividers="middle"

    app:divider="@drawable/line_diver_gray"

    app:dividerPadding="15dp">


        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text="收藏"

        android:padding="15dp"/>


        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text="相册"

        android:padding="15dp"/>


        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text="卡包"

        android:padding="15dp"/>


        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text="设置"

        android:padding="15dp"/>

line_diver_gray.xml:


    android:shape="rectangle">

   

   


效果图如下:


LinearLayoutCompat的源码分析

看源码需要有目的去看,分析实现的原理:LinearLayoutCompat是如何做到给里面的所有的child之间添加间隔线的?

观看源码,首先可以知道 LinearLayoutCompat继承了ViewGroup,我们知道View的绘制会经过三个方法:

1、onMearsue(测量自身和里面的所有子控件)

2、onLayout(摆放里面所有的子控件),

3、onDraw(绘制)

猜想:

1、mearsuredWidth,mearsuredHeight会变大(加上分割线)

2、摆放子控件位置会有一定的体现(childView: left/top/right/bottom)

3、onDraw绘制的时候也会有体现(childView: left/top/right/bottom)

1、首先我们查看它的构造函数:

1、从构造函数中,首先会把LinearLayoutCompat的所有风格属性的值保存到一个TintTypedArray数组中,然后从中取出用户给LinearLayoutCompat设置的orientation, gravity,baselineAligned的值,如果这些值存在,就给LinearLayoutCompat设置这些值。

2、当然还会从TintTypedArray中取出weightSum,baselineAlignedChildIndex,measureWithLargestChild等属性

3、setDividerDrawable方法设置分割线的Drawable,非常明显和分割线有关系

4、接着是从TintTypedArray中继续获取mShowDividers和mDividerPadding的值,分别用于判断显示分割线的模式和分割线的Padding值为多少。

public LinearLayoutCompat(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context, attrs, defStyleAttr);

    final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,

            R.styleable.LinearLayoutCompat, defStyleAttr, 0);

    int index = a.getInt(R.styleable.LinearLayoutCompat_android_orientation, -1);

    if (index >= 0) {

        setOrientation(index);

    }

    index = a.getInt(R.styleable.LinearLayoutCompat_android_gravity, -1);

    if (index >= 0) {

        setGravity(index);

    }

    boolean baselineAligned = a.getBoolean(R.styleable.LinearLayoutCompat_android_baselineAligned, true);

    if (!baselineAligned) {

        setBaselineAligned(baselineAligned);

    }

    mWeightSum = a.getFloat(R.styleable.LinearLayoutCompat_android_weightSum, -1.0f);

    mBaselineAlignedChildIndex =

            a.getInt(R.styleable.LinearLayoutCompat_android_baselineAlignedChildIndex, -1);

    mUseLargestChild = a.getBoolean(R.styleable.LinearLayoutCompat_measureWithLargestChild, false);

    setDividerDrawable(a.getDrawable(R.styleable.LinearLayoutCompat_divider));

    mShowDividers = a.getInt(R.styleable.LinearLayoutCompat_showDividers, SHOW_DIVIDER_NONE);

    mDividerPadding = a.getDimensionPixelSize(R.styleable.LinearLayoutCompat_dividerPadding, 0);

    a.recycle();

}

我们查看setDividerDrawable方法的内部实现:

1、可以看到,该方法中传进来一个Drawable,然后会进行if判断,是否和原有的Drawable相等,如果为true则return,不执行下面的语句,如果不是,则将该Drawable设置给全局的mDivider,

2、又是if判断,如果传进来的divider!= null,则获取它的固有宽高并设置给mDivider,否则mDivider的宽高设为0,然后会执行setWillNotDraw和requestLayout方法

public void setDividerDrawable(Drawable divider) {

    if (divider == mDivider) {

        return;

    }

    mDivider = divider;

    if (divider != null) {

        mDividerWidth = divider.getIntrinsicWidth();

        mDividerHeight = divider.getIntrinsicHeight();

    } else {

        mDividerWidth = 0;

        mDividerHeight = 0;

    }

    setWillNotDraw(divider == null);

    requestLayout();

}

2、查看分析View的绘制会经过三个方法onMeasure、onLayout、onDraw

下面我们就查看一下这几个方法的源码进行分析,看看分割线是如何进行绘制的。

1、首先查看一下onMeasure方法:

内部就是根据Orientation的不同,调用不同的方法:

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    if (mOrientation == VERTICAL) {

        measureVertical(widthMeasureSpec, heightMeasureSpec);

    } else {

        measureHorizontal(widthMeasureSpec, heightMeasureSpec);

    }

}

onMeasure分为了水平和竖直的情况,我们这次以竖直情况为例分析。我们猜想可以知道,在测量的时候,肯定加了分隔线的高度(只看核心代码):

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

for (int i = 0; i < count; ++i) {

final View child = getVirtualChildAt(i);

//如果有分隔线,那么测量的时候就加上分割线的Drawable的高度

if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {

mTotalLength += mDividerHeight;

}

}

}

--measureVertical方法最后是通过setMeasuredDimension方法对测量的值进行设置的;

--至于 maxWidth的值在源码的前面有相应的判断进行赋值;

--所以整个measure的方法基本围绕maxWidth和mTotalLength值的确定展开的;

--其中如果hasDividerBeforeChildAt返回的值为true,mTotalLength会加上分割线的高度;

--最后通过setMeasuredDimension赋值。

2、其次查看一下onLayout方法:

@Override

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

    if (mOrientation == VERTICAL) {

        layoutVertical(l, t, r, b);

    } else {

        layoutHorizontal(l, t, r, b);

    }

}

看一下layoutVertical的逻辑,里面基本围绕以下两个值展开的:

int childTop;

int childLeft;

循环遍历子View,根据不同的gravity对childLeft和childTop进行赋值,如果存在分割线childTop会加上分割线的高度mDividerHeight,最后是通过setChildFrame方法进行layout的完成的

for (int i = 0; i < count; i++) {

    final View child = getVirtualChildAt(i);

    if (child == null) {

        childTop += measureNullChild(i);

    } else if (child.getVisibility() != GONE) {

        final int childWidth = child.getMeasuredWidth();

        final int childHeight = child.getMeasuredHeight();

        final LinearLayoutCompat.LayoutParams lp =

                (LinearLayoutCompat.LayoutParams) child.getLayoutParams();

        int gravity = lp.gravity;

        if (gravity < 0) {

            gravity = minorGravity;

        }

        final int layoutDirection = ViewCompat.getLayoutDirection(this);

        final int absoluteGravity = GravityCompat.getAbsoluteGravity(gravity,

                layoutDirection);

        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {

            case Gravity.CENTER_HORIZONTAL:

                childLeft = paddingLeft + ((childSpace - childWidth) / 2)

                        + lp.leftMargin - lp.rightMargin;

                break;

            case Gravity.RIGHT:

                childLeft = childRight - childWidth - lp.rightMargin;

                break;

            case Gravity.LEFT:

            default:

                childLeft = paddingLeft + lp.leftMargin;

                break;

        }

    if (hasDividerBeforeChildAt(i)) {

            childTop += mDividerHeight;

        }

        childTop += lp.topMargin;

        setChildFrame(child, childLeft, childTop + getLocationOffset(child),

                childWidth, childHeight);

        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

        i += getChildrenSkipCount(child, i);

    }

}

3、最后看一下onDraw方法:

onDraw方法内部逻辑是,判断mDivider是否为空,然后是根据mOrientation的属性,来调用不同的方法进行横或者竖的分割线绘制。

@Override

protected void onDraw(Canvas canvas) {

    if (mDivider == null) {

        return;

    }

    if (mOrientation == VERTICAL) {

        drawDividersVertical(canvas);

    } else {

        drawDividersHorizontal(canvas);

    }

}

#查看drawDividersVertical方法内部:

1、循环遍历所有子孩子,进行是否为空和是否为不可见的判断;

2、然后调用hasDividerBeforeChildAt(i),如果为true,则通过获取child的LayoutParams进行计算;

3、然后就可以计算出分割线的top距离;

4、然后调用drawHorizontalDivider(canvas,top)方法。

void drawDividersVertical(Canvas canvas) {

    final int count = getVirtualChildCount();

    for (int i = 0; i < count; i++) {

        final View child = getVirtualChildAt(i);

        if (child != null && child.getVisibility() != GONE) {

            if (hasDividerBeforeChildAt(i)) {

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int top = child.getTop() - lp.topMargin - mDividerHeight;

                drawHorizontalDivider(canvas, top);

            }

        }

    }

    if (hasDividerBeforeChildAt(count)) {

        final View child = getVirtualChildAt(count - 1);

        int bottom = 0;

        if (child == null) {

            bottom = getHeight() - getPaddingBottom() - mDividerHeight;

        } else {

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            bottom = child.getBottom() + lp.bottomMargin;

        }

        drawHorizontalDivider(canvas, bottom);

    }

}

#查看一下hasDividerBeforeChildAt方法的内部逻辑:

1、基本就是根据子孩子的位置进行相应的判断,第一个位置,最后一个位置,还有中间所有位置,返回一个boolean值;

2、会根据这个值来判断是否画分割线;

3、然后回到drawDividersVertical方法中,它会在遍历子View的;

4、最后调用drawHorizontalDivider方法。

protected boolean hasDividerBeforeChildAt(int childIndex) {

    if (childIndex == 0) {

        return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;

    } else if (childIndex == getChildCount()) {

        return (mShowDividers & SHOW_DIVIDER_END) != 0;

    } else if ((mShowDividers & SHOW_DIVIDER_MIDDLE) != 0) {

        boolean hasVisibleViewBefore = false;

        for (int i = childIndex - 1; i >= 0; i--) {

            if (getChildAt(i).getVisibility() != GONE) {

                hasVisibleViewBefore = true;

                break;

            }

        }

        return hasVisibleViewBefore;

    }

    return false;

}

#查看一下drawHorizontalDivider方法:

分割线是如何绘制上去的:

1、发现分割线其实是通过Drawable的setBounds方法进行设置的,

2、然后会调用Drawable的draw方法对分割线进行绘制。

3、drawDividersHorizontal方法的逻辑跟drawDividersVertical方法差不多,它最后调用的是drawVerticalDivider方法。

void drawHorizontalDivider(Canvas canvas, int top) {

    mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,

            getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);

    mDivider.draw(canvas);

}

为什么要看分割线绘制的源码,因为在很多控件中并没有分割线,我们可以通过学习谷歌的源码,仿照着进行分割线的绘制,比如recyclerView就没有分割线,但我们可以自己写一个分割线,对于recyclerView分割线设置

csdn地址:https://blog.csdn.net/hylxnq/article/details/80184112

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

推荐阅读更多精彩内容