可折叠的流水布局

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) -> { });

内容概要:本文介绍了一个针对电力系统连锁故障传播路径的N-k多阶段双层优化及故障场景筛选模型,该模型基于混合整数线性规划(MILP)方法构建,旨在全面评估电力系统在遭受多重故障时的脆弱性与恢复能力。通过引入故障传播路径的概念,模型能够动态模拟故障在电网中的逐级扩散过程,并结合多阶段优化策略,实现对关键故障场景的有效识别与优先排序。整个框架不仅考虑了初始故障元件的选取,还涵盖了后续因潮流转移引发的级联跳闸行为,从而提升了风险评估的准确性与时效性。该研究已在Matlab平台上完成代码实现,具备良好的可复现性和工程应用价值,适用于提升现代电网的安全防御水平。; 适合人群:电力系统、能源安全及相关领域的科研人员、高校研究生以及从事电网规划与运行管理的工程技术人员。; 使用场景及目标:①用于电力系统安全评估中识别最危险的N-k故障组合;②支撑电网应急预案制定与薄弱环节改造;③作为学术研究中关于级联故障建模与优化求解的教学与验证工具;④服务于智能电网背景下抵御蓄意攻击或极端事件的风险防控决策。; 阅读建议:建议读者结合Matlab代码深入理解模型的数学 formulation 与求解流程,重点关注目标函数设计、约束条件构建及双层优化结构的实现逻辑,同时可通过调整系统参数和故障设定进行仿真对比分析,以掌握不同因素对连锁故障演化的影响规律。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值