Android从零开搞系列:自定义View(10)流式布局

转载请注意:http://blog.csdn.net/wjzj000/article/details/65936007


我和一帮应届生同学维护了一个公众号:IT面试填坑小分队。旨在帮助应届生从学生过度到开发者,并且每周树立学习目标,一同进步!
这里写图片描述


写在前面

这段时间被国足踢赢韩国刷屏,确实很解气。天朝有一百种方式赢棒子,非逼我们用这种方式出手…

这次博客记录一个自定义的ViewGroup,比较常见的效果:流式布局。

简单效果如下:
这里写图片描述

此效果来自开源库:https://github.com/crazyandcoder/MultiLineChoose
关于使用,各位看官感兴趣可以移步到大神的GitHub上一睹王者之霸气。本篇博客是我在看大神源码过程之中的总结和记录。

开始

针对这种布局,我们可以想到:继承ViewGroup重写onLayout方法,计算子控件的宽度,如果大于父控件的宽度,那我们就让子控件在下一行进行layout。Ok,让我们带着思路来看一下大神的源码是怎么做的。

首先如我们所想:

继承了ViewGroup

public class MultiLineChooseLayout extends ViewGroup

紧接着就是正常的在构造方法中完成对自定义属性的初始化:

final TypedArray attrsArray = context.obtainStyledAttributes(attrs,
R.styleable.MultiLineChooseItemTags,defStyleAttr,R.style.MultiLineChooseItemTags);

textColor = attrsArray.getColor(R.styleable.MultiLineChooseItemTags_item_textColor,default_text_color);

backgroundColor =attrsArray.getColor(R.styleable.MultiLineChooseItemTags_item_backgroundColor,default_background_color);

selectedTextColor =attrsArray.getColor(R.styleable.MultiLineChooseItemTags_item_selectedTextColor,default_checked_text_color);

//省略部分初始化         

onMeasure方法:

在这里我们需要小小的注意一下。onMeasure之中我们在处理warp_content时要考虑子View各个位置的情况,因为子View有可能排列时会大于一行,也有可能不足一行。

源码如下:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //调用此方法后,我们可以获取通过子View的getMeasuredWidth/getMeasuredHeight获取子View的宽高信息。
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int width = 0;
        int height = 0;

        int row = 0; // The row counter.
        int rowWidth = 0; // Calc the current row width.
        int rowMaxHeight = 0; // Calc the max tag height, in current row.

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            if (child.getVisibility() != GONE) {
                rowWidth += childWidth;
                if (rowWidth > widthSize) { 
                    // 下一行
                    rowWidth = childWidth; 
                    // 下一行宽度。
                    height += rowMaxHeight + verticalSpacing;
                    rowMaxHeight = childHeight; 
                    // 下一行最大高度。
                    row++;
                }
                else { 
                    // 这一行。
                    rowMaxHeight = Math.max(rowMaxHeight, childHeight);
                }
                rowWidth += horizontalSpacing;
            }
        }
        height += rowMaxHeight;

        height += getPaddingTop() + getPaddingBottom();

        if (row == 0) {
            //只有一行item
            width = rowWidth;
            width += getPaddingLeft() + getPaddingRight();
        }
        else {
            // 如果分组的标签超过一行,请设置宽度以匹配父级。
            width = widthSize;
        }

        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
                heightMode == MeasureSpec.EXACTLY ? heightSize : height);
    }

上诉的代码都是套路性的东西。通过遍历子View来计算是否占够一行的宽度,如果够那么就下一行。并且设置父View的宽度为行宽;否则便是子View一共多宽父View就有多宽。


onLayout方法:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //设置父View的起始和终止点
        final int parentLeft = getPaddingLeft();
        final int parentRight = r - l - getPaddingRight();
        final int parentTop = getPaddingTop();
        final int parentBottom = b - t - getPaddingBottom();

        int childLeft = parentLeft;
        int childTop = parentTop;

        int rowMaxHeight = 0;

        final int count = getChildCount();
        //遍历子View进行,计算宽高,然后调用layout
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            if (child.getVisibility() != GONE) {
                // 如果当前View的位置大于父View的宽度,那么就放置到下一行
                if (childLeft + width > parentRight) { 
                    childLeft = parentLeft;
                    childTop += rowMaxHeight + verticalSpacing;
                    rowMaxHeight = height;
                }
                else {
                    rowMaxHeight = Math.max(rowMaxHeight, height);
                }
                child.layout(childLeft, childTop, childLeft + width, childTop + height);

                childLeft += width + horizontalSpacing;
            }
        }
    }

Ok,截止到这,我们可以在MultiLineChooseLayout这个布局之中加入子View进行正常的流式显示。但是我们需要的是动态的添加子View,因此仅有这些是肯定不够的。


动态添加子View

我们正常使用的时候是这样色的:

List<String> mDataList = new ArrayList<>();
mDataList.add("赵云");
mDataList.add("关羽");
mDataList.add("张飞");
mDataList.add("黄忠");
mDataList.add("马超");
mDataList.add("吕布");
mDataList.add("高顺");
mDataList.add("张辽");
mDataList.add("诸葛亮");
singleChoose.setList(mDataList);

通过使用我们可以看出来,我们通过MultiLineChooseLayout的setList方法进行动态设置显示内容。

接下来让我们看代码:
public void setList(List<String> tagList) {
    setList(tagList.toArray(new String[tagList.size()]));
}

//setList
public void setList(String... tags) {
    removeAllViews();
    for (final String tag : tags) {
        addItem(tag);
    }
}

//addItem
private void addItem(CharSequence tag) {
    final ItemView item = new ItemView(getContext(), tag);
    item.setOnClickListener(mInternalTagClickListener);
    //此处,通过new自定义的ItemView,然后设置我们setList中的值。调用addView传入这个对象
    addView(item);
}
ItemView:
class ItemView extends TextView
//构造方法之中,除了进行一些初始化以为。就属这行代码比较特殊
setLayoutParams(new MultiLineChooseLayout.LayoutParams(itemWidth, itemHeight));

可以看出,MultiLineChooseLayout.LayoutParams仅仅是继承了ViewGroup.LayoutParams的一个类而已,并未做特殊处理。

public static class LayoutParams extends ViewGroup.LayoutParams {
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }
}

接下来我们就看一下onDraw方法:

@Override
protected void onDraw(Canvas canvas) {
    //在正常绘制TextView之前,我们要进行一些自己的绘制处理
    if (!animUpdateDrawable) {
        updateDrawable();
    }
    super.onDraw(canvas);
}

//updateDrawable()
private void updateDrawable() {
    //进行画框
    mStrokeColor = mStrokeColor == null ? ColorStateList.valueOf(Color.TRANSPARENT) : mStrokeColor;

    mCheckedStrokeColor = mCheckedStrokeColor == null ? mStrokeColor : mCheckedStrokeColor;

    updateDrawable(!isChecked ? mStrokeColor.getDefaultColor() : mCheckedStrokeColor.getDefaultColor());
}

//updateDrawable(int strokeColor)
private void updateDrawable(int strokeColor) {
    int mbackgroundColor;
    if (isChecked) {
        mbackgroundColor = selectedBackgroundColor;
    }
    else {
        mbackgroundColor = backgroundColor;
    }

    GradientDrawable drawable = new GradientDrawable();
    drawable.setCornerRadii(mRadius);
    drawable.setColor(mbackgroundColor);
    drawable.setStroke(mStrokeWidth, strokeColor);

    if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
        this.setBackgroundDrawable(drawable);
    }
    else {
        this.setBackground(drawable);
    }
}

然后就是onTouchEvent()方法:

        @Override
        public boolean onTouchEvent(MotionEvent event) {

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    getDrawingRect(mOutRect);
                    invalidatePaint();
                    invalidate();
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    if (!mOutRect.contains((int) event.getX(), (int) event.getY())) {
                        invalidatePaint();
                        invalidate();
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    invalidatePaint();
                    invalidate();
                    break;
                }
            }
            return super.onTouchEvent(event);
        }

        //invalidatePaint()
        private void invalidatePaint() {

            animUpdateDrawable = false;

            if (isChecked) {
                mBackgroundPaint.setColor(selectedBackgroundColor);
                setTextColor(selectedTextColor);
            }
            else {
                mBackgroundPaint.setColor(backgroundColor);
                setTextColor(textColor);
            }

        }

到此我们自定义的这个字View就被添加到了父View之中,说白了就是addView的功能。当然我们也可以使用LayoutInflate来构建自己的子View。


监听事件

监听事件的使用也和我们正常写回调的方式没有什么不同,而且我们在分析addView的时候我们就已经见到了监听的使用。

private void addItem(CharSequence tag) {
    final ItemView item = new ItemView(getContext(), tag);
    item.setOnClickListener(mInternalTagClickListener);
    addView(item);
}

让我们来看一看ItemClicker的代码是怎么写的:


    public void setOnItemClickListener(onItemClickListener l) {
        mOnItemClickLisener = l;
    }


    class ItemClicker implements OnClickListener {
        @Override
        public void onClick(View v) {
            final ItemView tag = (ItemView) v;
            int position = -1;
            //getSelectedItem()方法会通过遍历所有的ItemView中的isChecked来拿到被选中的ItemView
            final ItemView checkedTag = getSelectedItem();
            if (!multiChooseable) {
                //单选
                if (checkedTag != null) {
                    checkedTag.setItemSelected(false);
                }

                tag.setItemSelected(true);
                position = getSelectedIndex();
            }
            else {
                //多选
                tag.setItemSelected(!tag.isChecked);
                position = -1;
            }

            //外部点击事件,完成回调
            if (mOnItemClickLisener != null) {
                mOnItemClickLisener.onItemClick(position, tag.getText().toString());
            }

        }
    }

而我们使用的时候,就可以直接set

        singleChoose.setOnItemClickListener(new MultiLineChooseLayout.onItemClickListener() {
            @Override
            public void onItemClick(int position, String text) {
                singleChooseTv.setText("结果:position: " + position + "   " + text);
            }
        });

尾声

Ok,整体View的思路就是如此,当然我们还需要处理细节之处,因为整个项目是开源的。因此各位看官如果有特殊需求服务….移步大神的GitHub。

最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值