效果圖
item 布局文件kingoit_flow_layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingBottom="@dimen/margin5"
android:paddingEnd="@dimen/margin5"
android:paddingRight="@dimen/margin10"
tools:ignore="RtlSymmetry">
<TextView
android:id="@+id/value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:lines="1"
android:paddingBottom="2dp"
android:paddingTop="2dp"
android:textColor="#000000"
android:textSize="16sp"/>
<ImageView
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/value"
android:layout_alignEnd="@id/value"
android:layout_alignRight="@id/value"
android:layout_alignTop="@+id/value"
android:layout_marginEnd="-2dp"
android:layout_marginRight="-2dp"
android:src="@drawable/cancel"
android:tint="@color/colorDialogContent"
android:visibility="gone"/>
</RelativeLayout>
設(shè)置屬性 res\values\attrs.xml
<!--KingoitFlowLayout 恶耽,流式布局屬性設(shè)置-->
<declare-styleable name="KingoitFlowLayout">
<attr name="flowLayoutRadius" format="dimension"/>
<attr name="flowLayoutTextColor" format="color"/>
<attr name="flowLayoutTextColorSelector" format="color"/>
<attr name="flowLayoutTextSize" format="dimension"/>
<attr name="flowLayoutLineColor" format="color"/>
<attr name="flowLayoutLineWidth" format="dimension"/>
<attr name="flowLayoutBackgroundColor" format="color"/>
<attr name="flowLayoutBackgroundColorSelector" format="color"/>
<attr name="flowLayoutDeleteBtnColor" format="color"/>
</declare-styleable>
控件實現(xiàn) KingoitFlowLayout
/**
* kingoit,流式布局
* 20180815-修復(fù)不可滑動問題
* @author zuo
* @date 2018/7/16 11:48
*/
public class KingoitFlowLayout extends ViewGroup {
//記錄每個View的位置
private List<ChildPos> mChildPos = new ArrayList<ChildPos>();
private float textSize;
private int textColor;
private int textColorSelector;
private float shapeRadius;
private int shapeLineColor;
private int shapeBackgroundColor;
private int shapeBackgroundColorSelector;
private float shapeLineWidth;
private int deleteBtnColor;
/**
* 是否是可刪除模式
*/
private boolean isDeleteMode;
/**
* 記錄所有選中著的詞
*/
private List<String> mAllSelectedWords = new ArrayList<>();
private class ChildPos {
int left, top, right, bottom;
public ChildPos(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
}
public KingoitFlowLayout(Context context) {
this(context, null);
}
public KingoitFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
initAttributes(context, attrs);
}
/**
* 最終調(diào)用這個構(gòu)造方法
*
* @param context 上下文
* @param attrs xml屬性集合
* @param defStyle Theme中定義的style
*/
public KingoitFlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 流式布局屬性設(shè)置
*
* @param context
* @param attrs
*/
@SuppressLint("ResourceAsColor")
private void initAttributes(Context context, AttributeSet attrs) {
@SuppressLint("Recycle")
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.KingoitFlowLayout);
textSize = typedArray.getDimension(R.styleable.KingoitFlowLayout_flowLayoutTextSize, 16);
textColor = typedArray.getColor(R.styleable.KingoitFlowLayout_flowLayoutTextColor, Color.parseColor("#FF4081"));
textColorSelector = typedArray.getResourceId(R.styleable.KingoitFlowLayout_flowLayoutTextColorSelector, 0);
shapeRadius = typedArray.getDimension(R.styleable.KingoitFlowLayout_flowLayoutRadius, 40f);
shapeLineColor = typedArray.getColor(R.styleable.KingoitFlowLayout_flowLayoutLineColor, Color.parseColor("#ADADAD"));
shapeBackgroundColor = typedArray.getColor(R.styleable.KingoitFlowLayout_flowLayoutBackgroundColor, Color.parseColor("#c5cae9"));
shapeBackgroundColorSelector = typedArray.getResourceId(R.styleable.KingoitFlowLayout_flowLayoutBackgroundColorSelector, 0);
shapeLineWidth = typedArray.getDimension(R.styleable.KingoitFlowLayout_flowLayoutLineWidth, 4f);
deleteBtnColor = typedArray.getColor(R.styleable.KingoitFlowLayout_flowLayoutDeleteBtnColor, Color.GRAY);
}
/**
* 測量寬度和高度
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取流式布局的寬度和模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//獲取流式布局的高度和模式
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//使用wrap_content的流式布局的最終寬度和高度
int width = 0, height = 0;
//記錄每一行的寬度和高度
int lineWidth = 0, lineHeight = 0;
//得到內(nèi)部元素的個數(shù)
int count = getChildCount();
mChildPos.clear();
for (int i = 0; i < count; i++) {
//獲取對應(yīng)索引的view
View child = getChildAt(i);
//測量子view的寬和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//子view占據(jù)的寬度
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//子view占據(jù)的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//換行
if (lineWidth + childWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
//取最大的行寬為流式布局寬度
width = Math.max(width, lineWidth);
//疊加行高得到流式布局高度
height += lineHeight;
//重置行寬度為第一個View的寬度
lineWidth = childWidth;
//重置行高度為第一個View的高度
lineHeight = childHeight;
//記錄位置
mChildPos.add(new ChildPos(
getPaddingLeft() + lp.leftMargin,
getPaddingTop() + height + lp.topMargin,
getPaddingLeft() + childWidth - lp.rightMargin,
getPaddingTop() + height + childHeight - lp.bottomMargin));
} else { //不換行
//記錄位置
mChildPos.add(new ChildPos(
getPaddingLeft() + lineWidth + lp.leftMargin,
getPaddingTop() + height + lp.topMargin,
getPaddingLeft() + lineWidth + childWidth - lp.rightMargin,
getPaddingTop() + height + childHeight - lp.bottomMargin));
//疊加子View寬度得到新行寬度
lineWidth += childWidth;
//取當(dāng)前行子View最大高度作為行高度
lineHeight = Math.max(lineHeight, childHeight);
}
//最后一個控件
if (i == count - 1) {
width = Math.max(lineWidth, width);
height += lineHeight;
}
}
// 得到最終的寬高
// 寬度:如果是AT_MOST模式密任,則使用我們計算得到的寬度值,否則遵循測量值
// 高度:只要布局中內(nèi)容的高度大于測量高度偷俭,就使用內(nèi)容高度(無視測量模式)浪讳;否則才使用測量高度
int flowLayoutWidth = widthMode == MeasureSpec.AT_MOST ? width + getPaddingLeft() + getPaddingRight() : widthSize;
int flowLayoutHeight = heightMode == MeasureSpec.AT_MOST ? height + getPaddingTop() + getPaddingBottom() : heightSize;
//真實高度
realHeight = height + getPaddingTop() + getPaddingBottom();
//測量高度
measuredHeight = heightSize;
if (heightMode == MeasureSpec.EXACTLY) {
realHeight = Math.max(measuredHeight, realHeight);
}
scrollable = realHeight > measuredHeight;
// 設(shè)置最終的寬高
setMeasuredDimension(flowLayoutWidth, flowLayoutHeight);
}
/**
* 讓ViewGroup能夠支持margin屬性
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
/**
* 設(shè)置每個View的位置
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
ChildPos pos = mChildPos.get(i);
//設(shè)置View的左邊、上邊涌萤、右邊底邊位置
child.layout(pos.left, pos.top, pos.right, pos.bottom);
}
}
public void addItemView(LayoutInflater inflater, String tvName) {
//加載 ItemView并設(shè)置名稱淹遵,并設(shè)置名稱
View view = inflater.inflate(R.layout.kingoit_flow_layout, this, false);
ImageView delete = view.findViewById(R.id.delete);
if (isDeleteMode) {
delete.setVisibility(VISIBLE);
} else {
delete.setVisibility(GONE);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
delete.setImageTintList(ColorStateList.valueOf(deleteBtnColor));
}
TextView textView = view.findViewById(R.id.value);
textView.setTextSize(textSize / getContext().getResources().getDisplayMetrics().scaledDensity);
if (textColorSelector != 0) {
ColorStateList csl = getResources().getColorStateList(textColorSelector);
textView.setTextColor(csl);
} else {
textView.setTextColor(textColor);
}
textView.setPadding(20, 4, 20, 4);
textView.setText(tvName);
//動態(tài)設(shè)置shape
GradientDrawable drawable = new GradientDrawable();
drawable.setCornerRadius(shapeRadius);
drawable.setStroke((int) shapeLineWidth, shapeLineColor);
if (shapeBackgroundColorSelector != 0) {
ColorStateList csl = getResources().getColorStateList(shapeBackgroundColorSelector);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
drawable.setColor(csl);
}
} else {
drawable.setColor(shapeBackgroundColor);
}
textView.setBackgroundDrawable(drawable);
//把 ItemView加入流式布局
this.addView(view);
}
public boolean isDeleteMode() {
return isDeleteMode;
}
public void setDeleteMode(boolean deleteMode) {
isDeleteMode = deleteMode;
}
//---20180815---修復(fù)不可滑動bug----start----
private boolean scrollable; // 是否可以滾動
private int measuredHeight; // 測量得到的高度
private int realHeight; // 整個流式布局控件的實際高度
private int scrolledHeight = 0; // 已經(jīng)滾動過的高度
private int startY; // 本次滑動開始的Y坐標(biāo)位置
private int offsetY; // 本次滑動的偏移量
private boolean pointerDown; // 在ACTION_MOVE中,視第一次觸發(fā)為手指按下负溪,從第二次觸發(fā)開始計入正式滑動
/**
* 滾動事件的處理透揣,當(dāng)布局可以滾動(內(nèi)容高度大于測量高度)時,對手勢操作進行處理
*/
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
// 只有當(dāng)布局可以滾動的時候(內(nèi)容高度大于測量高度的時候)川抡,且處于攔截模式辐真,才會對手勢操作進行處理
if (scrollable && isInterceptedTouch) {
int currY = (int) event.getY();
switch (event.getAction()) {
// 因為ACTION_DOWN手勢可能是為了點擊布局中的某個子元素,因此在onInterceptTouchEvent()方法中沒有攔截這個手勢
// 因此崖堤,在這個事件中不能獲取到startY侍咱,也因此才將startY的獲取移動到第一次滾動的時候進行
case MotionEvent.ACTION_DOWN:
break;
// 當(dāng)?shù)谝淮斡|發(fā)ACTION_MOVE事件時,視為手指按下密幔;以后的ACTION_MOVE事件才視為滾動事件
case MotionEvent.ACTION_MOVE:
// 用pointerDown標(biāo)志位只是手指是否已經(jīng)按下
if (!pointerDown) {
startY = currY;
pointerDown = true;
} else {
offsetY = startY - currY; // 下滑大于0
// 布局中的內(nèi)容跟隨手指的滾動而滾動
// 用scrolledHeight記錄以前的滾動事件中滾動過的高度(因為不一定每一次滾動都是從布局的最頂端開始的)
this.scrollTo(0, scrolledHeight + offsetY);
}
break;
// 手指抬起時楔脯,更新scrolledHeight的值;
// 如果滾動過界(滾動到高于布局最頂端或低于布局最低端的時候)胯甩,設(shè)置滾動回到布局的邊界處
case MotionEvent.ACTION_UP:
scrolledHeight += offsetY;
if (scrolledHeight + offsetY < 0) {
this.scrollTo(0, 0);
scrolledHeight = 0;
} else if (scrolledHeight + offsetY + measuredHeight > realHeight) {
this.scrollTo(0, realHeight - measuredHeight);
scrolledHeight = realHeight - measuredHeight;
}
// 手指抬起后別忘了重置這個標(biāo)志位
pointerDown = false;
break;
default:
break;
}
}
return super.onTouchEvent(event);
}
/**
* 事件攔截昧廷,當(dāng)手指按下或抬起的時候不進行攔截(因為可能這個操作只是點擊了布局中的某個子元素)堪嫂;
* 當(dāng)手指移動的時候,才將事件攔截麸粮;
* 因增加最小滑動距離防止點擊時誤觸滑動
*/
private boolean isInterceptedTouch;
private int startYY = 0;
private boolean pointerDownY;
private int minDistance = 10;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int currY = (int) ev.getY();
int offsetY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
pointerDownY = true;
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (pointerDownY) {
startYY = currY;
} else {
offsetY = currY - startYY;
}
pointerDownY = false;
intercepted = Math.abs(offsetY) > minDistance;
break;
case MotionEvent.ACTION_UP:
// 手指抬起后別忘了重置這個標(biāo)志位
intercepted = false;
break;
default:
break;
}
isInterceptedTouch = intercepted;
return intercepted;
}
//---20180815---修復(fù)不可滑動bug----end----
/**
* 流式布局顯示
* Toast.makeText(FlowLayoutActivity.this, keywords, Toast.LENGTH_SHORT).show();
*
* @param list
*/
public void showTag(final List<String> list, final ItemClickListener listener) {
removeAllViews();
for (int i = 0; i < list.size(); i++) {
final String keywords = list.get(i);
addItemView(LayoutInflater.from(getContext()), keywords);
final int finalI = i;
getChildAt(i).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (isDeleteMode()) {
list.remove(keywords);
showTag(list, listener);
} else {
View child = getChildAt(finalI);
child.setSelected(!child.isSelected());
if (child.isSelected()) {
mAllSelectedWords.add(list.get(finalI));
} else {
mAllSelectedWords.remove(list.get(finalI));
}
listener.onClick(keywords, mAllSelectedWords);
}
}
});
}
}
public interface ItemClickListener {
/**
* item 點擊事件
*
* @param currentSelectedkeywords
* @param allSelectedKeywords
*/
void onClick(String currentSelectedkeywords, List<String> allSelectedKeywords);
}
}
控件使用
<com.kingoit.list.flowLayout.KingoitFlowLayout
android:id="@+id/kingoit_flow_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
app:flowLayoutBackgroundColor="@color/light_blue_700"
app:flowLayoutBackgroundColorSelector="@drawable/selector_flowlayout_item_bg"
app:flowLayoutDeleteBtnColor="@color/colorPrimary"
app:flowLayoutLineColor="@color/transation"
app:flowLayoutLineWidth="1dp"
app:flowLayoutRadius="50dp"
app:flowLayoutTextColor="@color/light_blue_50"
app:flowLayoutTextColorSelector="@drawable/selector_flowlayout_item_text_color"
app:flowLayoutTextSize="16sp"/>
/**
* 流式布局使用示例代碼
* @author zuo
* @date 2018/8/15 9:39
*/
public class FlowTestActivity extends Activity implements KingoitFlowLayout.ItemClickListener {
private KingoitFlowLayout flowLayout;
private KingoitHeadView headView;
private List<String> list = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flow_test);
headView = findViewById(R.id.head_view);
flowLayout = findViewById(R.id.kingoit_flow_layout);
initData();
initView();
}
private void initData() {
for (int i = 0; i < 10; i++) {
list.add("戰(zhàn)爭女神");
list.add("蒙多");
list.add("德瑪西亞皇子");
list.add("殤之木乃伊");
list.add("狂戰(zhàn)士");
list.add("布里茨克拉克");
list.add("冰晶鳳凰 艾尼維亞");
list.add("德邦總管");
list.add("野獸之靈 烏迪爾 (德魯伊)");
list.add("賽恩");
list.add("詭術(shù)妖姬");
list.add("永恒夢魘");
}
}
private void initView() {
headView.getHeadRightImg().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flowLayout.setDeleteMode(!flowLayout.isDeleteMode());
flowLayout.showTag(list, FlowTestActivity.this);
}
});
flowLayout.showTag(list, FlowTestActivity.this);
}
@Override
public void onClick(String currentSelectedkeywords, List<String> allSelectedKeywords) {
Toast.makeText(FlowTestActivity.this, currentSelectedkeywords, Toast.LENGTH_SHORT).show();
}
}
- 樣式代碼
selector_flowlayout_item_bg
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/light_blue_50" android:state_pressed="true" />
<item android:color="@color/colorWhite" android:state_selected="true" />
<item android:color="@color/light_blue_700" />
</selector>
selector_flowlayout_item_text_color
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/light_blue_700" android:state_pressed="true" />
<item android:color="@color/light_blue_700" android:state_selected="true" />
<item android:color="@color/light_blue_50" />
</selector>