效果图:
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>
class Line{ //用来记录当前行的所有TextView private ArrayList<View> viewList = new ArrayList<View>(); //表示当前行所有TextView的宽,还有他们之间的水平间距 private int width; //当前行的高度 private int height; } }
@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 } }
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>();
//遍历所有的子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; } }
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(); }
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); }
效果图比较不好看
然后我们可以设置下子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混合生成一种新的颜色 }
点击下载本节源码
访问密码:4vrl
Android自定义控件ImageViwe(一)——依据控件的大小来设置缩放图片显示
点击打开链接
Android自定义ImageView(二)——实现双击放大与缩小图片
点击打开链接
Android自定义控件ImageViwe(三)——随手指进行图片的缩放
点击打开链接
Android自定义控件ImageViwe(四)——多点触控实现图片的自由移动
点击打开链接
Android ListView分组排序显示数据
点击打开链接
Android自定义下拉刷新功能的ListView
点击打开链接
Android音乐播放器高级开发
点击打开链接
本章智力解答: 在一个房间里,有油灯 ,暖炉及壁炉。现在,想要用一根火柴将三个器具点燃,请问首先应该点燃哪一个?
都不是 ,应当先点火柴,只有点燃了火柴才能去点其他东西