

import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.appcompat.widget.AppCompatImageView;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import io.R;
import io.tools.MHB_RepeatClickListener;
/**
* 可折叠流式布局 - 支持最大行数、自定义间距、按钮尺寸、默认文字样式
*/
public class ExpandableFlowLayout extends FlowLayout {
private static final int DEFAULT_MAX_LINES = 3;
private static final float DEFAULT_TEXT_SIZE_SP = 14f;
private static final int DEFAULT_TEXT_COLOR = Color.BLACK;
private static final int DEFAULT_TEXT_BACKGROUND = android.R.color.transparent;
private int maxLines = DEFAULT_MAX_LINES;
private boolean isExpanded = false;
private AppCompatImageView toggleButton;
private List<View> contentViews = new ArrayList<>();
// 图标资源
private int expandIconRes = android.R.drawable.arrow_down_float;
private int collapseIconRes = android.R.drawable.arrow_up_float;
private boolean autoHideButtonWhenNoOverflow = true;
// 默认文字样式(用于 addText 无参版本)
private float defaultTextSizeSp = DEFAULT_TEXT_SIZE_SP;
private int defaultTextColor = DEFAULT_TEXT_COLOR;
private int defaultTextBackgroundRes = DEFAULT_TEXT_BACKGROUND;
// 按钮尺寸
private int toggleButtonWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
private int toggleButtonHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
public ExpandableFlowLayout(Context context) {
this(context, null);
}
public ExpandableFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandableFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttributes(context, attrs);
initToggleButton();
}
// ---------- 解析 XML 属性 ----------
private void initAttributes(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandableFlowLayout);
// 间距 —— 直接设置给父类 FlowLayout
int horizontalSpacing = ta.getDimensionPixelSize(R.styleable.ExpandableFlowLayout_horizontalSpacing, dp2px(8));
int verticalSpacing = ta.getDimensionPixelSize(R.styleable.ExpandableFlowLayout_verticalSpacing, dp2px(8));
setHorizontalSpacing(horizontalSpacing);
setVerticalSpacing(verticalSpacing);
// 图标
expandIconRes = ta.getResourceId(R.styleable.ExpandableFlowLayout_expandIcon, expandIconRes);
collapseIconRes = ta.getResourceId(R.styleable.ExpandableFlowLayout_collapseIcon, collapseIconRes);
// 按钮尺寸
toggleButtonWidth = ta.getLayoutDimension(R.styleable.ExpandableFlowLayout_toggleButtonWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
toggleButtonHeight = ta.getLayoutDimension(R.styleable.ExpandableFlowLayout_toggleButtonHeight, ViewGroup.LayoutParams.WRAP_CONTENT);
// 默认文字样式
defaultTextSizeSp = ta.getDimension(R.styleable.ExpandableFlowLayout_defaultTextSize, sp2px(defaultTextSizeSp));
// 将像素值转回 sp(简化:保存原始 sp 值更好,这里直接存储像素值,后面使用时需要转换)
// 更简单:存储 sp 值,但 getDimension 返回的是 px,所以我们再转回 sp 近似值
if (ta.hasValue(R.styleable.ExpandableFlowLayout_defaultTextSize)) {
defaultTextSizeSp = px2sp(defaultTextSizeSp);
}
defaultTextColor = ta.getColor(R.styleable.ExpandableFlowLayout_defaultTextColor, defaultTextColor);
defaultTextBackgroundRes = ta.getResourceId(R.styleable.ExpandableFlowLayout_defaultTextBackground, defaultTextBackgroundRes);
// 最大行数
maxLines = ta.getInt(R.styleable.ExpandableFlowLayout_maxLines, DEFAULT_MAX_LINES);
// 自动隐藏按钮
autoHideButtonWhenNoOverflow = ta.getBoolean(R.styleable.ExpandableFlowLayout_autoHideButton, autoHideButtonWhenNoOverflow);
ta.recycle();
}
// ---------- 初始化按钮 ----------
private void initToggleButton() {
toggleButton = new AppCompatImageView(getContext());
toggleButton.setImageResource(expandIconRes);
// 设置背景(可保留你的自定义背景)
toggleButton.setBackgroundResource(R.drawable.shape_radius6_f5f6f7);
toggleButton.setScaleType(AppCompatImageView.ScaleType.CENTER_INSIDE);
// 应用 XML 中设置的宽高
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(toggleButtonWidth, toggleButtonHeight);
toggleButton.setLayoutParams(lp);
toggleButton.setOnClickListener(v -> toggleExpandCollapse());
addView(toggleButton);
}
// ---------- 对外 API ----------
public void setMaxLines(int maxLines) {
if (this.maxLines != maxLines) {
this.maxLines = maxLines;
updateVisibilityBasedOnState();
}
}
public int getMaxLines() {
return maxLines;
}
public void setExpandIconRes(@DrawableRes int expandIconRes) {
this.expandIconRes = expandIconRes;
if (!isExpanded) {
toggleButton.setImageResource(expandIconRes);
}
}
public void setCollapseIconRes(@DrawableRes int collapseIconRes) {
this.collapseIconRes = collapseIconRes;
if (isExpanded) {
toggleButton.setImageResource(collapseIconRes);
}
}
public void setAutoHideButtonWhenNoOverflow(boolean autoHide) {
this.autoHideButtonWhenNoOverflow = autoHide;
updateVisibilityBasedOnState();
}
/**
* 设置默认文字样式(会影响后续 addText 的默认样式)
*/
public void setDefaultTextStyle(float textSizeSp, @ColorInt int textColor, @DrawableRes int backgroundRes) {
this.defaultTextSizeSp = textSizeSp;
this.defaultTextColor = textColor;
this.defaultTextBackgroundRes = backgroundRes;
}
/**
* 添加 TextView(使用默认样式)
*/
public void addText(JSONObject json) {
addText(json, defaultTextColor, defaultTextSizeSp, defaultTextBackgroundRes, dp2px(9), dp2px(4));
}
/**
* 添加带完整样式的 TextView
*/
public void addText(JSONObject json, int textColor, float textSizeSp, int backgroundRes,
int paddingHorizontal, int paddingVertical) {
MHB_FCTextView textView = new MHB_FCTextView(getContext());
textView.setText(json.optString("keyword"));
textView.setTextColor(textColor);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp);
textView.setBackgroundResource(backgroundRes);
textView.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical);
// textView.setGravity(Gravity.CENTER);
textView.setOnClickListener(new MHB_RepeatClickListener() {
@Override
public void antiRepeatClick(View v) {
if (onItemClickListener != null) {
int position = contentViews.indexOf(v);
onItemClickListener.onItemClick(v, position, json);
}
}
});
MarginLayoutParams lp = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
textView.setLayoutParams(lp);
addCustomView(textView);
}
/**
* 批量添加文本(使用默认样式)
*/
public void addTexts(List<JSONObject> texts) {
for (JSONObject json : texts) {
String keyword = json.optString("keyword");
if (TextUtils.isEmpty(keyword)) continue;
addText(json);
}
}
/**
* 添加任意自定义 View
*/
public void addCustomView(View view) {
contentViews.add(view);
int buttonIndex = indexOfChild(toggleButton);
addView(view, buttonIndex);
updateVisibilityBasedOnState();
}
/**
* 移除所有内容 View
*/
public void removeAllContentViews() {
for (View view : contentViews) {
removeView(view);
}
contentViews.clear();
updateVisibilityBasedOnState();
}
public int getContentCount() {
return contentViews.size();
}
// ---------- 内部逻辑 ----------
private void toggleExpandCollapse() {
isExpanded = !isExpanded;
float targetRotation = isExpanded ? 180f : 0f;
ObjectAnimator animator = ObjectAnimator.ofFloat(toggleButton, "rotation", targetRotation);
animator.setDuration(300);
animator.start();
updateVisibilityBasedOnState();
}
private void updateVisibilityBasedOnState() {
if (contentViews.isEmpty()) {
toggleButton.setVisibility(GONE);
return;
}
if (isExpanded) {
for (View v : contentViews) {
v.setVisibility(VISIBLE);
}
// toggleButton.setImageResource(collapseIconRes);
toggleButton.setVisibility(VISIBLE);
} else {
int visibleCount = calculateVisibleCountInMaxLines();
if (visibleCount >= contentViews.size() && autoHideButtonWhenNoOverflow) {
for (View v : contentViews) {
v.setVisibility(VISIBLE);
}
toggleButton.setVisibility(GONE);
} else {
for (int i = 0; i < contentViews.size(); i++) {
contentViews.get(i).setVisibility(i < visibleCount ? VISIBLE : GONE);
}
// toggleButton.setImageResource(expandIconRes);
toggleButton.setVisibility(VISIBLE);
}
}
requestLayout();
}
private int calculateVisibleCountInMaxLines() {
if (maxLines <= 0 || contentViews.isEmpty()) return 0;
int width = getMeasuredWidth();
if (width == 0) {
return contentViews.size();
}
int availableWidth = width - getPaddingLeft() - getPaddingRight();
int currentLineWidth = 0;
int currentLineCount = 0;
int totalLineCount = 1;
for (int i = 0; i < contentViews.size(); i++) {
View child = contentViews.get(i);
int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
child.measure(widthSpec, heightSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int marginWidth = lp != null ? lp.leftMargin + lp.rightMargin : 0;
int totalChildWidth = child.getMeasuredWidth() + marginWidth;
if (currentLineWidth + totalChildWidth + getHorizontalSpacing() > availableWidth && currentLineCount > 0) {
totalLineCount++;
if (totalLineCount > maxLines) {
return i;
}
currentLineWidth = totalChildWidth;
currentLineCount = 1;
} else {
currentLineWidth += totalChildWidth + (currentLineCount > 0 ? getHorizontalSpacing() : 0);
currentLineCount++;
}
}
return contentViews.size();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (!isExpanded) {
post(this::updateVisibilityBasedOnState);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw && !isExpanded) {
updateVisibilityBasedOnState();
}
}
// ---------- 辅助方法 ----------
private int getHorizontalSpacing() {
// 通过反射获取父类的 mHorizontalSpacing,或者父类提供了 getter 则直接调用
try {
java.lang.reflect.Field field = FlowLayout.class.getDeclaredField("mHorizontalSpacing");
field.setAccessible(true);
return (int) field.get(this);
} catch (Exception e) {
return dp2px(8);
}
}
private int dp2px(float dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
private float px2sp(float px) {
return px / getResources().getDisplayMetrics().scaledDensity;
}
private float sp2px(float sp) {
return sp * getResources().getDisplayMetrics().scaledDensity;
}
private OnItemClickListener onItemClickListener;
public interface OnItemClickListener {
void onItemClick(View view, int position, JSONObject data);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.onItemClickListener = listener;
}
}
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* 流式布局 - 自动换行的基础布局(Java版)
*/
public class FlowLayout extends ViewGroup {
private static final int DEFAULT_HORIZONTAL_SPACING = 8;
private static final int DEFAULT_VERTICAL_SPACING = 8;
private int horizontalSpacing;
private int verticalSpacing;
private List<Integer> lineHeights = new ArrayList<>();
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);
horizontalSpacing = dp2px(DEFAULT_HORIZONTAL_SPACING);
verticalSpacing = dp2px(DEFAULT_VERTICAL_SPACING);
}
public void setHorizontalSpacing(int horizontalSpacing) {
this.horizontalSpacing = horizontalSpacing;
requestLayout();
}
public void setVerticalSpacing(int verticalSpacing) {
this.verticalSpacing = verticalSpacing;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int totalHeight = 0;
int currentLineWidth = 0;
int currentLineHeight = 0;
int lineChildCount = 0;
int childCount = getChildCount();
lineHeights.clear();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childMarginWidth = lp.leftMargin + lp.rightMargin;
int childMarginHeight = lp.topMargin + lp.bottomMargin;
if (currentLineWidth + childWidth + childMarginWidth + horizontalSpacing > widthSize
&& lineChildCount > 0) {
// 换行
totalHeight += currentLineHeight + verticalSpacing;
lineHeights.add(currentLineHeight);
currentLineWidth = 0;
currentLineHeight = 0;
lineChildCount = 0;
}
currentLineWidth += childWidth + childMarginWidth + (lineChildCount > 0 ? horizontalSpacing : 0);
currentLineHeight = Math.max(currentLineHeight, childHeight + childMarginHeight);
lineChildCount++;
}
totalHeight += currentLineHeight;
if (currentLineHeight > 0) {
lineHeights.add(currentLineHeight);
}
setMeasuredDimension(
widthMode == MeasureSpec.EXACTLY ? widthSize : currentLineWidth,
heightMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(heightMeasureSpec) : totalHeight
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = getMeasuredWidth();
int currentX = getPaddingLeft();
int currentY = getPaddingTop();
int currentLineHeight = 0;
int lineIndex = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
if (currentX + childWidth + lp.leftMargin + lp.rightMargin > width - getPaddingRight()
&& currentLineHeight > 0) {
// 换行
currentY += currentLineHeight + verticalSpacing;
currentX = getPaddingLeft();
currentLineHeight = 0;
lineIndex++;
}
int left = currentX + lp.leftMargin;
int top = currentY + lp.topMargin;
int right = left + childWidth;
int bottom = top + childHeight;
child.layout(left, top, right, bottom);
currentX += childWidth + lp.leftMargin + lp.rightMargin + horizontalSpacing;
currentLineHeight = Math.max(currentLineHeight, childHeight + lp.topMargin + lp.bottomMargin);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return p instanceof MarginLayoutParams;
}
private int dp2px(float dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
}
attrs
<declare-styleable name="ExpandableFlowLayout">
<!-- 横向间距(子控件之间的水平间隔) -->
<attr name="horizontalSpacing" format="dimension" />
<!-- 纵向间距(行与行之间的垂直间隔) -->
<attr name="verticalSpacing" format="dimension" />
<!-- 展开按钮的图标 -->
<attr name="expandIcon" format="reference" />
<!-- 收起按钮的图标 -->
<attr name="collapseIcon" format="reference" />
<!-- 按钮宽度 -->
<attr name="toggleButtonWidth" format="dimension" />
<!-- 按钮高度 -->
<attr name="toggleButtonHeight" format="dimension" />
<!-- 默认文字大小(sp) -->
<attr name="defaultTextSize" format="dimension|reference" />
<!-- 默认文字颜色 -->
<attr name="defaultTextColor" format="color|reference" />
<!-- 默认文字背景(可引用 drawable 或 color) -->
<attr name="defaultTextBackground" format="reference|color" />
<!-- 最大显示行数 -->
<attr name="maxLines" format="integer" />
<!-- 是否自动隐藏按钮(内容未超过最大行数时) -->
<attr name="autoHideButton" format="boolean" />
</declare-styleable>
使用
<io.xio.gxfc.view.ExpandableFlowLayout
android:id="@+id/history_ll"
android:layout_below="@id/clear_history_iv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:layout_marginEnd="10dp"
app:horizontalSpacing="8dp"
app:verticalSpacing="8dp"
app:maxLines="2"
app:expandIcon="@mipmap/mhb_screen_down_black_icon_new"
app:toggleButtonWidth="22dp"
app:toggleButtonHeight="27dp"
app:defaultTextSize="14sp"
app:defaultTextColor="#171C20"
app:defaultTextBackground="@drawable/shape_radius6_f5f6f7"
app:autoHideButton="true" />
historyLl.getView().removeAllContentViews();
historyLl.getView().addTexts(hotData);
historyLl.getView().setOnItemClickListener((view, position, jsonData) -> { });

705

被折叠的 条评论
为什么被折叠?



