热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MaterialDesign系列探究之LinearLayoutCompat

谷歌MaterialDesign推出了许多非常好用的控件,所以我决定写一个专题来讲述MaterialDesign,今天带来MaterialDesign系列的第一弹LinearLa

谷歌Material Design推出了许多非常好用的控件,所以我决定写一个专题来讲述MaterialDesign,今天带来Material Design系列的第一弹 LinearLayoutCompat。

以前要在LinearLayout布局之间的子View之间添加分割线,还需要自己去自定义控件进行添加或者就是在子View之间写很多个TextView,但是谷歌已经给我们提供了这样一个组件,可以很轻松的解决分割线的问题,妈妈再也不用担心分割线问题啦,这个组件就是Material Design中的 LinearLayoutCompat。本篇博客将会从以下两个方面来对LinearLayoutCompat进行介绍:

1.    LinearLayoutCompat的使用

2.    LinearLayoutCompat的源码分析

 

LinearLayoutCompat的使用

LinearLayoutCompat位于support-v7包中,LinearLayoutCompat其实就是LinerLayout组件,只是为了兼容低版本,所以你必须的引用 V7包下面的LinearLayoutCompat。 LinearLayoutCompat除了拥有LinerLayout原本的属性之外,主要有如下几种属性来实现间隔线效果。

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

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表示不设置间隔线。


使用LinearLayoutCompat可以很方便的就做出微信的发现界面:


布局的代码如下:



    

    

        

            

            
        

        

            

            
        

        

            

            
        

        

            

            
        

        

            

            
        

        

            

            
        

        

            

            
        
    

当然和真正微信里的界面还是不一样的,还需要处理很多细节,这里就不过分纠结于细节了,主要还是了解LinearLayoutCompat的用法。


LinearLayoutCompat的源码分析

在使用完LinearLayoutCompat之后,我们会很好奇它内部是如何实现添加分割线的,那我们就看一下LinearLayoutCompat的源码进行分析。


1.  观看源码,首先可以知道 LinearLayoutCompat继承了ViewGroup,然后我们查看它的构造函数

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();
    }

从构造函数中,首先会把LinearLayoutCompat的所有风格属性的值保存到一个TintTypedArray数组中,然后从中取出用户给LinearLayoutCompat设置的orientation, gravity,baselineAligned的值,如果这些值存在,就给LinearLayoutCompat设置这些值。当然还会从TintTypedArray中取出weightSum,baselineAlignedChildIndex,measureWithLargestChild等属性,然后在构造函数的最低部,会发现这一段代码:

 	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);

可以发现setDividerDrawable方法,看名字意思是设置分割线的Drawable,非常明显和分割线有关系,接着是从TintTypedArray中继续获取mShowDividers和mDividerPadding的值,分别用于判断显示分割线的模式和分割线的Padding值为多少。我们查看setDividerDrawable方法的内部实现:

 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();
    }

可以看到,该方法中传进来一个Drawable,然后会进行if判断,是否和原有的Drawable相等,如果为true则return,不执行下面的语句,如果不是,则将该Drawable设置给全局的mDivider,又是if判断,如果传进来的divider!= null,则获取它的固有宽高并设置给mDivider,否则mDivider的宽高设为0,然后会执行setWillNotDraw和requestLayout方法。

我们都知道每一个ViewGroup都会拥有onDraw,onLayout和onMeasure方法,下面我们就查看一下这几个方法的源码进行分析,看看分割线是如何进行绘制的。从源码往下看,首先会看到onDraw方法。

 protected void onDraw(Canvas canvas) {
        if (mDivider == null) {
            return;
        }

        if (mOrientation == VERTICAL) {
            drawDividersVertical(canvas);
        } else {
            drawDividersHorizontal(canvas);
        }
    }

onDraw方法内部逻辑很简单,判断mDivider是否为空,然后是根据mOrientation的属性,来调用不同的方法进行横或者竖的分割线绘制。查看drawDividersVertical方法内部:
void drawDividersVertical(Canvas canvas) {
        final int count = getVirtualChildCount();
        for (int i = 0; i  
 

循环遍历所有子孩子,进行是否为空和是否为不可见的判断,然后调用hasDividerBeforeChildAt(i),如果为true,则通过获取child的LayoutParams进行计算,然后就可以计算出分割线的top距离,然后调用drawHorizontalDivider(canvas,top)方法,查看一下hasDividerBeforeChildAt方法的内部逻辑:

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;
    }
基本就是根据子孩子的位置进行相应的判断,第一个位置,最后一个位置,还有中间所有位置,返回一个boolean值,会根据这个值来判断是否画分割线。然后回到drawDividersVertical方法中,它会在遍历子View的最后调用drawHorizontalDivider方法,查看一下这个方法:

void drawHorizontalDivider(Canvas canvas, int top) {
        mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
                getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
        mDivider.draw(canvas);
    }
发现分割线其实是通过Drawable的setBounds方法进行设置的,然后会调用 Drawable的draw方法对分割线进行绘制。drawDividersHorizontal方法的逻辑跟drawDividersVertical方法差不多,它最后调用的是drawVerticalDivider方法。
 void drawDividersHorizontal(Canvas canvas) {
        final int count = getVirtualChildCount();
        final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this);
        for (int i = 0; i 然后我们查看一下onMeasure方法,内部就是根据Orientation的不同,调用不同的方法: 
 

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

查看measureVertical方法,内容较多,我们一点点分析,下面这段代码会循环遍历所有的子View,然后做出相应的判断,如果hasDividerBeforeChildAt方法返回true,mTotalLength会加上分割线的高度,这个方法我们前面已经看过他内部的逻辑,然后会获取子view的LayoutParams,totalWeight用于记录Weight的总和

for (int i = 0; i  
 

接下来会对heightMode进行判断,跟MeasureSpec.EXACTLY等属性进行比较,还会判断是否使用了权重,根据heightMode的值不同会有不同的处理方式,mTotalLength的值的处理是不同的,同时如果不满足if语句的条件,会调用 measureChildBeforeLayout方法进行一次测量:

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // Optimization: don't bother measuring children who are going to use
                // leftover space. These views will get measured again down below if
                // there is any leftover space.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                int oldHeight = Integer.MIN_VALUE;

                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                measureChildBeforeLayout(
                        child, i, widthMeasureSpec, 0, heightMeasureSpec,
                        totalWeight == 0 ? mTotalLength : 0);

                if (oldHeight != Integer.MIN_VALUE) {
                    lp.height = oldHeight;
                }

                final int childHeight = child.getMeasuredHeight();
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                        lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }


 
 

void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }


 
 

当heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED时,mTotalLength值的计算方式是不同的

 if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i  
 
到最后有下面一段代码:
  	maxWidth += getPaddingLeft() + getPaddingRight();
	
        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(ViewCompat.resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
measureVertical方法最后是通过setMeasuredDimension方法对测量的值进行设置的,至于 maxWidth的值在源码的前面有相应的判断进行赋值,所以整个measure的方法基本围绕 maxWidth和mTotalLength值的确定展开的,其中如果hasDividerBeforeChildAt返回的值为true,mTotalLength会加上分割线的高度,最后通过setMeasuredDimension赋值。

最后我们看看onLayout方法

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;
for (int i = 0; i 循环遍历子View,根据不同的gravity对childLeft和childTop进行赋值,如果存在分割线childTop会加上分割线的高度mDividerHeight,最后是通过setChildFrame方法进行layout的完成的,可以查看这个方法内部,调用了child的layout方法 
 
 private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }
到这里,所有的LinearLayoutCompat的源码分析,就结束了,为什么要看分割线绘制的源码,因为在很多控件中并没有分割线,我们可以通过学习谷歌的源码,仿照着进行分割线的绘制,比如recyclerView就没有分割线,但我们可以自己写一个分割线,对于 recyclerView分割线设置,有很多大神的博客都有描述,这里就不在赘述了,以后的博文会陆续给大家带来Material Design其他控件的博客。










推荐阅读
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 在一对一直播源码使用过程中,有时会出现软键盘切换闪屏问题,就是当切换表情的时候屏幕会跳动,因此要对一对一直播源码表情面板无缝切换进行优化。 ... [详细]
  • 使用Flutternewintegration_test进行示例集成测试?回答首先在dev下的p ... [详细]
  • 注:根据Qt小神童的视频教程改编概论:利用最新的Qt5.1.1在windows下开发的一个小的时钟程序,有指针与表盘。1.Qtforwindows开发环境最新的Qt已经集 ... [详细]
author-avatar
媛媛天下_945
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有