美文网首页Android技术知识Android开发Android知识
android自定义ViewGroup---设计简单的FlowL

android自定义ViewGroup---设计简单的FlowL

作者: Vonelone | 来源:发表于2017-05-05 17:00 被阅读0次

    马上周末了,好开心!

    今天手头事情不多,撸一个自定义的viewgroup吧,简单实现瀑布流效果的热门搜索条目:
    最终效果如下:


    如果对自定义viewgroup的一些基本概念不是很熟,可以先看看大神的文章,写的很细致:
    自定义viewgroup入门——Hongyang大神



    下面就是我们这个viewgroup的实现步骤了:

    • 首先,写一个ViewGroup的子类,重写generateLayoutParams(),这一步是为我们的ViewGroup指定一个LayoutParams:

        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MarginLayoutParams(getContext(), attrs);//直接使用系统的MarginLayoutParams
        }
    
    • 接下来,爸爸要根据儿子们的需求买地了。就是onMeasure(),需要量测viewgroup的尺寸了

    计算原理:
    - 把每一个child的尺寸的width累加,累加之前先计算,如果加上这个
    child的width会超过系统量测的最大width,就换行;

    - 每一行的行高是取这一行的所有儿子中最高的;
    
    - 最后的viewgroup的宽取所有行宽最大的,高是所有的行高累加起来;
    
    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(widthMeasureSpec);
    
            int width = 0;//如果设置width为wrap_content时的width
            int height = 0;//同理
    
            int lineWidth = 0;//记录每一行的宽度, width最后取最大的值
            int lineHeight = 0;//记录每一行的高度,height不断累加
    
            //遍历每一个view,计算容器的总尺寸
            for (int i = 0; i < getChildCount(); i++) {
    
                View childView = getChildAt(i);
                measureChild(childView, widthMeasureSpec, heightMeasureSpec);
                MarginLayoutParams childParams = (MarginLayoutParams) childView.getLayoutParams();
    
                int childWidth = childView.getMeasuredWidth() + childParams.leftMargin + childParams.rightMargin;
                int childHeight = childView.getMeasuredHeight() + childParams.topMargin + childParams.bottomMargin;
    
                if (lineWidth + childWidth > widthSize) {//如果当前控件和当前这一行的宽度之和大于widthSize,换行
                    width = Math.max(lineWidth, childWidth);
                    lineWidth = childWidth;//重新记录lineWidth,初始为childWidth
                    height += childHeight;
                    lineHeight = childHeight;
                } else {//不需要换行时
                    lineWidth += childWidth;
                    lineHeight = Math.max(lineHeight, childHeight);
                }
                if (i == getChildCount() - 1) {
                    width = Math.max(width, lineWidth);
                    height += lineHeight;
                }
            }
            //如果是wrap_content,就用计算的值,否则就用系统量测的值,至于另外的UNSPECIFIED先不考虑,毕竟用的少
            setMeasuredDimension((MeasureSpec.EXACTLY == widthMode) ? widthSize : width,
                    (MeasureSpec.EXACTLY == heightMode) ? heightSize : height);
        }
    
    • onLayout(),为每一个儿子划好地产,就是分配它们的绘制区域

      • 这一步的思路基本和onMeasure()中一样,但是定义两个list,mLines里面有所有的行,每一行的list里又存储了里面的child
    private ArrayList<ArrayList<View>> mLines = new ArrayList<>();//二级List,里面有所有的view
        private ArrayList<Integer> mLineHeight = new ArrayList<>();//记录每一行的行高
    
          for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();
                MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams();
                //先处理需要换行的情况
                if (lineWidth + childWidth + childParams.leftMargin + childParams.rightMargin > width) {
                    mLineHeight.add(lineHeight);
                    mLines.add(lineViews);
                    lineWidth = 0;
                    lineViews = new ArrayList<>();
                }
                //不用换行时,累加
                lineWidth = childWidth + childParams.leftMargin + childParams.rightMargin + lineWidth;
                lineHeight = Math.max(lineHeight, childParams.topMargin + childParams.bottomMargin + childHeight);
                lineViews.add(child);
            }
            //记录最后一行
            mLineHeight.add(lineHeight);
            mLines.add(lineViews);
    
     - 再根据mLines进行两次for循环,为每一个child定位
    
            int left = 0, top = 0;
            //根据行数和每行的view个数遍历所有view
            for (int i = 0; i < mLines.size(); i++) {
                lineViews = mLines.get(i);
                lineHeight = mLineHeight.get(i);
                //遍历当前行所有的view
                for (int j = 0; j < lineViews.size(); j++) {
                    View childView = lineViews.get(j);
                    if (childView.getVisibility() == GONE) continue;
                    MarginLayoutParams childParams = (MarginLayoutParams) childView.getLayoutParams();
                    int childLeft = left + childParams.leftMargin;
                    int childTop = top + childParams.topMargin;
                    int childBottom = childTop + childView.getMeasuredHeight();
                    int childRight = childLeft + childView.getMeasuredWidth();
                    childView.layout(childLeft, childTop, childRight, childBottom);
                    left += childView.getMeasuredWidth() + childParams.leftMargin + childParams.rightMargin;
                }
                left = 0;
                top += lineHeight;
            }
    
    • viewgroup的子类的基本方法实现完成,先在xml中引用一下

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.loneyang.someviewgroup.MainActivity">
    
        <com.loneyang.someviewgroup.veiw.FlowLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:background="@drawable/tv_bg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="VONE"
                />
    
            <TextView
                android:background="@drawable/tv_bg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="VONE"
                />
            <TextView
                android:background="@drawable/tv_bg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="这一步的思路基本和onMeasure()中一样,但是定义两个list,mLines里面有所有的行,每一行的list里又存储了里面的child"
                />
        </com.loneyang.someviewgroup.veiw.FlowLayout>
    </LinearLayout>
    

    跑一下效果如下,基本可以实现了(为了方便观察效果,textview中加了点shape属性)


    • 那么接下来就比较简单了,实现类似RadioGroup的功能,将childView上的文字加到edittext上就行了

      • 先定义一个接口:
        public interface OnChildSelectedListener{
            void onChildSelected(String content,View child);
        }
    
     - 在我们自定义的子类中,得到这个接口的实例
    
    private OnChildSelectedListener mOnChildSelectedListener;
    
        public void setOnChildSelectedListener(OnChildSelectedListener onChildSelectedListener) {
            this.mOnChildSelectedListener = onChildSelectedListener;
        }
    
     - 回到onLayout()中,在第一个for循环里,加上点击事件:
    
                final int finalI = i;
                child.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (v.getTag() == null) v.setTag(true);
                        //selectedIndex : 当前选中的child坐标,全局变量
                        if (selectedIndex != finalI) {//本次点击view不是上次选中的view
                            if (selectedIndex != -1) {
                                //将上一个被选中的child选中效果拿掉
                                getChildAt(selectedIndex).setBackground(getResources().getDrawable(R.drawable.tv_bg));
                            }
                            v.setBackground(getResources().getDrawable(R.drawable.tv_bg_selcected));//设定选中效果
                            selectedIndex = finalI;
                            TextView tv = (TextView) v;
                            childSelcted = (String) tv.getText();
                        } else {//点击的是已经选中的view,取消选中效果
                            selectedIndex = -1;
                            v.setBackground(getResources().getDrawable(R.drawable.tv_bg));
                            childSelcted = null;
                        }
                            //把当前child上的文字传过去
                            mOnChildSelectedListener.onChildSelected(childSelcted,v);
                    }
                });
    
     - 来到activity中,实现这个接口,并将被选中的文字设置到edittext中:
    
    flowLayout.setOnChildSelectedListener(new FlowLayout.OnChildSelectedListener() {
                @Override
                public void onChildSelected(String content, View child) {
                    editText.setText(content);
                }
            });
    

    搞定。

    老规矩,github上的代码,点击查看

    有什么bug,请及时指出,大家一起来解决 :)

    相关文章

      网友评论

        本文标题:android自定义ViewGroup---设计简单的FlowL

        本文链接:https://www.haomeiwen.com/subject/qzxstxtx.html