經(jīng)典自定義 ViewGroup集合,高效布局。

WidgetLayout 介紹

WidgetLayout是一組繼承于ViewGroup的自定義容器集合,大部分支持描邊和內(nèi)容分割線,目前實(shí)現(xiàn)了以下實(shí)用容器:

  1. ColumnLayout 以等分列方式布局,每列可設(shè)置內(nèi)容居左,中,右,及鋪滿,可設(shè)置最小最大列寬高限定。
  2. NestRefreshLayout 一個(gè)精簡(jiǎn)強(qiáng)大的支持任意類型View的下拉刷新,上拉加載更多,可添加頭部和尾部且可設(shè)置懸停模式。
  3. NestFloatLayout 支持列表的嵌套滑動(dòng)和指定子 View 懸停頂部,類似NestScrollView 。
  4. PageScrollView 可水平垂直方向布局和滑動(dòng)吸頂?shù)龋瑹o需嵌套,支持ScrollViewViewPager的交互和接口。
  5. PageScrollTabPageScrollView上擴(kuò)展支持Tab場(chǎng)景交互和各種UI定制。
  6. WrapLayout 支持水平布局,并自適應(yīng)換行,單行或列時(shí)支持weight屬性,可限定每行最少和最多Item數(shù),行內(nèi)容可水平和垂直居中。
  7. LabelLayout 繼承自 WrapLayout,以ItemProvider 方式提供內(nèi)容,有簡(jiǎn)單的回收復(fù)用機(jī)制,有Item點(diǎn)擊監(jiān)聽。
  8. HierarchyLayout 一個(gè)展示 View樹層級(jí)關(guān)系和工具容器,可畫出結(jié)束依賴圖和3D層級(jí)圖,計(jì)算出平均層級(jí)和最近一次measure,layout,draw的時(shí)間。

以下是各容器實(shí)現(xiàn)的結(jié)構(gòu)類圖, 查看工程地址 github 點(diǎn)我

classlayer.jpg

實(shí)現(xiàn)背景及使用場(chǎng)景

1. 現(xiàn)有系統(tǒng)容器下開發(fā)界面經(jīng)常遇到以下尷尬問題:

  • 實(shí)現(xiàn)特定的組合布局,我們會(huì)用功能鮮明的4大常用布局去嵌套實(shí)現(xiàn),增加了嚴(yán)重OverDraw的機(jī)率;
  • RelativeLayout 布局功能強(qiáng)大但是measure過程復(fù)雜(每次執(zhí)行onMeasure 所有直接子View會(huì)有兩次measure)
  • 為實(shí)現(xiàn)復(fù)雜交互工作量比開發(fā)業(yè)務(wù)繁重,比如滑動(dòng)控件的懸?;蚵?lián)動(dòng);
  • 分割線或是邊界線很多人常用View來實(shí)現(xiàn),即耗內(nèi)存,又影響測(cè)繪時(shí)間。
  • 等分布局,使用多層不同方向LinearLayout來實(shí)現(xiàn),嵌套太多。
  • 常用容器控件還沒有對(duì)自身做最大寬高限定的,也無對(duì)子 View 做最大寬高限定。

2. 適合的使用場(chǎng)景舉例按需要選擇,可減少布局嵌套和復(fù)雜的交互代碼。

  • 容器自身或?qū)χ苯幼?code>View的最大寬高限定,容器內(nèi)容和直接子Viewgravity靈活支持。
  • 容器需要描邊或子View間畫分割線的,或有各種間距要求,或按下自帶激變層效果可使用繼承于PressViewGroup的容器,如WrapLayout,LabelLayout,ColumnLayout。
  • 常見的表格布局或動(dòng)態(tài)等分布局可用ColumnLayout,支持每列的Align方式(左中右上下)和全鋪滿。
  • 標(biāo)簽布局或需自適應(yīng)換行可選用WrapLayoutLabelLayout,可設(shè)置行最少最多的View 個(gè)數(shù)和行居中,
  • 列表需要嵌套滑動(dòng)和懸停吸頂可用NestFloatLayout,類似NestScrollView 。
  • PageScrollView 可取代ScrollView&HorizontalScrollView 少嵌套,可設(shè)置任意子View滑動(dòng)懸停在開始和結(jié)束位置,可不限定子View大小像 ViewPager 一樣選中居中和滑動(dòng)的交互。
  • WrapLayout,ColumnLayout是完全可替代支持不同方向的LinearLayout并能提供更多的布局約束,和背景,描邊,分割等額外裝飾。
  • 以上容器都有一個(gè)共通的基類,便于統(tǒng)一監(jiān)控性能打點(diǎn)如layout,measure過程等。

Demo演示效果

WrapLayoutLabelLayout ColumnLayout的演示效果。的演示效果。

wraplabel.gif

column.gif

PageScrollViewPageScrollTab的使用示例。

example_scrollview.gif

example_viewpager.gif

Demo 入口 和 NestFloatLayout的演示效果。

entry.jpg

nestlist.gif

HiearchyLayout的靜態(tài)圖,實(shí)際是可隨手勢(shì)改變 3D 形態(tài)的。

hierarchyView.jpeg

hierarchyViewNode.jpeg

如何使用:XML 屬性和 API 簡(jiǎn)介

compile 'com.rexy.android:widgetlayout:1.0.0'

通用屬性說明和介紹

注;所有xml 中使用自定義屬性的地方,請(qǐng)?jiān)诟鶚?biāo)簽中加上xmlns:app="http://schemas.android.com/apk/res-auto"

1. 所有容器自身和子 View 對(duì)于 maxWidth,maxHeight,gravity 支持。

a. 容器控件自身標(biāo)簽下使用 android:gravity,android:maxWidth ,android:maxHeight,即可支持容器內(nèi)容的align 屬和最大寬與高的限制。
java 代碼可通過 setGravity ,setMaxWidth,setMaxHeight 來支持。

b. 容器直接子View使用android:layout_gravity,android:maxWidth,android:maxHeight 即可支持直接子View在容器內(nèi)的Align和自身大小的限制。
java 代碼可通過 BaseViewGroup.LayoutParams lp=(BaseViewGroup.LayoutParams)child.getLayoutParams(); lp.gravity=Gravity.CENTER;lp.maxWidth=100;lp.maxHeight=200

2. 部分容器FloatDrawableDividerMargin的應(yīng)用,僅限于繼承于PressViewGroup的容器

a. xml 中使用支持FloatDrawable屬性和解釋如下,java 都有對(duì)應(yīng)的set 和get 方法:

  -hover drawable 忽略手勢(shì)滑動(dòng)到自身之外取消按下狀態(tài)-->
  <attr name="ignoreForegroundStateWhenTouchOut" format="boolean"/>
  <!--hover drawable 顏色-->
  <attr name="foregroundColor" format="color"/>
  <!--hover drawable  圓角-->
  <attr name="foregroundRadius" format="dimension"/>
  <!--hover drawable 動(dòng)畫時(shí)間-->
  <attr name="foregroundDuration" format="integer"/>
  <!--hover drawable 最小不透明度-->
  <attr name="foregroundAlphaMin" format="integer"/>
  <!--hover drawable 最大不透明度-->
  <attr name="foregroundAlphaMax" format="integer"/>

b. xml 中使用支持DividerMargin屬性和解釋如下,java 都有對(duì)應(yīng)的set 和get 方法:

  <!--左邊線的顏色,寬度,和邊線padding-->
  <attr name="borderLeftColor" format="color"/>
  <attr name="borderLeftWidth" format="dimension"/>
  <attr name="borderLeftMargin" format="dimension"/>
  <attr name="borderLeftMarginStart" format="dimension"/>
  <attr name="borderLeftMarginEnd" format="dimension"/>

  <!--上邊線的顏色,寬度,和邊線padding-->
  <attr name="borderTopColor" format="color"/>
  <attr name="borderTopWidth" format="dimension"/>
  <attr name="borderTopMargin" format="dimension"/>
  <attr name="borderTopMarginStart" format="dimension"/>
  <attr name="borderTopMarginEnd" format="dimension"/>

  <!--右邊線的顏色,寬度,和邊線padding-->
  <attr name="borderRightColor" format="color"/>
  <attr name="borderRightWidth" format="dimension"/>
  <attr name="borderRightMargin" format="dimension"/>
  <attr name="borderRightMarginStart" format="dimension"/>
  <attr name="borderRightMarginEnd" format="dimension"/>

  <!--下邊線的顏色,寬度,和邊線padding-->
  <attr name="borderBottomColor" format="color"/>
  <attr name="borderBottomWidth" format="dimension"/>
  <attr name="borderBottomMargin" format="dimension"/>
  <attr name="borderBottomMarginStart" format="dimension"/>
  <attr name="borderBottomMarginEnd" format="dimension"/>

  <!--內(nèi)容四邊的間距,不同于padding -->
  <attr name="contentMarginLeft" format="dimension"/>
  <attr name="contentMarginTop" format="dimension"/>
  <attr name="contentMarginRight" format="dimension"/>
  <attr name="contentMarginBottom" format="dimension"/>
  <!--水平方向和垂直方向Item 的間距-->
  <attr name="contentMarginHorizontal" format="dimension"/>
  <attr name="contentMarginVertical" format="dimension"/>


  <!--水平分割線顏色-->
  <attr name="dividerColorHorizontal" format="color"/>
  <!--水平分割線寬-->
  <attr name="dividerWidthHorizontal" format="dimension"/>
  <!--水平分割線開始和結(jié)束padding-->
  <attr name="dividerPaddingHorizontal" format="dimension"/>
  <attr name="dividerPaddingHorizontalStart" format="dimension"/>
  <attr name="dividerPaddingHorizontalEnd" format="dimension"/>

  <!--垂直分割線顏色-->
  <attr name="dividerColorVertical" format="color"/>
  <!--垂直分割線寬-->
  <attr name="dividerWidthVertical" format="dimension"/>
  <!--垂直分割線開始 和結(jié)束padding-->
  <attr name="dividerPaddingVertical" format="dimension"/>
  <attr name="dividerPaddingVerticalStart" format="dimension"/>
  <attr name="dividerPaddingVerticalEnd" format="dimension"/>

具體容器組件的屬性和使用介紹

1.ColumnLayout xml 屬性支持屬性如下:java 都有對(duì)應(yīng)的set和get方法就不給示例了,分割線和各種間距見上面的通用屬性。

  <!--列個(gè)數(shù)-->
  <attr name="columnNumber" format="integer" />
  <!--每行內(nèi)容垂直居中-->
  <attr name="columnCenterVertical" format="boolean"/>

  <!--列內(nèi)內(nèi)容全展開的索引 * 或 1,3,5 類似列索引0 開始-->
  <attr name="stretchColumns" format="string" />
  <!--列內(nèi)內(nèi)容全靠中間 * 或 1,3,5 類似列索引0 開始-->
  <attr name="alignCenterColumns" format="string" />
  <!--列內(nèi)內(nèi)容全靠右 * 或 1,3,5 類似列索引0 開始-->
  <attr name="alignRightColumns" format="string" />

  <!--列寬和高的最大最小值限定-->
  <attr name="columnMinWidth" format="dimension" />
  <attr name="columnMaxWidth" format="dimension" />
  <attr name="columnMinHeight" format="dimension" />
  <attr name="columnMaxHeight" format="dimension" />

2.NestRefreshLayout 在xml 中可像ScrollView一樣用,只支持一個(gè)內(nèi)容??蓜?dòng)態(tài)設(shè)置頭部和尾部。

   //可選,設(shè)置頭部headerView,true 表示懸停在內(nèi)容上。
   refreshLayout.setHeaderView(headerView, true);
   
   //可選,設(shè)置尾部footerView,false 表示不需要懸停在內(nèi)容上。
   refreshLayout.setFooterView(footerView, false);
   
   //可選,設(shè)置覆蓋內(nèi)容的蒙層 View。
   refreshLayout.setMaskView(maskView, true);
   
   //設(shè)置下拉刷新的指示器。
   refreshLayout.setRefreshPullIndicator(new DefaultRefreshIndicator(getActivity()));
   //設(shè)置上拉加載更多的指示器。
   refreshLayout.setRefreshPushIndicator(new DefaultRefreshIndicator(getActivity()));
   
   設(shè)置下拉或上推的狀態(tài)和刷新動(dòng)作的監(jiān)聽
   mRefreshLayout.setOnRefreshListener(new NestRefreshLayout.OnRefreshListener() {
      @Override
      public void onRefreshStateChanged(NestRefreshLayout parent, int state, int preState, int moveAbsDistance) {
      }
      @Override
      public void onRefresh(NestRefreshLayout parent, boolean refresh) {
      }
   });

3.NestFloatLayou xml 屬性支持屬性和 java 代碼如下:

 <!--實(shí)現(xiàn)了嵌套滑動(dòng)NestScrollingChild 接口的滑動(dòng)的 View 所在的直接子 View 索引-->
 <attr name="nestViewIndex" format="integer"/>```
 <!--需要吸頂?shù)巾敳康?View 所在的直接子 View 索引-->
 <attr name="floatViewIndex" format="integer"/>`java`代碼可如下設(shè)置:
 mLastFloatLayout.setNestViewId(viewId);
 mLastFloatLayout.setFloatViewId(viewId);
 或
  mLastFloatLayout.setNestViewIndex(viewIndex);
  mLastFloatLayout.setFloatViewIndex(viewIndex);

4.PageScrollView,PageScrollTab 使用.

a. 支持的xml 屬性,對(duì)應(yīng)都有java 相應(yīng)的set 和 get;

  <!--布局方向,也決定了手勢(shì)方向,僅支持水平和垂直之一。-->
  <attr name="android:orientation"/>
  <!--滑動(dòng)交互 ViewPager 方式-->
  <attr name="viewPagerStyle" format="boolean"/>
  <!--所有的child居中-->
  <attr name="childCenter" format="boolean"/>
  <!--所有的child填充整個(gè)父容器-->
  <attr name="childFillParent" format="boolean"/>
  <!--內(nèi)容item 的間距-->
  <attr name="middleMargin" format="dimension"/>
  <!--item 的size 按父容器的size 百分比-->
  <attr name="sizeFixedPercent" format="float"/>
  <!--快速滑動(dòng)松手后的回彈距離-->
  <attr name="overFlingDistance" format="dimension"/>
  <!--滑動(dòng)懸停到開始位置的child 索引-->
  <attr name="floatViewStartIndex" format="integer"/>
  <!--滑動(dòng)懸停到結(jié)束位置的child 索引-->
  <attr name="floatViewEndIndex" format="integer"/>

b.java 其它接口設(shè)置

//接著上面
  mPageScrollView.setPageHeadView(headerView); //設(shè)置頭部 View
  mPageScrollView.setPageFooterView(footerView); 設(shè)置尾部 View
  //設(shè)置 PageTransformer 動(dòng)畫,實(shí)現(xiàn)滑動(dòng)視圖的變換。
  mPageScrollView.setPageTransformer(new PageScrollView.PageTransformer() {
  @Override
  public void transformPage(View view, float position, boolean horizontal) {
  //在這里根據(jù)滑動(dòng)相對(duì)偏移量 position,實(shí)現(xiàn)該視圖的動(dòng)畫效果。
  }
  @Override
  public void recoverTransformPage(View view, boolean horizontal) {
  //清除視圖的動(dòng)畫效果,在setPageTransformer(null)時(shí)會(huì)調(diào)用。
  }
  });
  PageScrollView.OnPageChangeListener pagerScrollListener = new PageScrollView.OnPageChangeListener() {
  @Override
  public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
  // ViewPager 滑動(dòng)視圖時(shí),相對(duì)偏移適時(shí)回調(diào)。
  }
  @Override
  public void onPageSelected(int position, int oldPosition) {
  // ViewPager 模式時(shí) 選中回調(diào)。
  }
  @Override
  public void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
  //視圖滑動(dòng)回調(diào) View.onScrollChanged
  }
  @Override
  public void onScrollStateChanged(int state, int oldState) {
  //state 的取值如下,標(biāo)明著容器的滑動(dòng)狀態(tài)。
  // SCROLL_STATE_IDLE = 0; // 滑動(dòng)停止?fàn)顟B(tài)。
  // SCROLL_STATE_DRAGGING = 1;//用戶正開始拖拽滑動(dòng) 。
  // SCROLL_STATE_SETTLING = 2;//開始松開手指快速滑動(dòng)。
  }
  };
  mPageScrollView.setOnPageChangeListener(pagerScrollListener);
  // 設(shè)置視圖滾動(dòng)的監(jiān)聽。
  mPageScrollView.setOnScrollChangeListener(pagerScrollListener);
  //設(shè)置可見子 View 發(fā)生變化時(shí) 可見索引區(qū)間的監(jiān)聽。
  mPageScrollView.setOnVisibleRangeChangeListener(new OnVisibleRangeChangeListener(){
      public void onVisibleRangeChanged(int firstVisible, int lastVisible, int oldFirstVisible, int oldLastVisible){
      }
  });
  //設(shè)置動(dòng)畫初始化滑動(dòng)到第二個(gè) View ,-1 表示動(dòng)畫時(shí)間內(nèi)部計(jì)算,如無需動(dòng)畫傳0
  mPageScrollView.scrollTo(1,0,-1);

c.PageScrollTab 繼承于 PageScrollView ,額外支持以下xml 屬性(java 均有g(shù)et 和set 對(duì)應(yīng))。

 <!--tab item 的背景-->
   <attr name="tabItemBackground" format="reference"/>
   <attr name="tabItemBackgroundFirst" format="reference"/>
   <attr name="tabItemBackgroundLast" format="reference"/>
   <attr name="tabItemBackgroundFull" format="reference"/>

   <!--底部指示線-->
   <attr name="tabIndicatorColor" format="color"/>
   <attr name="tabIndicatorHeight" format="dimension"/>
   <attr name="tabIndicatorOffset" format="dimension"/>
   <attr name="tabIndicatorWidthPercent" format="float"/>

   <!--頂部水平分界線-->
   <attr name="tabTopLineColor" format="color"/>
   <attr name="tabTopLineHeight" format="dimension"/>

   <!--底部水平分界線-->
   <attr name="tabBottomLineColor" format="color"/>
   <attr name="tabBottomLineHeight" format="dimension"/>

   <!-- item 之間垂直分割線-->
   <attr name="tabItemDividerColor" format="color"/>
   <attr name="tabItemDividerWidth" format="dimension"/>
   <attr name="tabItemDividerPadding" format="dimension"/>


   <!-- item 的最小 Padding 設(shè)置-->
   <attr name="tabItemMinPaddingHorizontal" format="dimension"/>
   <attr name="tabItemMinPaddingTop" format="dimension"/>
   <attr name="tabItemMinPaddingBottom" format="dimension"/>

   <!--item文字大寫開-->
   <attr name="tabItemTextCaps" format="boolean"/>

   <!--item 文字顏色-->
   <attr name="tabItemTextColor" format="reference"/>

java 額外的接口:

  //設(shè)置ItemProvider,初始化選中第0 個(gè)索引, 類似上面的 LabelLayout 的初始化。
  mPageScrollTab.setTabProvider(mItemProvider,0);
  mPageScrollTab.setTabClickListener(new PageScrollTab.ITabClickEvent() {
      @Override
      public boolean onTabClicked(PageScrollTab parent, View cur, int curPos, View pre, int prePos) {
          return false;
      }
  });

5.WrapLayout xml 屬性支持屬性如下:java 都有對(duì)應(yīng)的set 和get 方法就不給示例了。當(dāng)作LinearLayout如果要支持weight布局,需要設(shè)置 setSupportWeight為true.
且如果是單行需要設(shè)置lineMinItemCount為一個(gè)大于或等于子視圖數(shù)的值,如果是單列需要設(shè)置lineMaxItemCount為1. 才可以應(yīng)用權(quán)重去布局。

  <!--每行內(nèi)容水平居中-->
  <attr name="lineCenterHorizontal" format="boolean"/>
  <!--每行內(nèi)容垂直居中-->
  <attr name="lineCenterVertical" format="boolean"/>

  <!--每一行最少的Item 個(gè)數(shù)-->
  <attr name="lineMinItemCount" format="integer"/>
  <!--每一行最多的Item 個(gè)數(shù)-->
  <attr name="lineMaxItemCount" format="integer"/>
  
  <!-- 支持weight 屬性,前提條件是單行或單列的情況-->
  <attr name="weightSupport" format="boolean" />

6.LabelLayout 繼承WrapLayout有其所有功能接口。

不同是支持 android:textSize,android:textColor 在xml 中設(shè)置Label 的字號(hào)和字色。同樣可用java 代碼設(shè)置字號(hào)字色。
使用可通過ItemProvider 接口來初始化 Label,本工程中的示例初始化如下。

final String[] mLabels = new String[]{
        "A", "B", "C", "D", "E", "F", "G", "H"
};
labelLayout.setItemProvider(new ItemProvider.ViewProvider() {
    @Override
    public int getViewType(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return buildView(getTitle(position),true);
    }

    @Override
    public CharSequence getTitle(int position) {
        return mLabels[position];
    }

    @Override
    public Object getItem(int position) {
        return mLabels[position];
    }

    @Override
    public int getCount() {
        return mLabels == null ? 0 : mLabels.length;
    }
});
final LabelLayout.OnLabelClickListener mLabelClicker = new LabelLayout.OnLabelClickListener() {
    @Override
    public void onLabelClick(LabelLayout parent, View labelView) {
        Object tag = labelView.getTag();
        CharSequence text = tag == null ? null : String.valueOf(tag);
        if (text == null && labelView instanceof TextView) {
            text = ((TextView) labelView).getText();
        }
        if (text != null) {
            Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show();
        }
    }
};
mLabelLayout.setOnLabelClickListener(mLabelClicker);

最后感謝大家關(guān)注和多提建議或是issue,如果很感興趣請(qǐng) star 和 follower . 完整工程地址WidgetLayout

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容