Android 自定义View实现流式布局——彩色背景

效果图:

概述

1.流式布局原理:

在布局内,随意摆放任意个view,每行所摆放的view个数,根据实施计算出来的宽度,一旦当前要摆放的view宽度和之前摆放的所有view宽度加在一起,超过了布局的宽度,那么就把该view换行摆放

2.应用场景:

一般,像这种流式布局会应用在一些热门标签,热门推荐之类的应用上

3.测量模式:

谈到FlowLayout流式布局,不得不提及他的测量模式:

* MeasureSpec.EXACTLY:精确模式, eg:100dp,match_parent.(明确指出)

* MeasureSpec.AT_MOST: 至多模式, view最多可以获得的宽高值,它需要计算所有包含的子view的宽高,最后计算出来的宽高总和值,eg:wrap_content.

* UNSPECIFIED:未指定模式,想设置多宽多高,就给你多宽多高,一般的控件不会指定这种模式,但也存在,这种模式用的不多。eg:scrollview的宽高测量,就是使用的此种模式

4.在我们的流式布局内,应该怎么设置布局的宽高呢? onMeasure()

1:如果布局指定的宽是match_parent或者精确的宽度值,那么直接就可以从父控件传入的测量规格中直接获取布局宽度,高度同理.

2:如果布局指定的宽高不是EXACTLY,而是AT_MOST,那么这时候,就需要计算每一个子view的宽高,来决定布局的宽高了。

宽度:摆放的所有子view占据宽度最多的一行,作为布局宽度。

高度:摆放的所有子view总共占据几行的高度总和。

5.子View的布局方式: onLayout()

使用onLayout():设置ViewGroup内包含的所有子view的位置; 
获取到每一行的每一个子view,计算出它的left,top,right,bottom,调用layout方法设置其在流式布局当中的位置。

宽度=子view最多的那行的宽度=那一行每一个子view的宽度+leftMargin+rightMargin;

高度=所有行的高度 = 每一行的高度+topMargin+bottomMargin;

LayoutParams参数的设置

ViewGroup LayoutParams :每个 ViewGroup 对应一个 LayoutParams; 即 ViewGroup -> LayoutParams 
getLayoutParams 不知道转为哪个对应的LayoutParams ,其实很简单,就是如下: 
子View.getLayoutParams 得到的LayoutParams对应的就是 子View所在的父控件的LayoutParams; 
例如,LinearLayout 里面的子view.getLayoutParams ->LinearLayout.LayoutParams 
所以 咱们的FlowLayout 也需要一个LayoutParams,由于上面的效果图是子View的 margin, 
所以应该使用MarginLayoutParams。即FlowLayout->MarginLayoutParams

自定义ViewGroup的实现流式布局

根据上面的技术分析,自定义类继承于ViewGroup,并重写 onMeasure和onLayout等方法。具体实现代码如下:

package com.zhuoshi.inspecting.mvp.ui.view.views;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
    public FlowLayout(Context context) {
        this(context, null);
    }
    //这个方法必须实现
    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //布局:给每一个子view布局,childView.layout(l,t,r,b)
    private List<Integer> allHeights = new ArrayList<>();//集合中的元素:记录每一行的高度
    private List<List<View>> allViews = new ArrayList<>();//外层集合中的元素:由每行元素构成的集合

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int width = this.getWidth();//得到父视图的宽度

        int lineWidth = 0;
        int lineHeight = 0;

        // 一、给集合元素赋值
        int childCount = getChildCount();
        List<View> lineList = new ArrayList<>();//一行元素构成的集合
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //子视图的宽高
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();
            //获取视图的边距
            MarginLayoutParams mp = (MarginLayoutParams) childView.getLayoutParams();
            if (lineWidth + childWidth + mp.leftMargin + mp.rightMargin < width) {//不换行
                lineList.add(childView);//添加子视图到集合中
                lineWidth += childWidth + mp.leftMargin + mp.rightMargin;
                lineHeight = Math.max(lineHeight, childHeight + mp.topMargin + mp.bottomMargin);
            } else {//换行
                allViews.add(lineList);
                allHeights.add(lineHeight);

                //换行以后需要执行的情况
                lineList = new ArrayList<>();
                lineList.add(childView);
                lineWidth = childWidth + mp.leftMargin + mp.rightMargin;
                lineHeight = childHeight + mp.topMargin + mp.bottomMargin;
            }

            if (i == childCount - 1) {//如果最后一个元素
                allViews.add(lineList);
                allHeights.add(lineHeight);
            }
        }


        Log.e("TAG", "allViews.size()==" + allViews.size() + "allHeights.size()==" + allHeights.size());

        //二、遍历集合元素,调用元素的layout()

        int x = 0;
        int y = 0;

        for (int i = 0; i < allViews.size(); i++) {
            List<View> lineViews = allViews.get(i);//获取每一行的集合
            for (int j = 0; j < lineViews.size(); j++) {
                View childView = lineViews.get(j);//获取一行的指定的j位置

                MarginLayoutParams mp = (MarginLayoutParams) childView.getLayoutParams();
                //计算的到left,top,right,bottom
                int left = x + mp.leftMargin;
                int top = y + mp.topMargin;
                int right = left + childView.getMeasuredWidth();
                int bottom = top + childView.getMeasuredHeight();

                childView.layout(left, top, right, bottom);

                //重新赋值x,y
                x += childView.getMeasuredWidth() + mp.leftMargin + mp.rightMargin;
            }

            //换行
            x = 0;
            y += allHeights.get(i);
        }
    }

    //测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //获取宽度和高度的布局的数值,以及各自的设计模式,精确模式,至多模式
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //声明当前视图的宽和高,如果是至多模式,需要计算出此两个变量的值
        int width = 0;
        int height = 0;

        //声明每行的宽度和高度
        int lineWidth = 0;
        int lineHeight = 0;

        int childCount = getChildCount();//获取子视图的个数
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);

            //为了保证能够获取子视图的测量的宽高,需要调下面的方法
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            //获取子视图测量的宽高
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //获取视图的边距
            MarginLayoutParams mp = (MarginLayoutParams) childView.getLayoutParams();

            if (lineWidth + childWidth + mp.leftMargin + mp.rightMargin <= widthSize) {//不换行
                lineWidth += childWidth + mp.leftMargin + mp.rightMargin;
                lineHeight = Math.max(lineHeight, childHeight + mp.topMargin + mp.bottomMargin);
            } else {//换行
                width = Math.max(width, lineWidth);
                height += lineHeight;

                //重新赋值
                lineWidth = childWidth + mp.leftMargin + mp.rightMargin;
                lineHeight = childHeight + mp.topMargin + mp.bottomMargin;

            }
            //单独的考虑一下最后一个!因为最后一个元素并没有计算进去
            if (i == childCount - 1) {
                width = Math.max(width, lineWidth);
                height += lineHeight;
            }
        }

        Log.e("TAG", "width ==" + width + ",height==" + height);
        Log.e("TAG", "widthSize ==" + widthSize + ",heightSize==" + heightSize);

        //调用此方法,设置当前布局的宽高
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
                heightMode == MeasureSpec.EXACTLY ? heightSize : height);

    }
    //FlowLayout中有了如下的方法,在onMeasure()中可通过child就可以getLayoutParams()
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        MarginLayoutParams mp = new MarginLayoutParams(getContext(), attrs);
        return mp;

    }


}

需要用到的工具类:

------------------------DrawUtils------------------------

package com.zhuoshi.inspecting.app.utils;

import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;

public class DrawUtils {
    
    //提供一个指定颜色和圆角半径的Drawable对象
    public static GradientDrawable getDrawable(int rgb,float radius){
        GradientDrawable gradientDrawable = new GradientDrawable();
        gradientDrawable.setColor(rgb);//设置颜色
        gradientDrawable.setGradientType(GradientDrawable.RECTANGLE);//设置显示的样式
        gradientDrawable.setCornerRadius(radius);//设置圆角的半径
        gradientDrawable.setStroke(UIUtils.dp2px(1),rgb);//描边
        return gradientDrawable;
    }

    public static StateListDrawable getSelector(Drawable normalDrawable, Drawable pressDrawable) {
        StateListDrawable stateListDrawable = new StateListDrawable();
        //给当前的颜色选择器添加选中图片指向状态,未选中图片指向状态
        stateListDrawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, pressDrawable);
        stateListDrawable.addState(new int[]{android.R.attr.state_enabled}, normalDrawable);
        //设置默认状态
        stateListDrawable.addState(new int[]{}, normalDrawable);
        return stateListDrawable;
    }

}

-------------------------MyApplication-----------------------

package com.zhuoshi.inspecting.app;

import android.content.Context;

public class MyApplication extends Application{

    private static MyApplication instance;
    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        

    }


    public static MyApplication getInstance(){
        return instance;
    }

}

--------------------------UIUtils--------------------------

package com.zhuoshi.inspecting.app.utils;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.zhuoshi.inspecting.app.MyApplication;

public class UIUtils {
    /**
     * @return 应用的上下文
     */
    public static Context getContext() {
        return MyApplication.getInstance();
    }

    /**
     * 获取资源对象
     */
    public static Resources getResources() {
        return getContext().getResources();
    }

    /**
     * @param id
     * @return 资源文件字符串
     */
    public static String getString(int id) {
        return getResources().getString(id);
    }

    /**
     * @param id
     * @return 资源文件字符串数组
     */
    public static String[] getStringArray(int id) {
        return getResources().getStringArray(id);
    }

    /**
     * @param id
     * @return 资源文件图片
     */
    public static Drawable getDrawable(int id) {
        return ContextCompat.getDrawable(getContext(), id);
    }

    /**
     * @param id
     * @return 资源文件颜色
     */
    public static int getColor(int id) {
        return ContextCompat.getColor(getContext(), id);
    }

    /**
     * @param id
     * @return 颜色的状态选择器
     */
    public static ColorStateList getColorStateList(int id) {
        return ContextCompat.getColorStateList(getContext(), id);
    }

    /**
     * @param id
     * @return 尺寸
     */
    public static int getDimen(int id) {
        // 返回具体像素值
        return getResources().getDimensionPixelSize(id);
    }

    /**
     * dp ->px
     *
     * @param dp
     * @return
     */
    public static int dp2px(float dp) {
        float density = getResources().getDisplayMetrics().density;
        return (int) (dp * density + 0.5f);
    }

    /**
     * px ->dp
     *
     * @param px
     * @return
     */
    public static float px2dp(int px) {
        float density = getResources().getDisplayMetrics().density;
        return px / density;
    }

    /**
     * 加载布局文件
     *
     * @param id
     * @return
     */
    public static View inflate(int id) {
        return View.inflate(getContext(), id, null);
    }

    /**
     * 把自身从父View中移除
     *
     * @param view
     */
    public static void removeSelfFromParent(View view) {
        if (view != null) {
            ViewParent parent = view.getParent();
            if (parent != null && parent instanceof ViewGroup) {
                ViewGroup group = (ViewGroup) parent;
                group.removeView(view);
            }
        }
    }

    /**
     * 请求View树重新布局,用于解决中层View有布局状态而导致上层View状态断裂
     *
     * @param view
     * @param isAll
     */
    public static void requestLayoutParent(View view, boolean isAll) {
        ViewParent parent = view.getParent();
        while (parent != null && parent instanceof View) {
            if (!parent.isLayoutRequested()) {
                parent.requestLayout();
                if (!isAll) {
                    break;
                }
            }
            parent = parent.getParent();
        }
    }

    /**
     * 判断触点是否落在该View上
     *
     * @param ev
     * @param v
     * @return
     */
    public static boolean isTouchInView(MotionEvent ev, View v) {
        int[] vLoc = new int[2];
        v.getLocationOnScreen(vLoc);
        float motionX = ev.getRawX();
        float motionY = ev.getRawY();
        return motionX >= vLoc[0] && motionX <= (vLoc[0] + v.getWidth())
                && motionY >= vLoc[1] && motionY <= (vLoc[1] + v.getHeight());
    }

    /**
     * findViewById的泛型封装,减少强转代码
     *
     * @param layout
     * @param id
     * @param <T>
     * @return
     */
    public static <T extends View> T findViewById(View layout, int id) {
        return (T) layout.findViewById(id);
    }

    /**
     * *获取屏幕的比例
     *
     * @param context *@return
     */
    public static float getScaledDensity(Context context) {
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        float value = dm.scaledDensity;
        return value;
    }

    /**
     * 获取控件的高度,如果获取的高度为0,则重新计算尺寸后再返回高度
     *
     * @param view
     * @return
     */
    public static int getViewMeasuredHeight(View view) {
        calcViewMeasure(view);
        return view.getMeasuredHeight();
    }

    /**
     * 获取控件的宽度,如果获取的宽度为0,则重新计算尺寸后再返回宽度
     *
     * @param view
     * @return
     */
    public static int getViewMeasuredWidth(View view) {
        calcViewMeasure(view);
        return view.getMeasuredWidth();
    }

    /**
     * 测量控件的尺寸
     *
     * @param view
     */
    public static void calcViewMeasure(View view) {
        int width = View.MeasureSpec.makeMeasureSpec(0,
                View.MeasureSpec.UNSPECIFIED);
        int expandSpec = View.MeasureSpec.makeMeasureSpec(
                Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
        view.measure(width, expandSpec);
    }

    /**
     * 设置textview指定文字为某一颜色
     *
     * @param content 显示的文字
     * @param color   需要转换成的颜色值
     * @param start   需要变色文字开始位置
     * @param end     需要变色文字结束位置
     */
    public static SpannableStringBuilder changeTextColor(String content, int color, int start, int end) {
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(content);
        spannableStringBuilder.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return spannableStringBuilder;
    }


}

在布局文件中加入自定义的flowLayout

​
<?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"
    android:orientation="vertical">

    <com.example.administrator.p2pinvest.ui.FlowLayout
        android:id="@+id/flow_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.example.administrator.p2pinvest.ui.FlowLayout>
</LinearLayout>


​

初始化布局

private FlowLayout flowLayout;
flowLayout = view.findViewById(R.id.flow_layout);

提供页面要显示的数据,这个数据也可以放在服务器中进行联网获取

private String[] datas = new String[]{"重点关注车辆", "逾期未年检车辆", "疑似套牌车辆", "本地布控车辆",
            "违法未处理车辆", "黑名单车辆", "重点区域车辆"};
 private Random random;

在其他类中直接进行调用即可

 @Override
    public void initData(String content) {

        random = new Random();
        for(int i = 0; i < datas.length; i++) {
            final TextView textView = new TextView(getActivity());
            textView.setText(datas[i]);
            //提供边距的对象,并设置到textView中
            ViewGroup.MarginLayoutParams mp = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
            mp.leftMargin = UIUtils.dp2px(8);
            mp.rightMargin = UIUtils.dp2px(8);
            mp.topMargin = UIUtils.dp2px(8);
            mp.bottomMargin = UIUtils.dp2px(8);
            textView.setLayoutParams(mp);

            //设置背景
            //设置textView的背景
            int red = random.nextInt(211);
            int green = random.nextInt(211);
            int blue = random.nextInt(211);
            //方式一:
//            textView.setBackground(DrawUtils.getDrawable(Color.rgb(red, green, blue),UIUtils.dp2px(5)));

            //方式二:
            //保存按下能显示selector的效果,需要设置一个如下的属性
            textView.setBackground(DrawUtils.getSelector(DrawUtils.getDrawable(Color.rgb(red, green, blue),UIUtils.dp2px(5)),DrawUtils.getDrawable(Color.WHITE,UIUtils.dp2px(5))));
            //方式一:
//            textView.setClickable(true);

            //添加点击事件,也是实现显示selector的效果的一种方式
            textView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(ProductHotFragment.this.getActivity(), textView.getText(), Toast.LENGTH_SHORT).show();
                }
            });

            //设置边距
            //设置内边距
            int padding = UIUtils.dp2px(10);
            textView.setPadding(padding, padding, padding, padding);


            // 2.添加到FlowLayout布局中
            flowLayout.addView(textView);
        }
    }

猜你喜欢

转载自blog.csdn.net/qq_40116418/article/details/84789426