欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 中自动换行的标签实现

程序员文章站 2022-05-31 18:47:08
...

前言

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

Android 中自动换行的标签实现

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

实现

像这种布局我们一般都是集成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>  

好了搞定了。