Android自定义流式布局/自动换行布局实例

最近,Google开源了一个流式排版库“FlexboxLayout”,功能强大,支持多种排版方式,如各种方向的自动换行等,具体资料各位可搜索学习^_^。

由于我的项目中,只需要从左到右S型的自动换行,需求效果图如下:

使用FlexboxLayout这个框架未免显得有些臃肿,所以自己动手写了一个流式ViewGroup。

安卓中自定义ViewGroup的步骤是:

1. 新建一个类,继承ViewGroup

2. 重写构造方法

3. 重写onMeasure、onLayout方法

onMeasuer方法里一般写测量子View宽高、确定此控件宽高的代码;onLayout方法则是确定子View如何摆放(排版)。

代码如下:

  import android.content.Context;
  import android.util.AttributeSet;
  import android.view.View;
  import android.view.ViewGroup;

  public class FlexBoxLayout extends ViewGroup {

    private int mScreenWidth;
    private int horizontalSpace, verticalSpace;
    private float mDensity;//设备密度,用于将dp转为px

    public FlexBoxLayout(Context context) {
      this(context, null);
    }

    public FlexBoxLayout(Context context, AttributeSet attrs) {
      super(context, attrs);
      //获取屏幕宽高、设备密度
      mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
      mDensity = context.getResources().getDisplayMetrics().density;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      //确定此容器的宽高
      int widthMode = MeasureSpec.getMode(widthMeasureSpec);
      int widthSize = MeasureSpec.getSize(widthMeasureSpec);
      int heightMode = MeasureSpec.getMode(heightMeasureSpec);
      int heightSize = MeasureSpec.getSize(heightMeasureSpec);

      //测量子View的宽高
      int childCount = getChildCount();
      View child = null;
      //子view摆放的起始位置
      int left = getPaddingLeft();
      //一行view中将最大的高度存于此变量,用于子view进行换行时高度的计算
      int maxHeightInLine = 0;
      //存储所有行的高度相加,用于确定此容器的高度
      int allHeight = 0;
      for (int i = 0; i < childCount; i++) {
        child = getChildAt(i);
        //测量子View宽高
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        //两两对比,取得一行中最大的高度
        if (child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom() > maxHeightInLine) {
          maxHeightInLine = child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();
        }
        left += child.getMeasuredWidth() + dip2px(horizontalSpace) + child.getPaddingLeft() + child.getPaddingRight();
        if (left >= widthSize - getPaddingRight() - getPaddingLeft()) {//换行
          left = getPaddingLeft();
          //累积行的总高度
          allHeight += maxHeightInLine + dip2px(verticalSpace);
          //因为换行了,所以每行的最大高度置0
          maxHeightInLine = 0;
        }
      }
      //再加上最后一行的高度,因为之前的高度累积条件是换行
      //最后一行没有换行操作,所以高度应该再加上
      allHeight += maxHeightInLine;

      if (widthMode != MeasureSpec.EXACTLY) {
        widthSize = mScreenWidth;//如果没有指定宽,则默认为屏幕宽
      }

      if (heightMode != MeasureSpec.EXACTLY) {//如果没有指定高度
        heightSize = allHeight + getPaddingBottom() + getPaddingTop();
      }

      setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
     if (changed) {
       //摆放子view
       View child = null;
       //初始子view摆放的左上位置
       int left = getPaddingLeft();
       int top = getPaddingTop();
       //一行view中将最大的高度存于此变量,用于子view进行换行时高度的计算
       int maxHeightInLine = 0;
       for (int i = 0, len = getChildCount(); i < len; i++) {
         child = getChildAt(i);
         //从第二个子view开始算起
         //因为第一个子view默认从头开始摆放
         if (i > 0) {
           //两两对比,取得一行中最大的高度
         if (getChildAt(i - 1).getMeasuredHeight() > maxHeightInLine) {
             maxHeightInLine = getChildAt(i - 1).getMeasuredHeight();
           }
           //当前子view的起始left为 上一个子view的宽度+水平间距
           left += getChildAt(i - 1).getMeasuredWidth() + dip2px(horizontalSpace);
           if (left + child.getMeasuredWidth() >= getWidth() - getPaddingRight() - getPaddingLeft()) {//这一行所有子view相加的宽度大于容器的宽度,需要换行
             //换行的首个子view,起始left应该为0+容器的paddingLeft
             left = getPaddingLeft();
             //top的位置为上一行中拥有最大高度的某个View的高度+垂直间距
             top += maxHeightInLine + dip2px(verticalSpace);
             //将上一行View的最大高度置0
             maxHeightInLine = 0;
           }
         }
         //摆放子view
         child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
       }
     }
   }

    /**
     * dp转为px
     *
     * @param dpValue
     * @return
     */
    private int dip2px(float dpValue) {
      return (int) (dpValue * mDensity + 0.5f);
    }

    /**
     * 设置子view间的水平间距 单位dp
     *
     * @param horizontalSpace
     */
    public void setHorizontalSpace(int horizontalSpace) {
      this.horizontalSpace = horizontalSpace;
    }

    /**
     * 设置子view间的垂直间距 单位dp
     *
     * @param verticalSpace
     */
    public void setVerticalSpace(int verticalSpace) {
      this.verticalSpace = verticalSpace;
    }
  }  

使用如下:

xml文件:

    <com.zengd.FlexBoxLayout
      android:id="@+id/flexBoxLayout"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <!--这里写子View,也可代码动态添加-->
      ……

    </com.zengd.FlexBoxLayout>

Activity里的代码:

  FlexBoxLayout flexBoxLayout = (FlexBoxLayout) findViewById(R.id.flex_box_layout);
  flexBoxLayout.setHorizontalSpace(10);//不设置默认为0
  flexBoxLayout.setVerticalSpace(10);//不设置默认为0

运行效果如图:

本项目Demo地址:

https://github.com/zengd0/FlexBoxLayout

补充知识:Android 流式布局(修改版) 当达到两行,隐藏多余的

我就废话不多说了,还是直接看代码吧!

public class SearchLayout extends LinearLayout {

  private final int mParentWidth;
  private float textSize;
  private boolean textColor;
  private boolean background;
  private boolean isHide = true;

  public void setHide(boolean hide) {
    isHide = hide;
  }

  public SearchLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    //获取屏幕的宽度
    DisplayMetrics metrics = context.getResources().getDisplayMetrics();
    mParentWidth = metrics.widthPixels - dip2px(16f);
    //自定义属性
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SearchLayout);
    background = array.getBoolean(R.styleable.SearchLayout_Sear_background,false);
    textColor = array.getBoolean(R.styleable.SearchLayout_Sear_textColor, false);
    textSize = array.getDimension(R.styleable.SearchLayout_Sear_textSize, 0);
    //方向为纵向
    setOrientation(VERTICAL);
  }

  //移除子控件
  public void removeView() {
    removeAllViews();
  }

  //流式布局
  public void setData(List<String> data) {
    if (data.isEmpty()){
      return;
    }
    //获取一个子布局
    LinearLayout linearLayout = getLinearLayout();
    for (int i = 0; i < data.size(); i++) {
      //标题
      final String name = data.get(i);
      //已存在的宽度
      int numBar = 0;
      //子控件的个数
      int count = linearLayout.getChildCount();
      for (int j = 0; j < count; j++) {
        //一个一个获取
        ThemeTextView textView = (ThemeTextView) linearLayout.getChildAt(j);
        //获取左外边距
        LayoutParams params = (LayoutParams) textView.getLayoutParams();
        int leftWidth = params.leftMargin;
        int rightWidth = params.rightMargin;
        //获取宽高
        textView.measure(getMeasuredWidth(), getMeasuredHeight());
        //计算已存在的宽度
        numBar += textView.getMeasuredWidth()+leftWidth+rightWidth;
      }
      //获取一个子控件
      ThemeTextView text = getText();
      //给每一个控件设置点击事件
      text.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
          if (onItemTitleClickListener != null){
            onItemTitleClickListener.onItemTitle(name);
          }
        }
      });
      //赋值
      text.setText(name);
      //获取宽高
      text.measure(getMeasuredWidth(), getMeasuredHeight());
      //当前控件的宽度
      int textWidth = text.getMeasuredWidth() + text.getPaddingLeft() + text.getPaddingRight();
      //判断是否超过屏幕
      if (isHide && getChildCount() == 2){
        ImageView imageView = getMore(false);
        LayoutParams layoutParams = (LayoutParams) imageView.getLayoutParams();
        int leftM = layoutParams.leftMargin;
        int rightM = layoutParams.rightMargin;
        imageView.measure(getMeasuredWidth(), getMeasuredHeight());
        int width = imageView.getMeasuredWidth() + imageView.getPaddingLeft() + imageView.getPaddingRight();
        int imageWidth = leftM + rightM + width;
        if (numBar + textWidth + imageWidth >= mParentWidth){
          if (numBar + textWidth + imageWidth > mParentWidth){
            imageView.setOnClickListener(new OnClickListener() {
              @Override
              public void onClick(View v) {
                if (onMoreClickListener != null){
                  onMoreClickListener.onShowMore(isHide);
                }
              }
            });
            linearLayout.addView(imageView);
            return;
          } else {
            imageView.setOnClickListener(new OnClickListener() {
              @Override
              public void onClick(View v) {
                if (onMoreClickListener != null){
                  onMoreClickListener.onShowMore(isHide);
                }
              }
            });
            linearLayout.addView(text);
            linearLayout.addView(imageView);
            return;
          }
        }else {
          if (i + 1 <= data.size()-1) {
            String title = data.get(i + 1);
            ThemeTextView themeTextView = getText();
            themeTextView.setText(title);
            themeTextView.measure(getMeasuredWidth(),getMeasuredHeight());
            int themeTextViewWidth = themeTextView.getMeasuredWidth() + themeTextView.getPaddingLeft() + themeTextView.getPaddingRight();
            if (mParentWidth >= numBar + textWidth + imageWidth + themeTextViewWidth ){
              linearLayout.addView(text);
              continue;
            }else {
              imageView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                  if (onMoreClickListener != null){
                    onMoreClickListener.onShowMore(isHide);
                  }
                }
              });
              linearLayout.addView(text);
              linearLayout.addView(imageView);
              return;
            }
          }
        }
      }

      if (i == data.size() - 1 && (getChildCount() >= 3 || (mParentWidth < numBar + textWidth) && getChildCount() == 2)){
        ImageView imageView = getMore(true);
        LayoutParams layoutParams = (LayoutParams) imageView.getLayoutParams();
        int leftM = layoutParams.leftMargin;
        int rightM = layoutParams.rightMargin;
        imageView.measure(getMeasuredWidth(), getMeasuredHeight());
        int width = imageView.getMeasuredWidth() + imageView.getPaddingLeft() + imageView.getPaddingRight();
        int imageWidth = leftM + rightM + width;
        imageView.setOnClickListener(new OnClickListener() {
          @Override
          public void onClick(View v) {
            if (onMoreClickListener != null){
              onMoreClickListener.onShowMore(isHide);
            }
          }
        });
        if (mParentWidth >= numBar + textWidth + imageWidth){
          linearLayout.addView(text);
          linearLayout.addView(imageView);
        }else {
          if (mParentWidth >= numBar + textWidth){
            linearLayout.addView(text);
            linearLayout = getLinearLayout();
            linearLayout.addView(imageView);
          }else {
            linearLayout = getLinearLayout();
            linearLayout.addView(text);
            linearLayout.addView(imageView);
          }
        }
        return;
      }

      if (mParentWidth >= numBar + textWidth) {
        //没有,继续添加
        linearLayout.addView(text);
      } else {
        //否者,重新获取一个子布局,再添加
        linearLayout = getLinearLayout();
        linearLayout.addView(text);
      }

    }
  }

  public LinearLayout getLinearLayout() {
    //创建LinearLayout布局
    LinearLayout linearLayout = new LinearLayout(getContext());
    //设置宽高
    LayoutParams params = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
    linearLayout.setLayoutParams(params);
    //添加到主布局中
    this.addView(linearLayout);
    return linearLayout;
  }

  public ThemeTextView getText() {
    //创建TextView控件
    //设置字体大小,颜色,内边距
    ThemeTextView themeTextView = new ThemeTextView(getContext());
    themeTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX , textSize);
    themeTextView.setMaxEms(7);
    themeTextView.setLines(1);
    themeTextView.setEllipsize(TextUtils.TruncateAt.END);
    themeTextView.setPadding(dip2px(8), dip2px(4), dip2px(8), dip2px(4));
    if (textColor){//可以根据自己的需求修改判断
    	themeTextView.setTextColor(ContextCompat.getColor(getContext(),R.color.day_text_color_thirdly));
    }else {
      themeTextView.setTextColor(ContextCompat.getColor(getContext(),R.color.day_text_color_thirdly));
    }

    if (background){
       themeTextView.setBackgroundResource(R.drawable.border_search_background_day);
    }
    //设置宽高
    LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    //外边距
    params.setMargins(dip2px(8),dip2px(8),dip2px(8),dip2px(8));
    themeTextView.setLayoutParams(params);
    return themeTextView;
  }

  public ImageView getMore(boolean isHide){
    ImageView imageView = new ImageView(getContext());
    if (background){
      imageView.setBackgroundResource(R.drawable.border_search_background_day);
    }
    imageView.setImageResource(R.drawable.icon_more);

    if (isHide){
      imageView.setRotation(180f);
    }
	  imageView.setColorFilter(ContextCompat.getColor(getContext(),R.color.day_text_color_primary));
    imageView.setPadding(dip2px(6), dip2px(6), dip2px(7), dip2px(7));

    //设置宽高
    LayoutParams params = new LayoutParams(ConfigSingleton.dip2px(27), ConfigSingleton.dip2px(27));
    //外边距
    params.setMargins(dip2px(8),dip2px(8),dip2px(8),dip2px(8));
    imageView.setLayoutParams(params);
    return imageView;
  }

  public interface OnItemTitleClickListener{
    void onItemTitle(String title);
  }

  public interface OnMoreClickListener{
    void onShowMore(boolean ishide);
  }

  private OnItemTitleClickListener onItemTitleClickListener;
  private OnMoreClickListener onMoreClickListener;

  public void setOnItemTitleClickListener(OnItemTitleClickListener onItemTitleClickListener) {
    this.onItemTitleClickListener = onItemTitleClickListener;
  }

  public void setOnMoreClickListener(OnMoreClickListener onMoreClickListener) {
    this.onMoreClickListener = onMoreClickListener;
  }

	public int dip2px(float dipValue) {
			float scale = getContext().getResources().getDisplayMetrics().density;
			return (int) (dipValue * scale + 0.5f);
		}
}

attrs文件:

 <declare-styleable name="SearchLayout">
    <attr name="Sear_textSize" format="dimension"/>
    <attr name="Sear_textColor" format="boolean"/>
    <attr name="Sear_background" format="boolean"/>
  </declare-styleable>

以上这篇Android自定义流式布局/自动换行布局实例就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 基于Android在布局中动态添加view的两种方法(总结)

    一.说明 添加视图文件的时候有两种方式:1.通过在xml文件定义layout:2.java代码编写 二.前言说明 1.构造xml文件 2.LayoutInflater 提到addview,首先要了解一下LayoutInflater类.这个类最主要的功能就是实现将xml表述的layout转化为View的功能.为了便于理解,我们可以将它与findViewById()作一比较,二者都是实例化某一对象,不同的是findViewById()是找xml布局文件下的具体widget控件实例化,而LayoutI

  • Android自定义流式布局/自动换行布局实例

    最近,Google开源了一个流式排版库"FlexboxLayout",功能强大,支持多种排版方式,如各种方向的自动换行等,具体资料各位可搜索学习^_^. 由于我的项目中,只需要从左到右S型的自动换行,需求效果图如下: 使用FlexboxLayout这个框架未免显得有些臃肿,所以自己动手写了一个流式ViewGroup. 安卓中自定义ViewGroup的步骤是: 1. 新建一个类,继承ViewGroup 2. 重写构造方法 3. 重写onMeasure.onLayout方法 onMeasu

  • Android自定义流式布局的实现示例

    在日常的app使用中,我们会在android 的app中看见 热门标签等自动换行的流式布局,今天,我们就来看看如何自定义一个类似热门标签那样的流式布局.下面我们就来详细介绍流式布局的应用特点以及用的的技术点. 1.流式布局的特点以及应用场景 特点:当上面一行的空间不够容纳新的TextView时候,才开辟下一行的空间. 原理图: 场景:主要用于关键词搜索或者热门标签等场景 2.自定义ViewGroup (1)onMeasure:测量子view的宽高,设置自己的宽和高 (2)onLayout:设置子

  • Android自定义流式布局实现淘宝搜索记录

    本文实例为大家分享了Android实现淘宝搜索记录的具体代码,供大家参考,具体内容如下 效果如下: 废话不多说 实现代码: attrs.xml <declare-styleable name="TagFlowLayout"> <!--最大选择数量--> <attr name="max_select" format="integer"/> <!--最大可显示行数--> <attr name=&q

  • Android简单实现自定义流式布局的方法

    本文实例讲述了Android简单实现自定义流式布局的方法.分享给大家供大家参考,具体如下: 首先来看一下 手淘HD - 商品详情 - 选择商品属性 页面的UI 商品有很多尺码,而且展现每个尺码所需要的View的大小也不同(主要是宽度),所以在从服务器端拉到数据之前,展现所有尺码所需要的行数和每一行的个数都无法确定,因此不能直接使用GridView或ListView. 如果使用LinearLayout呢? 一个LinearLayout只能显示一行,如果要展示多行,则每一行都要new一个Linear

  • Android FlowLayout流式布局实现详解

    本文实例为大家分享了Android FlowLayout流式布局的具体代码,供大家参考,具体内容如下 最近使用APP的时候经常看到有 这种流式布局 ,今天我就跟大家一起来动手撸一个这种自定义控件. 首先说一下自定义控件的流程: 自定义控件一般要么继承View要么继承ViewGroup View的自定义流程: 继承一个View-->重写onMeasure方法-->重写onDraw方法-->定义自定义属性-->处理手势操作 ViewGroup的自定义流程: 继承一个ViewGroup-

  • Android自定义顶部导航栏控件实例代码

    下面一段代码给大家介绍了android 自定义顶部导航栏控件功能,具体代码如下所示: class HeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { //重写构造方法 在java里面 我们一般是重写三个构造方法//在kotlin中 我们可以使用

  • Android自定义View中attrs.xml的实例详解

    Android自定义View中attrs.xml的实例详解 我们在自定义View的时候通常需要先完成attrs.xml文件 在values中定义一个attrs.xml 然后添加相关属性 这一篇先详细介绍一下attrs.xml的属性. <?xml version="1.0" encoding="utf-8"?> <resources> //自定义属性名,定义公共属性 <attr name="titleText" for

  • Android自定义processor实现bindView功能的实例

    一.简介 在现阶段的Android开发中,注解越来越流行起来,比如ButterKnife,Retrofit,Dragger,EventBus等等都选择使用注解来配置.按照处理时期,注解又分为两种类型,一种是运行时注解,另一种是编译时注解,运行时注解由于性能问题被一些人所诟病.编译时注解的核心依赖APT(Annotation Processing Tools)实现,原理是在某些代码元素上(如类型.函数.字段等)添加注解,在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的

  • Android 自定义缩短Toast显示时间的实例代码

    我这个主要是缩短Toast显示时间,要延长时间的话,可自行更改 废话不多说哈,见代码 import android.content.Context; import android.os.CountDownTimer; import android.util.Log; import android.widget.Toast; public class ToastUtil { private String TAG = "ToastUtil"; private Toast mToast; p

  • Android 自定义 View 中使用 Spannable的实例详解

    我们都知道 Android 中使用 Spannable 可以实现 TextView 富文本的显示,但是在自定义控件中如何使用 Spannable 绘制不同样式的文字呢? 例如这种效果,标题中的 分数字61 是粗体,分 是常规字体,并且相对于 61 更小些. 第一反应可能是使用 SpannableString.setSpan() 设置 RelativeSizeSpan, 然后在 onDraw() 中进行绘制,事实是这样实现是没有效果的,因为 onDraw() 中只能获取到 SpannableStr

随机推荐