前言

最近看自定义控件时,看到了这种控件就搞了一个研究一下。先看图

Android 文本自动换行优化 安卓自动换行布局_ide

就是根据每行剩余的位置和要显示的标签进行比对换行的控件。

实现

像这种布局我们一般都是集成ViewGroup然后再覆写它的onMeasure和onLayout方法。
public class FlowViewGroup extends ViewGroup {
        private int mPaddingTop;
        private int mPaddingLeft;
        private int mPaddingRight;
        private int mPaddingBottom;
        private ArrayList<String> mList = new ArrayList<>();
        private Context mContext;

        public FlowViewGroup(Context context) {
            super(context);
            mContext = context;
        }


        public FlowViewGroup(Context context, AttributeSet attrs) {
            super(context, attrs);
            mContext = context;
        }

        public FlowViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mContext = context;
        }


        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int top = 0;
            int left;
            int lineHeight = 0;
            for (int i = 0; i < lines.size(); i++) {
                left = 0;
                top += lineHeight;
                lineHeight = 0;
                ArrayList<View> views = lines.get(i);
                for (View view : views) {
                    if (view.getVisibility() == GONE) {
                        continue;
                    }
                    MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
                    view.layout(left + layoutParams.leftMargin + mPaddingLeft, top + layoutParams.topMargin + mPaddingTop, left + view.getMeasuredWidth() + layoutParams.leftMargin + mPaddingLeft, top + view.getMeasuredHeight() + layoutParams.topMargin + mPaddingTop);
                    lineHeight = Math.max(lineHeight, view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin);
                    left = left + view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
                }
            }

        }

        //计算后要显示行的数据的集合
        private ArrayList<ArrayList<View>> lines = new ArrayList<>();

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            lines.clear();
            //自身的padding
            mPaddingTop = getPaddingTop();
            mPaddingLeft = getPaddingLeft();
            mPaddingRight = getPaddingRight();
            mPaddingBottom = getPaddingBottom();
            int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec);

            //通过计算获取的总行高(包括ViewGroup的padding和子View的margin)
            int linesHeight = 0;
            //最宽行的宽度
            int widthMax = 0;
            //当前行已占用的宽度
            int lineEmployWidth = 0;
            //计算时当前行的最大高度
            int currentLineHeightMax = 0;
            //每一行中View的数据集
            ArrayList<View> lineInfo = new ArrayList<>();
            //获取子View的个数
            int childCount = getChildCount();
            //遍历子View对其进行测算
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                //判断子View的显示状态 gone就不进行测算
                if (childView.getVisibility() == GONE) {
                    continue;
                }
                measureChild(childView, widthMeasureSpec, heightMeasureSpec);
                MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
                int childWidth = 0;
                int childHeight = 0;
                //获取view的测量宽度
                childWidth += childView.getMeasuredWidth();
                //每行的第一个添加父布局的paddingLeft
                if (0 == i) {
                    childWidth += mPaddingLeft;
                }
                //获取子View自身的margin属性
                childWidth += (layoutParams.leftMargin + layoutParams.rightMargin);
                //当前的行高
                childHeight = childHeight + (childView.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin);
                //当前行放不下时,重起一行显示
                if (lineEmployWidth + childWidth > widthMeasureSize - mPaddingRight) {
                    //初始当前行的宽度
                    lineEmployWidth = childWidth + mPaddingLeft;
                    //添加一次行高
                    linesHeight += currentLineHeightMax;
                    //初始化行高
                    currentLineHeightMax = childHeight;
                    lines.add(lineInfo);
                    lineInfo = new ArrayList<>();
                    lineInfo.add(childView);
                } else {//当前行可以显示时
                    lineInfo.add(childView);
                    //增加当前行已显示的宽度
                    lineEmployWidth += childWidth;
                    //为了显示最大的行高
                    currentLineHeightMax = Math.max(currentLineHeightMax, childHeight);
                    //显示中最大的行宽
                    widthMax = Math.max(widthMax, lineEmployWidth);
                }
            }
            lines.add(lineInfo);
            linesHeight += (mPaddingTop + mPaddingBottom + currentLineHeightMax);
            setMeasuredDimension((widthMeasureMode == MeasureSpec.EXACTLY) ? widthMeasureSize : widthMax + mPaddingRight, (heightMeasureMode == MeasureSpec.EXACTLY) ? heightMeasureSize : linesHeight);
        }


        /**
         * 重写该方法是为了使用MarginLayoutParams获取子View的margin值
         */
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MarginLayoutParams(getContext(), attrs);
        }
        /**
         *当使用adapter添加数据时使用。
         */
        @Override
        protected LayoutParams generateDefaultLayoutParams() {
            return super.generateDefaultLayoutParams();
        }

        private TagFlowAdapter mAdapter;

        public void setAdapter(TagFlowAdapter adapter) {
            if (null == adapter) {
                throw new NullPointerException("TagFlowAdapter is null, please check setAdapter(TagFlowAdapter adapter)...");
            }
            mAdapter = adapter;
            adapter.setOnNotifyDataSetChangedListener(new TagFlowAdapter.OnNotifyDataSetChangedListener() {
                @Override
                public void OnNotifyDataSetChanged() {
                    notifyDataSetChanged();
                }
            });
            adapter.notifyDataSetChanged();
        }

        private void notifyDataSetChanged() {
            removeAllViews();
            if (mAdapter == null || mAdapter.getCount() == 0) {
                return;
            }
            MarginLayoutParams layoutParams = new MarginLayoutParams(generateDefaultLayoutParams());
            for (int i = 0; i < mAdapter.getCount(); i++) {
                View view = mAdapter.getView(i);
                if (view == null) {
                    throw new NullPointerException("item layout is null, please check getView()...");
                }
                addView(view, i, layoutParams);
            }
        }
    }

我们先看一下onMeasure方法。我们使用getPadding的方法获取父布局的内边距,再通过MeasureSpec的getMode和getSize方法获取测量模式和测量的宽高。遍历子View通过getMeasureWidth和getMeasureHeight来获取其宽高(该值包括子view的padding值),然后根据子View的MarginLayoutParams来获取它的margin值(需要注意的是在布局中直接添加子View的话要重写public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs);}方法。不然会抛类转换异常)。现在我们需要的数值都已经获取到了剩下就好办了。我们先定义一个ArrayList < ArrayList < View > > lines 来储存有多少行的数据。每开始一行 我们就new一个ArrayList lineInfo来储存这一行的要绘制的View。具体的测算方法看上面的代码就行了。

接下来我们再看一下onLayout的方法。在该方法中我们要摆放子view位置。我们在测算过程中已经将子View分行处理。所以我们可以直接遍历lines设置每个字View的位置就行了。具体的看代码就行了。

我们的标签个数、样式等不可能是固定的。所以我们仿照adapter的模式来为该view动态设置数据。我们看一下notifyDataSetChanged方法,我们遍历通过adapter添加的数据,使用addView进行添加。因为我们在测量方法中用到了MarginLayoutParams所以我们添加View的时候要加上一个MarginLayoutParams防止测算方法中抛异常。

再看一下我们定义的adapter。我们定义一个类继承adapter。就可以设置数据了

public abstract class TagFlowAdapter {

    public abstract int getCount();

    public abstract Object getItem(int position);

    public abstract long getItemId(int position);

    public abstract View getView(int position);

    public void notifyDataSetChanged(){
        if(null != mOnNotifyDataSetChangedListener){
            mOnNotifyDataSetChangedListener.OnNotifyDataSetChanged();
        }
    }

    /**
     *  释放一个接口 串联adapter与view中间的数据刷新
     */
    public interface OnNotifyDataSetChangedListener{
        void OnNotifyDataSetChanged();
    }
    private OnNotifyDataSetChangedListener mOnNotifyDataSetChangedListener;
    public void setOnNotifyDataSetChangedListener(OnNotifyDataSetChangedListener listener){
        mOnNotifyDataSetChangedListener = listener;
    }
}

具体的调用代码。
Activity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FlowViewGroup group = (FlowViewGroup) findViewById(R.id.group);
        MyTagAdapter myTagAdapter = new MyTagAdapter(this);
        group.setAdapter(myTagAdapter);
        ArrayList<String> list = new ArrayList<>();
        list.add("上班族");
        list.add("程序员");
        list.add("喜欢美食");
        list.add("懒得健身");
        list.add("没事就喜欢LOL");
        list.add("宅");
        list.add("美女");
        list.add("帅哥");
        list.add("尸兄");
        list.add("不修边幅");
        list.add("德玛西亚");
        myTagAdapter.setData(list);
        myTagAdapter.notifyDataSetChanged();
    }
}

activity_main资源文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >

    <com.tianfb.text.myflowlayout.FlowViewGroup
        android:id="@+id/group"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:padding="5dp"
        >
    </com.tianfb.text.myflowlayout.FlowViewGroup>

</RelativeLayout>

adapter的代码

public class MyTagAdapter extends TagFlowAdapter {

    private ArrayList<String> mList = new ArrayList<>();
    private Context mContext;
    public MyTagAdapter(Context context){
        mContext = context;
    }

    public void setData(ArrayList<String> list){
        mList.clear();
        mList.addAll(list);
        notifyDataSetChanged();
    }


    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position) {
        View view = View.inflate(mContext, R.layout.item, null);
        TextView tv = (TextView) view.findViewById(R.id.tv);
        tv.setText(mList.get(position));
        return view;
    }
}

item资源文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        style="@style/text_flag_01"
        android:id="@+id/tv"/>
</LinearLayout>

style代码

<style name="text_flag_01">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_margin">5dp</item>
        <item name="android:background">@drawable/flag_01</item>
        <item name="android:textColor">#ffffff</item>
    </style>

flag_01代码

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >

    <solid android:color="#7690A5" >
    </solid>

    <corners android:radius="5dp"/>
    <padding
        android:bottom="2dp"
        android:left="10dp"
        android:right="10dp"
        android:top="2dp" />

</shape>

好了搞定了。