Android自定义控件之流式布局




效果图:


Android自定义控件之流式布局


一、首先创建我 们的自定义流式布局

public class FlowLayoutView extends ViewGroup {
    public FlowLayoutView(Context context) {
        this(context, null);
    }

    public FlowLayoutView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        
    }
}



二、在布局文件中使用我们的控件

<com.androidlongs.flowlayoutviewapplication.FlowLayoutView
    android:id="@+id/flowlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</com.androidlongs.flowlayoutviewapplication.FlowLayoutView>

三、在 activity中向我们的控件中添加子view



四、定义一行对象,来保存每行中所包含的控件信息

class Line{
        //用来记录当前行的所有TextView
        private ArrayList<View> viewList = new ArrayList<View>();
        //表示当前行所有TextView的宽,还有他们之间的水平间距
        private int width;
        //当前行的高度
        private int height;
		
		}
}

五、 在onMeasure方法中完成测量分行操作


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

        //1.获取FlowLayout的宽度
        int width = MeasureSpec.getSize(widthMeasureSpec);
        //2.计算用于实际比较的宽度,就是width减去左右的padding值
        int noPaddingWidth = width - getPaddingLeft()- getPaddingRight();
        //3.遍历所有的子TextView,在遍历过程中进行比较,进行分行操作
        //创建一个行
        Line  line = new Line();
        for (int i = 0; i < getChildCount(); i++) {
            final View childAt = getChildAt(i);
            //1. 当 当前的行中没有子View,那么直接将子childAt直接放入行中,而不用再比较宽度,要保证
            if(line.getViewList().size()==0){
                line.addLineView(childAt);
            }
            // 每行至少有一个子TextView
         
        }
    }

可以看到这里有一个addLineView方法,我们需要在Line中定义addLineView这个方法

Line对象的addLineView方法简析

1.当我们将view添加到Line去,我们就需要将子View添加到保存子View的集合(viewList)中去,并且要更新Line的width,
    
2.那么在更新width的过程中,如果当前添加的子View是当前行中的第一个子控件,那么当前子View的宽就是当前行的宽
    如果不是第一个,则要在当前width的基础上+水平间距+当前添加子View的宽度
3.在这里使用到和水平间距,所以需要定义当前行中的每个子View的水平间距,


private final int DEFAULT_SPACING = 10;
		private int horizontalSpacing = DEFAULT_SPACING;//水平间距

4. 通过自定义属性的方式来实现设定水平间距

1. 在values文件夹下新建attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
					<resources>
						<attr name="horizontalSpacing" format="integer"/>
						<declare-styleable name="FlowLayoutView">
							<attr name="horizontalSpacing"/>
						</declare-styleable>
					</resources>

2. 在自定义FlowLayoutView的构造方法中获取我们所设定的水平间距

public FlowLayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		//获取所有和自定义属性和样式
		final TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FlowLayoutView, defStyleAttr, 0);
		final int indexCount = typedArray.getIndexCount();
		for (int i = 0; i < indexCount; i++) {
			final int indexAttr = typedArray.getIndex(i);
			switch (indexAttr) {
			case R.styleable.FlowLayoutView_horizontalSpacing:
				horizontalSpacing = typedArray.getInt(indexAttr,10);
				break;
					 }
				}
			typedArray.recycle();
		}


5. 通过向外暴露一个方法来设置

/**
 * 设置子View直接的水平间距
 * @param horizontalSpacing
*/
public void setHorizontalSpacing(int horizontalSpacing){
		if(horizontalSpacing>0){
		this.horizontalSpacing = horizontalSpacing;
		}
	}

6. 添加子view后,更新当前行的高度,实际上在这里,每个子View的高度就是当前行的高度

public void addLineView(View lineView){
			if(!viewList.contains(lineView)){
				viewList.add(lineView);
				
				//更新width
				if(viewList.size()==1){
					//如果是第一个TextView,那么width就是lineView的宽度
					width = lineView.getMeasuredWidth();
				}else {
					//如果不是第一个,则要在当前width的基础上+水平间距+lineView的宽度
					width += horizontalSpacing + lineView.getMeasuredWidth();
				}
				//更新height,在此所有的TextView的高度都是一样的
				height = Math.max(height,lineView.getMeasuredHeight());
			}
			}

分行逻辑简析

  当当前行中已经有了子view后,当前行的宽度+水平间距+当前添加的子View的宽度大于控件的宽度的时候,需要进行换行操作,那么在换行操作之前,需要先保存之前的行,所以这里使用集合来进行保存


private ArrayList<Line> lineList = new ArrayList<FlowLayout.Line>();

  当前行的宽度+水平间距+当前添加的子View的宽度不大于控件的宽度的时候,直接将当前子View添加到当前行中去

//遍历所有的子TextView,进行分行操作
		Line line = new Line();//只要不换行,始终都是同一个Line对象
		for (int i = 0; i<getChildCount(); i++) {
		//获取子TextView
			View childView = getChildAt(i);
			//引起view的onMeasure方法回调,从而保证后面的方法能够有值
			childView.measure(0,0);
			
			//如果当前line中 没有TextView,则直接放入当前Line中
			if(line.getViewList().size()==0){
				line.addLineView(childView);
			}else if(line.getWidth()+horizontalSpacing+childView.getMeasuredWidth()>noPaddingWidth) {
				//如果当前line的宽+水平间距+childView的宽大于noPaddingWidth,则换行
				lineList.add(line);//先保存之前的line对象
				
				line = new Line();//重新创建Line
				line.addLineView(childView);//将chidlView放入新的Line
			}else {
				//如果小于noPaddingWidth,则将childView放入当前Line中
				line.addLineView(childView);
			}
			
			//7.如果当前childView是最后一个,那么就会造成最后的一个Line对象丢失,
			if(i==(getChildCount()-1)){
				lineList.add(line);//保存最后的line对象
			}
		}


设定控件加载的高度

//for循环结束后,lineList就存放了所有的Line对象,而每个line中有记录自己的所有TextView
        //为了能够垂直的摆放所有的Line的TextView,所以要给当前FlowLayout设置对应的宽高,
        //计算所需要的高度:上下的padding + 所有line的高度   + 所有line之间的垂直间距
        int height = getPaddingTop()+getPaddingBottom();
        for (int i = 0; i < lineList.size(); i++) {
            height += lineList.get(i).getHeight();
        }
        height += (lineList.size()-1)*verticalSpacing;

所以这里使用到了所有行之间的垂直间距,这里可以定义自定属性来设置,也可以设定一个调用方法来进行设置

//行与行之间的垂直间距
    private int verticalSpacing = 10;
    /**
     * 设置行与行之间的垂直间距
     * @param verticalSpacing
     */
    public void setVerticalSpacing(int verticalSpacing){
        if(verticalSpacing>0){
            this.verticalSpacing = verticalSpacing;
        }
    }

六、 在onLayout方法中进行摆放操作

1.摆放所有的行的时候,我们需要循环取出每一个行Line,其次我们再获取每个Line中的所有的子view,然后再获取每一个子view
2.再摆放每一行中的第一个子view,其次再摆放当前行中的第二个以后的子view,在排放后面的子view的时候,需要参考前面的子view

/**
	 * 摆放操作,让所有的子TextView摆放到指定的位置上面
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		int paddingLeft = getPaddingLeft();
		int paddingTop = getPaddingTop();
		for (int i = 0; i < lineList.size(); i++) {
			Line line = lineList.get(i);//获取line对象
			
			//从第二行开始,他们的top总是比上一行多一个行高+垂直间距
			if(i>0){
				paddingTop += lineList.get(i-1).getHeight()+verticalSpacing;
			}
			ArrayList<View> viewList = line.getViewList();//获取line所有的TextView
			//1.计算出当前line的留白区域的值
			int remainSpacing = getLineRemainSpacing(line);
			//2.计算每个TextView分到多少留白
			float perSpacing = remainSpacing/viewList.size();
			
			for (int j = 0; j < viewList.size(); j++) {
				View childView = viewList.get(j);//获取每个TextView
				//3.将perSpacing增加到每个TextView的宽度上
				int widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (childView.getMeasuredWidth()+perSpacing),MeasureSpec.EXACTLY);
				childView.measure(widthMeasureSpec,0);
				
				if(j==0){
					//摆放每行的第一个TextView
					childView.layout(paddingLeft,paddingTop,paddingLeft+childView.getMeasuredWidth()
							,paddingTop+childView.getMeasuredHeight());
				}else {
					//摆放后面的TextView,需要参照前一个View
					View preView = viewList.get(j-1);
					int left = preView.getRight()+horizontalSpacing;
					childView.layout(left,preView.getTop(),left+childView.getMeasuredWidth(), 
							preView.getBottom());
				}
			}
		}
	}

当一行中放不下下一个控件,但是还有很宽的空白处,这时候,我们需要将这段空白计算出来,然后将这段空白平均分配给当前行中的每个子view

/**
	 * 获取line的留白区域
	 * @param line
	 * @return
	 */
	private int getLineRemainSpacing(Line line){
		return getMeasuredWidth()-getPaddingLeft()-getPaddingRight()-line.getWidth();
	}



七、在activity中向布局中添加子View


 final FlowLayoutView viewById = (FlowLayoutView) findViewById(R.id.framlayout);
        for (int i = 0; i < mStringArray.length; i++) {
                final TextView textView = new TextView(this);
                textView.setText(mStringArray[i]);
                textView.setTextColor(Color.BLUE);
                textView.setGravity(Gravity.CENTER);
                textView.setTextSize(16);
                textView.setPadding(15, 15, 15, 15);
                viewById.addView(textView);
            }

其中mStringArray是一个保存了String的字符串数组

Android自定义控件之流式布局


效果图比较不好看Android自定义控件之流式布局


然后我们可以设置下子view的样式背景


 Drawable normal = generateDrawable(randomColor(), 10);
            Drawable pressed = generateDrawable(randomColor(), 10);
            textView.setBackgroundDrawable(generateSelector(pressed, normal));

这里是我们在java代码中动态创建状态选择器,其中文字的背景是随机生成 的

 public static StateListDrawable generateSelector(Drawable pressed,Drawable normal){
        StateListDrawable drawable = new StateListDrawable();
        drawable.addState(new int[]{android.R.attr.state_pressed}, pressed);//设置按下的图片
        drawable.addState(new int[]{}, normal);//设置默认的图片
        return drawable;
    }
    
    public  GradientDrawable generateDrawable(int argb,float radius){
        GradientDrawable drawable = new GradientDrawable();
        drawable.setShape(GradientDrawable.RECTANGLE);//设置为矩形,默认就是矩形
        drawable.setCornerRadius(radius);//设置圆角的半径
        drawable.setColor(argb);
        return drawable;
    }
    /**
     * 随机生成漂亮的颜色
     * @return
     */
    public  int randomColor(){
        Random random = new Random();
        //如果值太大,会偏白,太小则会偏黑,所以需要对颜色的值进行范围限定
        int red = random.nextInt(150)+50;//50-199
        int green = random.nextInt(150)+50;//50-199
        int blue = random.nextInt(150)+50;//50-199
        return Color.rgb(red, green, blue);//根据rgb混合生成一种新的颜色
    }



Android自定义控件之流式布局




点击下载本节源码

访问密码:4vrl






 Android自定义控件ImageViwe(一)——依据控件的大小来设置缩放图片显示
   点击打开链接
    
 Android自定义ImageView(二)——实现双击放大与缩小图片
   点击打开链接
    
 Android自定义控件ImageViwe(三)——随手指进行图片的缩放
    点击打开链接
    
 Android自定义控件ImageViwe(四)——多点触控实现图片的*移动  
    点击打开链接
    
 Android ListView分组排序显示数据
    点击打开链接
    
 Android自定义下拉刷新功能的ListView
   点击打开链接
    
 Android音乐播放器高级开发
   点击打开链接
   




本章智力解答:  在一个房间里,有油灯 ,暖炉及壁炉。现在,想要用一根火柴将三个器具点燃,请问首先应该点燃哪一个?


都不是 ,应当先点火柴,只有点燃了火柴才能去点其他东西












上一篇:一手遮天 Android - view(基础): 位置相关


下一篇:一个自动换行,不可以滚动的 textview