ANDROID--仿瀑布布局

Android自定义视图瀑布布局hierarchyviewer

转载: http://blog.youkuaiyun.com/a396901990/article/details/38688409

 

简介:

 

在自定义view的时候,其实很简单,只需要知道3步骤:

1.测量——onMeasure():决定View的大小

2.布局——onLayout():决定View在ViewGroup中的位置

3.绘制——onDraw():如何绘制这个View。

 

第3步的onDraw系统已经封装的很好了,基本不用我们来操心,只需要专注到1,2两个步骤就中好了。

第一步的测量,可以参考:(ANDROID自定义视图——onMeasure,MeasureSpec源码流程思路详解

第二步的布局,可以参考:(ANDROID自定义视图——onLayout源码流程思路详解

 

下面来介绍是如何通过之前学习的onMeasure和onLayout去自定义一个仿瀑布型的自定义视图。

 

效果图:



          


    

 

    

第一个gif图是在手机模拟器上,由于手机屏幕小所以在竖直状态下每行显示一个,在横屏时每行显示两个。而在平板上时候由于屏幕很大,所以可以根据具体尺寸和需要调整每行显示的view数。

本例只是简单的显示,但是这里可以把每个view当做是一个Card。每个Card用一个fragment控制,这样就可以在一个大屏中按需求显示更多的Fragment,这样就不用在ViewPager中左右滑动来显示fragment。

 

 

代码分析:

 

Activity

[java] view plaincopyprint?

1.  @Override  

2.  protected void onCreate( Bundle savedInstanceState )  

3.  {  

4.      super.onCreate(savedInstanceState);  

5.      setContentView(R.layout.main_layout);  

6.  }  

 

main_layout.xml

[html] view plaincopyprint?

1.  <?xml version="1.0" encoding="utf-8"?>  

2.  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  

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

4.      android:layout_width="match_parent"  

5.      android:layout_height="match_parent"  

6.      android:orientation="vertical">  

7.    

8.      <com.gxy.autolayout.MyScrollView  

9.          auto:columns="1"  

10.         android:id="@+id/myScrollView"  

11.         android:layout_margin="5dip"  

12.         android:layout_width="match_parent"  

13.         android:layout_height="match_parent">  

14.         </com.gxy.autolayout.MyScrollView>  

15.   

16. </LinearLayout>  

没什么特别的,只是在一个LinearLayout中加入了一个自定义的View——MyScrollView。而且在该View中有个自定义属性columns,它表示每行显示多少个View。关于自定义属性网上很多,我这里就不浪费时间了。

 

MyScrollView:

[java] view plaincopyprint?

1.  /** 

2.   * 该类继承自ScrollView,目的是为了可以滑动我们自定义的视图 

3.   */  

4.  public class MyScrollView  

5.      extends ScrollView  

6.  {  

7.      int columns = 0;  

8.    

9.      public MyScrollView( Context context )  

10.     {  

11.         super(context);  

12.     }  

13.   

14.     public MyScrollView( Context context, AttributeSet attrs )  

15.     {  

16.         super(context, attrs);  

17.         // 取出布局中自定义的属性  

18.         TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyScrollView);  

19.         columns = typedArray.getInteger(R.styleable.MyScrollView_columns, 0);  

20.         typedArray.recycle();  

21.         // 初始化视图  

22.         initView(columns);  

23.     }  

24.   

25.     private void initView( int columns )  

26.     {  

27.         // 建立一个LinearLayout作为ScrollView的顶层视图(因为ScrollView只可以有一个子ViewGroup  

28.         LinearLayout linearLayout = new LinearLayout(getContext());  

29.         linearLayout.setOrientation(LinearLayout.VERTICAL);  

30.         // LinearLayout中加入一个自定义视图AutoCardLayout(我们的逻辑全部在这个自定义视图类中)  

31.         linearLayout.addView(new AutoCardLayout(getContext(), columns));  

32.         addView(linearLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));  

33.     }  

34. }  


AutoCardLayout

[java] view plaincopyprint?

1.  /** 

2.   * AutoCardLayout继承自ViewGroup,主要作用就是根据column的值动态的排列每个card视图 

3.   */  

4.  public class AutoCardLayout  

5.          extends ViewGroup {  

6.    

7.      // 每行显示的列数  

8.      int column = 0;  

9.      // 每个Card的横向间距  

10.     int margin = 20;  

11.   

12.     // 构造方法中加入5个已经定义好的布局(这里就是为了图方便,就直接扔构造方法里了)  

13.     public AutoCardLayout(Context context, int columns) {  

14.         super(context);  

15.         this.column = columns;  

16.         View v1 = LayoutInflater.from(context).inflate(R.layout.card_layout1, null);  

17.         View v2 = LayoutInflater.from(context).inflate(R.layout.card_layout2, null);  

18.         View v3 = LayoutInflater.from(context).inflate(R.layout.card_layout3, null);  

19.         View v4 = LayoutInflater.from(context).inflate(R.layout.card_layout4, null);  

20.         View v5 = LayoutInflater.from(context).inflate(R.layout.card_layout5, null);  

21.   

22.         addView(v1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));  

23.         addView(v2, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));  

24.         addView(v3, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));  

25.         addView(v4, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));  

26.         addView(v5, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));  

27.     }  

28.   

29.     // 重写的onMeasure方法  

30.     @Override  

31.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

32.   

33.     }  

34.   

35.     // 重写的onLayout方法  

36.     @Override  

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

38.           

39.     }  

40. }  

onMeasure和onLayout是本文的重点,所以单独拿出来讲解。

不过为了更好的理解,我先把思路讲解一下:

1. 此布局类似与瀑布布局,从左到右排序,但会重上到下按列对齐(按列对齐这点非常重要)

2. 在onMeasure方法中需要测量每个子View的宽。每个子View的宽应该是相同的,和每行显示的列数和间距有关

3. 在onMeasure方法中我们无法测量出每个子View的测量高度(MeasureSpec.getSize(heightMeasureSpec)=0),因为在ScrollView中高度不确定(个人理解,希望指正)

4.  在onMeasure方法需要测量父视图的大小,宽度是确定的,主要测量实际的高度(父View高度是最长列子View的高度之和)

5. 在onLayout方法中需要根据每个View所在的位置进行布局

 

onMeasure

[java] view plaincopyprint?

1.  // 重写的onMeasure方法  

2.  @Override  

3.  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

4.    

5.      // 得到父View的实际测量宽  

6.      int width = MeasureSpec.getSize(widthMeasureSpec);  

7.    

8.      // (width - (column - 1) * margin) / column 得到每个子View的宽度(思路:父View减去所有的间距再除以列数)  

9.      // 根据子View宽度和测量模式确定出子View的详细测量宽  

10.     int colWidthSpec = MeasureSpec.makeMeasureSpec((width - (column - 1) * margin) / column, MeasureSpec.EXACTLY);  

11.     // 因为测量不出来子View的高度,所以这里设置其测量模式为未指定得到详细测量高  

12.     int colHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);  

13.   

14.     // 列数组,代表这一列所有View的高度  

15.     int[] colPosition = new int[column];  

16.   

17.     // 循环所有子View  

18.     for (int i = 0; i < getChildCount(); i++) {  

19.         View child = getChildAt(i);  

20.         // 调用子Viewmeasure方法并传入之前计算好的值进行测量  

21.         child.measure(colWidthSpec, colHeightSpec);  

22.   

23.         // (i + 1 + column) % column 这段代码就是通过当前的View和列数算出当前View是在第几列(多加了一个column值是防止被余数小于余数)  

24.         // 将相应列的子View高度相加  

25.         colPosition[(i + 1 + column) % column] += child.getMeasuredHeight();  

26.     }  

27.   

28.     // View的长度值  

29.     int height = 0;  

30.     // 下面代码计算出列数组中最长列的值,最长列的值就是父View的高度  

31.     for (int j = 0; j < column; j++) {  

32.         height = colPosition[j] > height ? colPosition[j] : height;  

33.     }  

34.   

35.     // 根据计算得到的宽高值测量父View  

36.     setMeasuredDimension(width, height);  

37. }  



onLayout

[java] view plaincopyprint?

1.  // 重写的onLayout方法  

2.  @Override  

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

4.      // 列数组,代表这一列所有View的高度  

5.      int[] colPosition = new int[column];  

6.    

7.      // 循环所有子View  

8.      for (int i = 0; i < getChildCount(); i++) {  

9.          View child = getChildAt(i);  

10.   

11.         // 得到子View的宽高,  

12.         int width = child.getMeasuredWidth();  

13.         int height = child.getMeasuredHeight();  

14.   

15.         // 得到当前View在列数组中的下标(如果余数为0则是最后一列)  

16.         int index = (i + 1 + column) % column == 0 ? column - 1 : (i + 1 + column) % column - 1;  

17.         // 将子View的高度加到列高度中  

18.         colPosition[index] += height;  

19.   

20.         // 计算当前View的左上右下值,传入layout方法中进行布局  

21.         // 具体思路我在之前介绍onlayout的文章提过,只要知道left值和top值还有子View的宽高值就可以确定出rightbottom值(right = left + widthbottom = top + height  

22.         int left = l + index * (width + margin);  

23.         int right = column == index + 1 ? r : left + width;  

24.         int top = t + colPosition[index] - height;  

25.         int bottom = top + height;  

26.         child.layout(left, top, right, bottom);  

27.     }  

28. }  

onMeasure和onLayout就介绍完了,我自认为这个算法还是很不错的。

 

下面介绍一下我在自定义View时的技巧:

1. hierarchyviewer

2. DEBUG + 找张纸拿笔算

 

hierarchyviewer

这个不用多说,自定义View时的神器

大家可以在如下目录中找到它:your sdkpath\sdk\tools

下面放几张hierarchyviewer的截图,顺便看看这个例子的视图结构:


最外层结构



如图,每个子视图都是在FrameLayout视图之上,否则不能正确测量(什么原因请大神指点)。

我为了方便直接把FrameLayout写在每个Cardlayout中,其实比较好的做法是应该在代码中new一个FrameLayout然后再addView(看代码时fragment经常包裹在一个FrameLayout中,道理应该是相同的)。



如图还可以点击观看详细的比例情况

我为了方便直接把FrameLayout写在每个Cardlayout中,其实比较好的做法是应该在代码中new一个FrameLayout然后再addView(看代码时fragment经常包裹在一个FrameLayout中,道理应该是相同的)。

 

如图还可以点击观看详细的比例情况

大家还可以点击右上角的Profile Node查看View的执行效率(视图上面的三个小圆点就是)。

hierarchyviewer还可以查看详细的屏幕画面,具体到像素级别的问题都可以通过它发现。

如果看一个比较复杂的代码时也可以使用hierarchyviewer快速了解视图结构。

 

DEBUG+找张纸拿笔算:

使用hierarchyviewer的主要作用就是为了调错用的,而具体的宽高计算还需要不停的跟踪debug,而算法和思路就需要用纸笔慢慢设计计算了(除非你有一个牛逼的大脑)

 

总结:

之前写完onMeasure和onLayout的内容时就想写一个小例子,本来计划写个FlowLayout(流布局)的例子。但是前几个星期发现有人刚写了一个,所以也是借着流布局的思路写出来这个,写完发现这不就是Waterfall Layout(瀑布布局)么。

这个例子有很多可以改进的地方,比如还不能动态添加和删除视图,列值也不能动态设置。没有根据屏幕大小按比例放大/缩小每个Card视图。而且Card视图也应该在Fragment中,然后再添加到自定义View中去。以后有时间我会好好改进一下。

 

 

有人要下载的话就看看逻辑就好了,那几个CardLayout我就是东挪西凑弄出来的,里面的代码简直不忍直视大家就忽略好了。

另外这个工程是eclipse建立,然后导入到Android Studio中编写的。正常导入是没问题的,如果有问题的话试试把build.gradle等文件删除再导入,实在不好使就新建个工程把几个关键类复制进去吧。。。

 

代码点击下载

          


    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值