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

来源:互联网 发布:目前常见数据库有哪些 编辑:程序博客网 时间:2024/05/20 04:28

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

本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)


写在前面

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

这次博客记录一个自定义的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()]));}//setListpublic void setList(String... tags) {    removeAllViews();    for (final String tag : tags) {        addItem(tag);    }}//addItemprivate 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方法:

@Overrideprotected 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

1 0
原创粉丝点击