美文网首页Android
Android-FlexboxLayout源码浅析

Android-FlexboxLayout源码浅析

作者: zzq_nene | 来源:发表于2020-07-03 14:34 被阅读0次

    看流式布局的源码解析,首先需要从自定义布局的几个步骤进行考虑,即onMeasure、onLayout、onDraw。因为流式布局是ViewGroup的,所以我们这里不考虑onDraw。

    一、FlexboxLayout.onMeasure

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 初始化缓存
        if (mOrderCache == null) {
            mOrderCache = new SparseIntArray(getChildCount());
        }
        // 判断缓存中的在最后一次测量之后是否发生了改变
        if (mFlexboxHelper.isOrderChangedFromLastMeasurement(mOrderCache)) {
            // 如果发生了改变,则重新针对View做索引排序
            // 这里是将容器内存储对应每个View的Order集合做排序,
            // 然后根据排序结果,将Order的index(代表View的索引)保存在reorderedIndices数组中
            // 数组中保存的其实就是View的索引位置,并且会想缓存中拼接对应的Order信息
            mReorderedIndices = mFlexboxHelper.createReorderedIndices(mOrderCache);
        }
    
        // TODO: Only calculate the children views which are affected from the last measure.
    
        // 根据流式布局的排列方向进行展示
        switch (mFlexDirection) {
            // 横向排列,即按子View顺序在横向根据设置是从左到右还是从右到左
            case FlexDirection.ROW: // Intentional fall through
            case FlexDirection.ROW_REVERSE:
                measureHorizontal(widthMeasureSpec, heightMeasureSpec);
                break;
            // 纵向排列 
            case FlexDirection.COLUMN: // Intentional fall through
            case FlexDirection.COLUMN_REVERSE:
                measureVertical(widthMeasureSpec, heightMeasureSpec);
                break;
            default:
                throw new IllegalStateException(
                        "Invalid value for the flex direction is set: " + mFlexDirection);
        }
    }
    

    1.FlexboxHelper.createReorderedIndices

    创建流式布局内的View的索引数组

    int[] createReorderedIndices(SparseIntArray orderCache) {
        int childCount = mFlexContainer.getFlexItemCount();
        // 针对布局内的每一个View.LayoutParams创建一个Order对象,并保存在List集合
        List<Order> orders = createOrders(childCount);
        // 针对Order排序,并且缓存其index这个索引值,然后将Order信息缓存在OrderCache中
        return sortOrdersIntoReorderedIndices(childCount, orders, orderCache);
    }
    
    (1)FlexboxHelper.createOrders
    @NonNull
    private List<Order> createOrders(int childCount) {
        List<Order> orders = new ArrayList<>(childCount);
        for (int i = 0; i < childCount; i++) {
            View child = mFlexContainer.getFlexItemAt(i);
            FlexItem flexItem = (FlexItem) child.getLayoutParams();
            Order order = new Order();
            order.order = flexItem.getOrder();
            order.index = i;
            orders.add(order);
        }
        return orders;
    }
    
    (2)FlexboxHelper.sortOrdersIntoReorderedIndices
    private int[] sortOrdersIntoReorderedIndices(int childCount, List<Order> orders,
            SparseIntArray orderCache) {
        Collections.sort(orders);
        orderCache.clear();
        int[] reorderedIndices = new int[childCount];
        int i = 0;
        for (Order order : orders) {
            // 缓存Order的索引值
            reorderedIndices[i] = order.index;
            orderCache.append(order.index, order.order);
            i++;
        }
        return reorderedIndices;
    }
    

    2.FlexboxLayout.measureHorizontal

    这里就分析一个方向的关于子View的测量过程,着重分析水平方向的做法,因为水平方向和垂直方向大体相同

    /**
    * 具体测量布局的
    * 这是测量横向排列的时候的布局情况
    */
    private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
        // 因为onMeasure会多次调用测量,所以每次需要重置
        mFlexLines.clear();
    
        mFlexLinesResult.reset();
        // 计算水平方向的每一行的信息
        mFlexboxHelper
                .calculateHorizontalFlexLines(mFlexLinesResult, widthMeasureSpec,
                        heightMeasureSpec);
        mFlexLines = mFlexLinesResult.mFlexLines;
    
        // 确定主轴方向的尺寸
        mFlexboxHelper.determineMainSize(widthMeasureSpec, heightMeasureSpec);
    
        // TODO: Consider the case any individual child's mAlignSelf is set to ALIGN_SELF_BASELINE
        if (mAlignItems == AlignItems.BASELINE) {
            for (FlexLine flexLine : mFlexLines) {
                // The largest height value that also take the baseline shift into account
                int largestHeightInLine = Integer.MIN_VALUE;
                for (int i = 0; i < flexLine.mItemCount; i++) {
                    // flexLine.mFirstIndex是当前行的第一个View的位置
                    // flexLine.mFirstIndex+i就表示当前行第i个View在
                    // 布局中的位置
                    int viewIndex = flexLine.mFirstIndex + i;
                    View child = getReorderedChildAt(viewIndex);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }
                    LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    if (mFlexWrap != FlexWrap.WRAP_REVERSE) {
                        int marginTop = flexLine.mMaxBaseline - child.getBaseline();
                        marginTop = Math.max(marginTop, lp.topMargin);
                        largestHeightInLine = Math.max(largestHeightInLine,
                                child.getMeasuredHeight() + marginTop + lp.bottomMargin);
                    } else {
                        int marginBottom = flexLine.mMaxBaseline - child.getMeasuredHeight() +
                                child.getBaseline();
                        marginBottom = Math.max(marginBottom, lp.bottomMargin);
                        largestHeightInLine = Math.max(largestHeightInLine,
                                child.getMeasuredHeight() + lp.topMargin + marginBottom);
                    }
                }
                flexLine.mCrossSize = largestHeightInLine;
            }
        }
        // 计算辅轴的大小
        mFlexboxHelper.determineCrossSize(widthMeasureSpec, heightMeasureSpec,
                getPaddingTop() + getPaddingBottom());
        // Now cross size for each flex line is determined.
        // Expand the views if alignItems (or mAlignSelf in each child view) is set to stretch
        // 拉伸视图,通过AlignItems的属性为STRETCH来判断是否拉伸
        // 拉伸的方向是辅轴方向,如果横向是主轴,那么就拉伸纵向
        // 根据辅轴方向的大小做拉伸限制。比如纵向是辅轴,就需要重新计算View的新的高度
        mFlexboxHelper.stretchViews();
        // 根据FlexboxLayout的widthMode和heightMode,确定其实际的widthSize和heightSize
        setMeasuredDimensionForFlex(mFlexDirection, widthMeasureSpec, heightMeasureSpec,
                mFlexLinesResult.mChildState);
    }
    
    (1)FlexboxHelper.calculateHorizontalFlexLines

    测量水平每一行的子View的数量以及对每个子View做测量

    void calculateHorizontalFlexLines(FlexLinesResult result, int widthMeasureSpec,
            int heightMeasureSpec) {
        calculateFlexLines(result, widthMeasureSpec, heightMeasureSpec, Integer.MAX_VALUE,
                0, NO_POSITION, null);
    }
    
    (2)FlexboxHelper.calculateFlexLines
    void calculateFlexLines(FlexLinesResult result, int mainMeasureSpec,
            int crossMeasureSpec, int needsCalcAmount, int fromIndex, int toIndex,
            @Nullable List<FlexLine> existingLines) {
        // 判断主轴方向是否是水平方向
        boolean isMainHorizontal = mFlexContainer.isMainAxisDirectionHorizontal();
        // 获取主轴方向的mode和size
        // 如果是横向排列,则是width的mode和size
        int mainMode = View.MeasureSpec.getMode(mainMeasureSpec);
        int mainSize = View.MeasureSpec.getSize(mainMeasureSpec);
    
        int childState = 0;
    
        // 初始化每一行的存储集合
        List<FlexLine> flexLines;
        if (existingLines == null) {
            flexLines = new ArrayList<>();
        } else {
            flexLines = existingLines;
        }
        // 保存每一行数据集合到FlexLinesResult中,这是一个FlexboxHelper的静态内部类
        result.mFlexLines = flexLines;
        // 判断是否有记录的索引
        boolean reachedToIndex = toIndex == NO_POSITION;
        // 初始化FlexboxLayout的padding
        int mainPaddingStart = getPaddingStartMain(isMainHorizontal);
        int mainPaddingEnd = getPaddingEndMain(isMainHorizontal);
        int crossPaddingStart = getPaddingStartCross(isMainHorizontal);
        int crossPaddingEnd = getPaddingEndCross(isMainHorizontal);
        // 初始化在辅轴方向的最大值
        int largestSizeInCross = Integer.MIN_VALUE;
    
        // The amount of cross size calculated in this method call.
        int sumCrossSize = 0;
    
        // The index of the view in the flex line.
        int indexInFlexLine = 0;
        // 初始化每一行的信息,包括当前行的第一个View的索引和使用的padding大小
        FlexLine flexLine = new FlexLine();
        flexLine.mFirstIndex = fromIndex;
        flexLine.mMainSize = mainPaddingStart + mainPaddingEnd;
        // 遍历FlexboxLayout内的每一个View
        // 横向排列遍历时,fromIndex的初始值为0
        int childCount = mFlexContainer.getFlexItemCount();
        for (int i = fromIndex; i < childCount; i++) {
            View child = mFlexContainer.getReorderedFlexItemAt(i);
    
            if (child == null) {
                // 如果child为null,判断是否是最后一行,是的话,添加到集合中保存
                if (isLastFlexItem(i, childCount, flexLine)) {
                    addFlexLine(flexLines, flexLine, i, sumCrossSize);
                }
                continue;
            } else if (child.getVisibility() == View.GONE) {
                // 如果View的visibility是GONE,则记录,并且判断是否是最后一行
                flexLine.mGoneItemCount++;
                flexLine.mItemCount++;
                if (isLastFlexItem(i, childCount, flexLine)) {
                    addFlexLine(flexLines, flexLine, i, sumCrossSize);
                }
                continue;
            } else if (child instanceof CompoundButton) {
                evaluateMinimumSizeForCompoundButton((CompoundButton) child);
            }
            // 获取child的LayoutParams
            FlexItem flexItem = (FlexItem) child.getLayoutParams();
            // 判断child的LayoutParams的AlignItems是否为STRETCH
            if (flexItem.getAlignSelf() == AlignItems.STRETCH) {
                // 保存AlignItems为STRETCH的View的索引
                // AlignItems为STRETCH,当子View没高度时默认充满容器
                flexLine.mIndicesAlignSelfStretch.add(i);
            }
            // 计算child的主轴大小(水平的就是宽度)
            int childMainSize = getFlexItemSizeMain(flexItem, isMainHorizontal);
            // 判断child的基本比例是否不是默认比例,并且FlexboxLayout的mainMode为EXACTLY确切值
            if (flexItem.getFlexBasisPercent() != FLEX_BASIS_PERCENT_DEFAULT
                    && mainMode == View.MeasureSpec.EXACTLY) {
                // 重新计算child的主轴大小,按基本比例计算
                childMainSize = Math.round(mainSize * flexItem.getFlexBasisPercent());
                // Use the dimension from the layout if the mainMode is not
                // MeasureSpec.EXACTLY even if any fraction value is set to
                // layout_flexBasisPercent.
            }
            // 测量child的宽高
            int childMainMeasureSpec;
            int childCrossMeasureSpec;
            if (isMainHorizontal) {
                childMainMeasureSpec = mFlexContainer.getChildWidthMeasureSpec(mainMeasureSpec,
                        mainPaddingStart + mainPaddingEnd +
                                getFlexItemMarginStartMain(flexItem, true) +
                                getFlexItemMarginEndMain(flexItem, true),
                        childMainSize);
                childCrossMeasureSpec = mFlexContainer.getChildHeightMeasureSpec(crossMeasureSpec,
                        crossPaddingStart + crossPaddingEnd +
                                getFlexItemMarginStartCross(flexItem, true) +
                                getFlexItemMarginEndCross(flexItem, true)
                                + sumCrossSize,
                        getFlexItemSizeCross(flexItem, true));
                child.measure(childMainMeasureSpec, childCrossMeasureSpec);
                // 修改子View的MeasureSpec的缓存信息
                updateMeasureCache(i, childMainMeasureSpec, childCrossMeasureSpec, child);
            } else {
                childCrossMeasureSpec = mFlexContainer.getChildWidthMeasureSpec(crossMeasureSpec,
                        crossPaddingStart + crossPaddingEnd +
                                getFlexItemMarginStartCross(flexItem, false) +
                                getFlexItemMarginEndCross(flexItem, false) + sumCrossSize,
                        getFlexItemSizeCross(flexItem, false));
                childMainMeasureSpec = mFlexContainer.getChildHeightMeasureSpec(mainMeasureSpec,
                        mainPaddingStart + mainPaddingEnd +
                                getFlexItemMarginStartMain(flexItem, false) +
                                getFlexItemMarginEndMain(flexItem, false),
                        childMainSize);
                child.measure(childCrossMeasureSpec, childMainMeasureSpec);
                updateMeasureCache(i, childCrossMeasureSpec, childMainMeasureSpec, child);
            }
            // 修改子View在容器中的缓存,其实就是调用FlexboxLayout的updateViewCache方法
            // 但是FlexboxLayout并没有实现该方法,是一个空方法
            mFlexContainer.updateViewCache(i, child);
    
            // Check the size constraint after the first measurement for the child
            // To prevent the child's width/height violate the size constraints imposed by the
            // {@link FlexItem#getMinWidth()}, {@link FlexItem#getMinHeight()},
            // {@link FlexItem#getMaxWidth()} and {@link FlexItem#getMaxHeight()} attributes.
            // E.g. When the child's layout_width is wrap_content the measured width may be
            // less than the min width after the first measurement.
            checkSizeConstraints(child, i);
    
            childState = View.combineMeasuredStates(
                    childState, child.getMeasuredState());
            // 判断是否是正常方向,并且当前子View加入之后是否需要换行
            if (isWrapRequired(child, mainMode, mainSize, flexLine.mMainSize,
                    getViewMeasuredSizeMain(child, isMainHorizontal)
                            + getFlexItemMarginStartMain(flexItem, isMainHorizontal) +
                            getFlexItemMarginEndMain(flexItem, isMainHorizontal),
                    flexItem, i, indexInFlexLine, flexLines.size())) {
                // 满足if条件的是需要换行的
                if (flexLine.getItemCountNotGone() > 0) {
                    addFlexLine(flexLines, flexLine, i > 0 ? i - 1 : 0, sumCrossSize);
                    sumCrossSize += flexLine.mCrossSize;
                }
                // 针对Item的高度为MATCH_PARENT的子View做特别的测量处理
                if (isMainHorizontal) {
                    if (flexItem.getHeight() == ViewGroup.LayoutParams.MATCH_PARENT) {
                        // This case takes care of the corner case where the cross size of the
                        // child is affected by the just added flex line.
                        // E.g. when the child's layout_height is set to match_parent, the height
                        // of that child needs to be determined taking the total cross size used
                        // so far into account. In that case, the height of the child needs to be
                        // measured again note that we don't need to judge if the wrapping occurs
                        // because it doesn't change the size along the main axis.
                        childCrossMeasureSpec = mFlexContainer.getChildHeightMeasureSpec(
                                crossMeasureSpec,
                                mFlexContainer.getPaddingTop() + mFlexContainer.getPaddingBottom()
                                        + flexItem.getMarginTop()
                                        + flexItem.getMarginBottom() + sumCrossSize,
                                flexItem.getHeight());
                        child.measure(childMainMeasureSpec, childCrossMeasureSpec);
                        checkSizeConstraints(child, i);
                    }
                } else {
                    if (flexItem.getWidth() == ViewGroup.LayoutParams.MATCH_PARENT) {
                        // This case takes care of the corner case where the cross size of the
                        // child is affected by the just added flex line.
                        // E.g. when the child's layout_width is set to match_parent, the width
                        // of that child needs to be determined taking the total cross size used
                        // so far into account. In that case, the width of the child needs to be
                        // measured again note that we don't need to judge if the wrapping occurs
                        // because it doesn't change the size along the main axis.
                        childCrossMeasureSpec = mFlexContainer.getChildWidthMeasureSpec(
                                crossMeasureSpec,
                                mFlexContainer.getPaddingLeft() + mFlexContainer.getPaddingRight()
                                        + flexItem.getMarginLeft()
                                        + flexItem.getMarginRight() + sumCrossSize,
                                flexItem.getWidth());
                        child.measure(childCrossMeasureSpec, childMainMeasureSpec);
                        checkSizeConstraints(child, i);
                    }
                }
                // 换行,对行变量重新初始化,并且将当前行的子View数量置为1
                // 这是在当前子View测量的时候判断当前子View是否需要缓存处理
                flexLine = new FlexLine();
                flexLine.mItemCount = 1;
                flexLine.mMainSize = mainPaddingStart + mainPaddingEnd;
                flexLine.mFirstIndex = i;
                indexInFlexLine = 0;
                largestSizeInCross = Integer.MIN_VALUE;
            } else {
                // 如果不需要换行,当前行子View数量加1,并且在行内的索引也加1
                flexLine.mItemCount++;
                indexInFlexLine++;
            }
            flexLine.mAnyItemsHaveFlexGrow |= flexItem.getFlexGrow() != FLEX_GROW_DEFAULT;
            flexLine.mAnyItemsHaveFlexShrink |= flexItem.getFlexShrink() != FLEX_SHRINK_NOT_SET;
    
            if (mIndexToFlexLine != null) {
                mIndexToFlexLine[i] = flexLines.size();
            }
            // 计算当前行的主轴大小
            // 由上一次的结果+子View的主轴大小和主轴的开始和结束时的margin决定
            flexLine.mMainSize += getViewMeasuredSizeMain(child, isMainHorizontal)
                    + getFlexItemMarginStartMain(flexItem, isMainHorizontal) +
                    getFlexItemMarginEndMain(flexItem, isMainHorizontal);
            flexLine.mTotalFlexGrow += flexItem.getFlexGrow();
            flexLine.mTotalFlexShrink += flexItem.getFlexShrink();
            // 当有一个新的子View加入容器中,在这里检查是否需要在开始或者中间有分割线
            mFlexContainer.onNewFlexItemAdded(child, i, indexInFlexLine, flexLine);
    
            largestSizeInCross = Math.max(largestSizeInCross,
                    getViewMeasuredSizeCross(child, isMainHorizontal) +
                            getFlexItemMarginStartCross(flexItem, isMainHorizontal) +
                            getFlexItemMarginEndCross(flexItem, isMainHorizontal) +
                            mFlexContainer.getDecorationLengthCrossAxis(child));
            // Temporarily set the cross axis length as the largest child in the flexLine
            // Expand along the cross axis depending on the mAlignContent property if needed
            // later
            flexLine.mCrossSize = Math.max(flexLine.mCrossSize, largestSizeInCross);
    
            if (isMainHorizontal) {
                if (mFlexContainer.getFlexWrap() != FlexWrap.WRAP_REVERSE) {
                    flexLine.mMaxBaseline = Math.max(flexLine.mMaxBaseline,
                            child.getBaseline() + flexItem.getMarginTop());
                } else {
                    // if the flex wrap property is WRAP_REVERSE, calculate the
                    // baseline as the distance from the cross end and the baseline
                    // since the cross size calculation is based on the distance from the cross end
                    flexLine.mMaxBaseline = Math.max(flexLine.mMaxBaseline,
                            child.getMeasuredHeight() - child.getBaseline()
                                    + flexItem.getMarginBottom());
                }
            }
    
            if (isLastFlexItem(i, childCount, flexLine)) {
                addFlexLine(flexLines, flexLine, i, sumCrossSize);
                sumCrossSize += flexLine.mCrossSize;
            }
    
            if (toIndex != NO_POSITION
                    && flexLines.size() > 0
                    && flexLines.get(flexLines.size() - 1).mLastIndex >= toIndex
                    && i >= toIndex
                    && !reachedToIndex) {
                // Calculated to include a flex line which includes the flex item having the
                // toIndex.
                // Let the sumCrossSize start from the negative value of the last flex line's
                // cross size because otherwise flex lines aren't calculated enough to fill the
                // visible area.
                sumCrossSize = -flexLine.getCrossSize();
                reachedToIndex = true;
            }
            if (sumCrossSize > needsCalcAmount && reachedToIndex) {
                // Stop the calculation if the sum of cross size calculated reached to the point
                // beyond the needsCalcAmount value to avoid unneeded calculation in a
                // RecyclerView.
                // To be precise, the decoration length may be added to the sumCrossSize,
                // but we omit adding the decoration length because even without the decorator
                // length, it's guaranteed that calculation is done at least beyond the
                // needsCalcAmount
                break;
            }
        }
    
        result.mChildState = childState;
    }
    
    (3)FlexboxHelper.determineMainSize

    确定容器的实际的主轴尺寸,即MainSize。并且根据是否允许放大或者收缩以及计算得到的每一行的mainSize和FlexboxLayout的mainSize比较,对子View做一定的放大或者缩小

    void determineMainSize(int widthMeasureSpec, int heightMeasureSpec) {
        determineMainSize(widthMeasureSpec, heightMeasureSpec, 0);
    }
    
    void determineMainSize(int widthMeasureSpec, int heightMeasureSpec, int fromIndex) {
        ensureChildrenFrozen(mFlexContainer.getFlexItemCount());
        if (fromIndex >= mFlexContainer.getFlexItemCount()) {
            return;
        }
        int mainSize;
        int paddingAlongMainAxis;
        // 获取FlexboxLayout的排列方向,根据排列方向确定mainSize
        int flexDirection = mFlexContainer.getFlexDirection();
        switch (mFlexContainer.getFlexDirection()) {
            case FlexDirection.ROW: // Intentional fall through
            case FlexDirection.ROW_REVERSE:
                int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
                int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
                // 遍历每一行,获取每一行的主轴size,然后得到最大值
                int largestMainSize = mFlexContainer.getLargestMainSize();
                // 判断FlexboxLayout的widthMode,如果是EXACTLY,则直接使用FlexboxLayout的宽度
                // 如果不是,则通过比较widthSize和largestMainSize取小值
                if (widthMode == View.MeasureSpec.EXACTLY) {
                    mainSize = widthSize;
                } else {
                    mainSize = largestMainSize > widthSize ? widthSize : largestMainSize;
                }
                // 主轴方向两侧的padding的总和
                paddingAlongMainAxis = mFlexContainer.getPaddingLeft()
                        + mFlexContainer.getPaddingRight();
                break;
            case FlexDirection.COLUMN: // Intentional fall through
            case FlexDirection.COLUMN_REVERSE:
                int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
                int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
                if (heightMode == View.MeasureSpec.EXACTLY) {
                    mainSize = heightSize;
                } else {
                    mainSize = mFlexContainer.getLargestMainSize();
                }
                paddingAlongMainAxis = mFlexContainer.getPaddingTop()
                        + mFlexContainer.getPaddingBottom();
                break;
            default:
                throw new IllegalArgumentException("Invalid flex direction: " + flexDirection);
        }
    
        int flexLineIndex = 0;
        if (mIndexToFlexLine != null) {
            flexLineIndex = mIndexToFlexLine[fromIndex];
        }
        List<FlexLine> flexLines = mFlexContainer.getFlexLinesInternal();
        // 遍历每一行,并且根据每一行的宽度做判断
        // 并且判断是否是允许展开或者收缩
        for (int i = flexLineIndex, size = flexLines.size(); i < size; i++) {
            FlexLine flexLine = flexLines.get(i);
            // flexLine.mAnyItemsHaveFlexGrow是允许放大一定的比例
            if (flexLine.mMainSize < mainSize && flexLine.mAnyItemsHaveFlexGrow) {
                expandFlexItems(widthMeasureSpec, heightMeasureSpec, flexLine,
                        mainSize, paddingAlongMainAxis, false);
            } 
            // flexLine.mAnyItemsHaveFlexShrink是允许收缩一定的比例
            else if (flexLine.mMainSize > mainSize && flexLine.mAnyItemsHaveFlexShrink) {
                shrinkFlexItems(widthMeasureSpec, heightMeasureSpec, flexLine,
                        mainSize, paddingAlongMainAxis, false);
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:Android-FlexboxLayout源码浅析

        本文链接:https://www.haomeiwen.com/subject/sxesqktx.html