首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何使用MaterialCardView来模拟MaterialShapeDrawable的可视化

如何使用MaterialCardView来模拟MaterialShapeDrawable的可视化
EN

Stack Overflow用户
提问于 2020-12-26 18:32:38
回答 1查看 594关注 0票数 0

我试过什么?

在浏览了一下MaterailCardViewHelper 来源之后,我试图复制它绘制相关的Drawable的方式。不幸的是,它的形状是黑色的,有一些“经过处理的”角,看起来一点也不像MaterialCardView。我知道MaterialCardViewHelper在实际的CardView上应用了背景和前景,在查看了源代码之后,它似乎并没有做什么特别的事情,也就是说,它似乎只是调用setBackgroundDrawable (我在someView上做的,如下所示)。

我正在使用Xamarin,所以我的代码是用C#编写的。实际上,我已经将(MaterialCardViewHelper的) Java源代码转换为它的C#等价物,并在适当的地方替换了"materialCardView“对MaterialCardDrawable的引用。

我试图将代码保持在与原始Java源代码一样接近的位置,以确保任何阅读此代码的人都可以轻松地将原始代码与我的源代码进行比较。我所做的改动只足以使代码编译。主要的区别是“抽签”法,我认为这是我的问题所在。

代码语言:javascript
复制
public sealed class MaterialCardDrawable : MaterialShapeDrawable
{
    private static readonly int[] CHECKED_STATE_SET = { Android.Resource.Attribute.StateChecked };
    private static readonly int DEFAULT_STROKE_VALUE = -1;
    private static readonly double COS_45 = Math.Cos(Math.ToRadians(45));
    private static readonly float CARD_VIEW_SHADOW_MULTIPLIER = 1.5f;
    private static readonly int CHECKED_ICON_LAYER_INDEX = 2;

    // this class will act as MaterialCardView (so any references to "materialCardView" will just be referenced to this class instead)
    //private readonly MaterialCardView materialCardView; 

    private readonly Rect userContentPadding = new Rect();
    private readonly MaterialShapeDrawable bgDrawable;
    private readonly MaterialShapeDrawable foregroundContentDrawable;

    private int checkedIconMargin;
    private int checkedIconSize;
    private int strokeWidth;

    private Drawable fgDrawable;
    private Drawable checkedIcon;
    private ColorStateList rippleColor;
    private ColorStateList checkedIconTint;
    private ShapeAppearanceModel shapeAppearanceModel;
    private ColorStateList strokeColor;
    private Drawable rippleDrawable;
    private LayerDrawable clickableForegroundDrawable;
    private MaterialShapeDrawable compatRippleDrawable;
    private MaterialShapeDrawable foregroundShapeDrawable;

    private bool isBackgroundOverwritten = false;
    private bool checkable;

    public MaterialCardDrawable(Context context)
    {
        bgDrawable = new MaterialShapeDrawable(context, null, 0, 0); // different
        bgDrawable.InitializeElevationOverlay(context);
        bgDrawable.SetShadowColor(Color.DarkGray/*potentially different*/);
        ShapeAppearanceModel.Builder shapeAppearanceModelBuilder = bgDrawable.ShapeAppearanceModel.ToBuilder();
        shapeAppearanceModelBuilder.SetAllCornerSizes(DimensionHelper.GetPixels(4)); // different, use 4 as opposed to 0 as default (converts dp to pixels)
        foregroundContentDrawable = new MaterialShapeDrawable();
        setShapeAppearanceModel(shapeAppearanceModelBuilder.Build());

        loadFromAttributes(context);
    }

    // assuming responsibility for drawing the rest of the drawables
    public override void Draw(Canvas canvas)
    {
        bgDrawable?.Draw(canvas);
        clickableForegroundDrawable?.Draw(canvas);
        compatRippleDrawable?.Draw(canvas);
        fgDrawable?.Draw(canvas);
        foregroundContentDrawable?.Draw(canvas);
        foregroundShapeDrawable?.Draw(canvas);
        rippleDrawable?.Draw(canvas);
    }

    public override void SetBounds(int left, int top, int right, int bottom)
    {
        base.SetBounds(left, top, right, bottom);
        bgDrawable?.SetBounds(left, top, right, bottom);
        clickableForegroundDrawable?.SetBounds(left, top, right, bottom);
        compatRippleDrawable?.SetBounds(left, top, right, bottom);
        fgDrawable?.SetBounds(left, top, right, bottom);
        foregroundContentDrawable?.SetBounds(left, top, right, bottom);
        foregroundShapeDrawable?.SetBounds(left, top, right, bottom);
        rippleDrawable?.SetBounds(left, top, right, bottom);
    }

    void loadFromAttributes(Context context)
    {
        // this is very different to the original source
        // just use default values            
        strokeColor = ColorStateList.ValueOf(new Color(DEFAULT_STROKE_VALUE));

        strokeWidth = 0;
        checkable = false;
        // ignore checkedIcon related calls for testing purposes

        TypedArray attributes = context.ObtainStyledAttributes(new int[] { Android.Resource.Attribute.ColorControlHighlight, Android.Resource.Attribute.ColorForeground });

        rippleColor = ColorStateList.ValueOf(attributes.GetColor(0, 0));

        ColorStateList foregroundColor = attributes.GetColorStateList(1);
        setCardForegroundColor(foregroundColor);

        updateRippleColor();
        updateElevation();
        updateStroke();

        fgDrawable = /*materialCardView.*/isClickable() ? getClickableForeground() : foregroundContentDrawable;
    }

    // original source calls "materialCardView" but this class simply mimicks the MaterialCardView so this method exists here
    bool isClickable()
    {
        return false;
    }

    // original source calls "materialCardView" but this class simply mimicks the MaterialCardView so this method exists here
    float getMaxCardElevation()
    {
        // apparently used for when dragging to clamp the shadow
        // using this as a default value
        return DimensionHelper.GetPixels(12);
    }

    // original source calls "materialCardView" but this class simply mimicks the MaterialCardView so this method exists here
    float getCardViewRadius()
    {
        // just using a radius of 4dp for now
        return DimensionHelper.GetPixels(4);
    }

    // original source calls "materialCardView" but this class simply mimicks the MaterialCardView so this method exists here
    bool getUseCompatPadding()
    {
        // no effect when API version is Lollipop and beyond
        return false;
    }

    // original source calls "materialCardView" but this class simply mimicks the MaterialCardView so this method exists here
    bool getPreventCornerOverlap()
    {
        // no effect when API version is Lollipop and beyond
        return false;
    }

    bool getIsBackgroundOverwritten()
    {
        return isBackgroundOverwritten;
    }

    void setBackgroundOverwritten(bool isBackgroundOverwritten)
    {
        this.isBackgroundOverwritten = isBackgroundOverwritten;
    }

    void setStrokeColor(ColorStateList strokeColor)
    {
        if (this.strokeColor == strokeColor)
        {
            return;
        }

        this.strokeColor = strokeColor;
        updateStroke();
    }


    int getStrokeColor()
    {
        return strokeColor == null ? DEFAULT_STROKE_VALUE : strokeColor.DefaultColor;
    }

    ColorStateList getStrokeColorStateList()
    {
        return strokeColor;
    }

    void setStrokeWidth(int strokeWidth)
    {
        if (strokeWidth == this.strokeWidth)
        {
            return;
        }
        this.strokeWidth = strokeWidth;
        updateStroke();
    }


    int getStrokeWidth()
    {
        return strokeWidth;
    }

    MaterialShapeDrawable getBackground()
    {
        return bgDrawable;
    }

    void setCardBackgroundColor(ColorStateList color)
    {
        bgDrawable.FillColor = color;
    }

    ColorStateList getCardBackgroundColor()
    {
        return bgDrawable.FillColor;
    }

    void setCardForegroundColor(ColorStateList foregroundColor)
    {
        foregroundContentDrawable.FillColor = foregroundColor == null ? ColorStateList.ValueOf(Color.Transparent) : foregroundColor;
    }

    ColorStateList getCardForegroundColor()
    {
        return foregroundContentDrawable.FillColor;
    }

    void setUserContentPadding(int left, int top, int right, int bottom)
    {
        userContentPadding.Set(left, top, right, bottom);
        updateContentPadding();
    }

    Rect getUserContentPadding()
    {
        return userContentPadding;
    }

    void updateClickable()
    {
        Drawable previousFgDrawable = fgDrawable;
        fgDrawable = /*materialCardView.*/isClickable() ? getClickableForeground() : foregroundContentDrawable;
        if (previousFgDrawable != fgDrawable)
        {
            updateInsetForeground(fgDrawable);
        }
    }

    void setCornerRadius(float cornerRadius)
    {
        setShapeAppearanceModel(shapeAppearanceModel.WithCornerSize(cornerRadius));
        fgDrawable.InvalidateSelf();
        if (shouldAddCornerPaddingOutsideCardBackground()
            || shouldAddCornerPaddingInsideCardBackground())
        {
            updateContentPadding();
        }

        if (shouldAddCornerPaddingOutsideCardBackground())
        {
            updateInsets();
        }
    }

    float getCornerRadius()
    {
        return bgDrawable.TopLeftCornerResolvedSize;
    }

    void setProgress(float progress)
    {
        bgDrawable.Interpolation = progress;
        if (foregroundContentDrawable != null)
        {
            foregroundContentDrawable.Interpolation = progress;
        }

        if (foregroundShapeDrawable != null)
        {
            foregroundShapeDrawable.Interpolation = progress;
        }
    }

    float getProgress()
    {
        return bgDrawable.Interpolation;
    }

    void updateElevation()
    {
        bgDrawable.Elevation = 4; // different for simplicity's sake use a default value of 4
    }

    void updateInsets()
    {
        // No way to update the inset amounts for an InsetDrawable, so recreate insets as needed.
        if (!getIsBackgroundOverwritten())
        {
            // this is unavailable outside of "material-components" package
            //materialCardView.setBackgroundInternal(insetDrawable(bgDrawable));                
            // maybe a call to
            // InvalidateSelf()
            // works in place of the above?
        }
        // can't find this in the original "MaterialCardView" or "CardView" source, any ideas?
        // I assume it's on a base class, like "FrameLayout" but I couldn't find it there either
        //materialCardView.setForeground(insetDrawable(fgDrawable));
        // don't know enough about the above to provide a replacement call, any ideas?
    }

    void updateStroke()
    {
        foregroundContentDrawable.SetStroke(strokeWidth, strokeColor);
    }

    void updateContentPadding()
    {
        bool includeCornerPadding = shouldAddCornerPaddingInsideCardBackground() || shouldAddCornerPaddingOutsideCardBackground();
        // The amount with which to adjust the user provided content padding to account for stroke and
        // shape corners.
        int contentPaddingOffset = (int)((includeCornerPadding ? calculateActualCornerPadding() : 0) - getParentCardViewCalculatedCornerPadding());

        // this is unavailable outside of "material-components" package
        // and possibly not required to simulate this
        //materialCardView.setAncestorContentPadding(
        //    userContentPadding.left + contentPaddingOffset,
        //    userContentPadding.top + contentPaddingOffset,
        //    userContentPadding.right + contentPaddingOffset,
        //    userContentPadding.bottom + contentPaddingOffset);
    }

    void setCheckable(bool checkable)
    {
        this.checkable = checkable;
    }

    bool isCheckable()
    {
        return checkable;
    }

    void setRippleColor(ColorStateList rippleColor)
    {
        this.rippleColor = rippleColor;
        updateRippleColor();
    }

    void setCheckedIconTint(ColorStateList checkedIconTint)
    {
        this.checkedIconTint = checkedIconTint;
        if (checkedIcon != null)
        {
            DrawableCompat.SetTintList(checkedIcon, checkedIconTint);
        }
    }

    ColorStateList getCheckedIconTint()
    {
        return checkedIconTint;
    }

    ColorStateList getRippleColor()
    {
        return rippleColor;
    }

    Drawable getCheckedIcon()
    {
        return checkedIcon;
    }

    void setCheckedIcon(Drawable checkedIcon)
    {
        this.checkedIcon = checkedIcon;
        if (checkedIcon != null)
        {
            this.checkedIcon = DrawableCompat.Wrap(checkedIcon.Mutate());
            DrawableCompat.SetTintList(this.checkedIcon, checkedIconTint);
        }

        if (clickableForegroundDrawable != null)
        {
            Drawable checkedLayer = createCheckedIconLayer();
            clickableForegroundDrawable.SetDrawableByLayerId(Resource.Id.mtrl_card_checked_layer_id, checkedLayer);
        }
    }

    int getCheckedIconSize()
    {
        return checkedIconSize;
    }

    void setCheckedIconSize(int checkedIconSize)
    {
        this.checkedIconSize = checkedIconSize;
    }

    int getCheckedIconMargin()
    {
        return checkedIconMargin;
    }

    void setCheckedIconMargin(int checkedIconMargin)
    {
        this.checkedIconMargin = checkedIconMargin;
    }

    void onMeasure(int measuredWidth, int measuredHeight)
    {
        if (clickableForegroundDrawable != null)
        {
            int left = measuredWidth - checkedIconMargin - checkedIconSize;
            int bottom = measuredHeight - checkedIconMargin - checkedIconSize;
            bool isPreLollipop = VERSION.SdkInt < Android.OS.BuildVersionCodes.Lollipop;
            if (isPreLollipop || /*materialCardView.*/getUseCompatPadding())
            {
                bottom -= (int)Math.Ceil(2f * calculateVerticalBackgroundPadding());
                left -= (int)Math.Ceil(2f * calculateHorizontalBackgroundPadding());
            }

            int right = checkedIconMargin;
            // potentially not required for this use case
            //if (ViewCompat.GetLayoutDirection(materialCardView) == ViewCompat.LayoutDirectionRtl)
            //{
            //    // swap left and right
            //    int tmp = right;
            //    right = left;
            //    left = tmp;
            //}

            clickableForegroundDrawable.SetLayerInset(CHECKED_ICON_LAYER_INDEX, left, checkedIconMargin /* top */, right, bottom);
        }
    }

    void forceRippleRedraw()
    {
        if (rippleDrawable != null)
        {
            Rect bounds = rippleDrawable.Bounds;
            // Change the bounds slightly to force the layer to change color, then change the layer again.
            // In API 28 the color for the Ripple is snapshot at the beginning of the animation,
            // it doesn't update when the drawable changes to android:state_checked.
            int bottom = bounds.Bottom;
            rippleDrawable.SetBounds(bounds.Left, bounds.Top, bounds.Right, bottom - 1);
            rippleDrawable.SetBounds(bounds.Left, bounds.Top, bounds.Right, bottom);
        }
    }

    void setShapeAppearanceModel(ShapeAppearanceModel shapeAppearanceModel)
    {
        this.shapeAppearanceModel = shapeAppearanceModel;
        bgDrawable.ShapeAppearanceModel = shapeAppearanceModel;
        bgDrawable.SetShadowBitmapDrawingEnable(!bgDrawable.IsRoundRect);
        if (foregroundContentDrawable != null)
        {
            foregroundContentDrawable.ShapeAppearanceModel = shapeAppearanceModel;
        }

        if (foregroundShapeDrawable != null)
        {
            foregroundShapeDrawable.ShapeAppearanceModel = shapeAppearanceModel;
        }

        if (compatRippleDrawable != null)
        {
            compatRippleDrawable.ShapeAppearanceModel = shapeAppearanceModel;
        }
    }

    ShapeAppearanceModel getShapeAppearanceModel()
    {
        return shapeAppearanceModel;
    }

    private void updateInsetForeground(Drawable insetForeground)
    {
        // unsure what getForeground and setForeground is referring to here, perhaps fgDrawable?
        //if (VERSION.SdkInt >= Android.OS.BuildVersionCodes.M && materialCardView.getForeground() is Android.Graphics.Drawables.InsetDrawable)
        //{
        //    ((Android.Graphics.Drawables.InsetDrawable)materialCardView.getForeground()).setDrawable(insetForeground);
        //}
        //else
        //{
        //    materialCardView.setForeground(insetDrawable(insetForeground));
        //}
    }

    private Drawable insetDrawable(Drawable originalDrawable)
    {
        int insetVertical = 0;
        int insetHorizontal = 0;
        bool isPreLollipop = VERSION.SdkInt < Android.OS.BuildVersionCodes.Lollipop;
        if (isPreLollipop || /*materialCardView.*/getUseCompatPadding())
        {
            // Calculate the shadow padding used by CardView
            insetVertical = (int)Math.Ceil(calculateVerticalBackgroundPadding());
            insetHorizontal = (int)Math.Ceil(calculateHorizontalBackgroundPadding());
        }
        // new custom class (see end)
        return new InsetDrawable(originalDrawable, insetHorizontal, insetVertical, insetHorizontal, insetVertical);
    }

    private float calculateVerticalBackgroundPadding()
    {
        return /*materialCardView.*/getMaxCardElevation() * CARD_VIEW_SHADOW_MULTIPLIER + (shouldAddCornerPaddingOutsideCardBackground() ? calculateActualCornerPadding() : 0);
    }

    private float calculateHorizontalBackgroundPadding()
    {
        return /*materialCardView.*/getMaxCardElevation() + (shouldAddCornerPaddingOutsideCardBackground() ? calculateActualCornerPadding() : 0);
    }

    private bool canClipToOutline()
    {
        return VERSION.SdkInt >= Android.OS.BuildVersionCodes.Lollipop && bgDrawable.IsRoundRect;
    }

    private float getParentCardViewCalculatedCornerPadding()
    {
        if (/*materialCardView.*/getPreventCornerOverlap() && (VERSION.SdkInt < Android.OS.BuildVersionCodes.Lollipop || /*materialCardView.*/getUseCompatPadding()))
        {
            return (float)((1 - COS_45) * /*materialCardView.*/getCardViewRadius());
        }
        return 0f;
    }

    private bool shouldAddCornerPaddingInsideCardBackground()
    {
        return /*materialCardView.*/getPreventCornerOverlap() && !canClipToOutline();
    }

    private bool shouldAddCornerPaddingOutsideCardBackground()
    {
        return /*materialCardView.*/getPreventCornerOverlap() && canClipToOutline() && /*materialCardView.*/getUseCompatPadding();
    }

    private float calculateActualCornerPadding()
    {
        return Math.Max(
            Math.Max(
                calculateCornerPaddingForCornerTreatment(
                    shapeAppearanceModel.TopLeftCorner, bgDrawable.TopLeftCornerResolvedSize),
                calculateCornerPaddingForCornerTreatment(
                    shapeAppearanceModel.TopRightCorner,
                    bgDrawable.TopRightCornerResolvedSize)),
            Math.Max(
                calculateCornerPaddingForCornerTreatment(
                    shapeAppearanceModel.BottomRightCorner,
                    bgDrawable.BottomRightCornerResolvedSize),
                calculateCornerPaddingForCornerTreatment(
                    shapeAppearanceModel.BottomLeftCorner,
                    bgDrawable.BottomLeftCornerResolvedSize)));
    }

    private float calculateCornerPaddingForCornerTreatment(CornerTreatment treatment, float size)
    {
        if (treatment is RoundedCornerTreatment)
        {
            return (float)((1 - COS_45) * size);
        }
        else if (treatment is CutCornerTreatment)
        {
            return size / 2;
        }
        return 0;
    }

    private Drawable getClickableForeground()
    {
        if (rippleDrawable == null)
        {
            rippleDrawable = createForegroundRippleDrawable();
        }

        if (clickableForegroundDrawable == null)
        {
            Drawable checkedLayer = createCheckedIconLayer();
            clickableForegroundDrawable = new LayerDrawable(new Drawable[] { rippleDrawable, foregroundContentDrawable, checkedLayer });
            clickableForegroundDrawable.SetId(CHECKED_ICON_LAYER_INDEX, Resource.Id.mtrl_card_checked_layer_id);
        }

        return clickableForegroundDrawable;
    }

    private Drawable createForegroundRippleDrawable()
    {
        if (RippleUtils.UseFrameworkRipple)
        {
            foregroundShapeDrawable = createForegroundShapeDrawable();
            return new RippleDrawable(rippleColor, null, foregroundShapeDrawable);
        }

        return createCompatRippleDrawable();
    }

    private Drawable createCompatRippleDrawable()
    {
        StateListDrawable rippleDrawable = new StateListDrawable();
        compatRippleDrawable = createForegroundShapeDrawable();
        compatRippleDrawable.FillColor = rippleColor;
        rippleDrawable.AddState(new int[] { Android.Resource.Attribute.StatePressed }, compatRippleDrawable);
        return rippleDrawable;
    }

    private void updateRippleColor()
    {
        if (RippleUtils.UseFrameworkRipple && rippleDrawable != null)
        {
            ((RippleDrawable)rippleDrawable).SetColor(rippleColor);
        }
        else if (compatRippleDrawable != null)
        {
            compatRippleDrawable.FillColor = rippleColor;
        }
    }

    private Drawable createCheckedIconLayer()
    {
        StateListDrawable checkedLayer = new StateListDrawable();
        if (checkedIcon != null)
        {
            checkedLayer.AddState(CHECKED_STATE_SET, checkedIcon);
        }
        return checkedLayer;
    }

    private MaterialShapeDrawable createForegroundShapeDrawable()
    {
        return new MaterialShapeDrawable(shapeAppearanceModel);
    }

    // used in "insetDrawable" method
    private class InsetDrawable : Android.Graphics.Drawables.InsetDrawable
    {
        public InsetDrawable(Drawable drawable, float inset) : base(drawable, inset) { }

        public InsetDrawable(Drawable drawable, int inset) : base(drawable, inset) { }

        public InsetDrawable(Drawable drawable, float insetLeftFraction, float insetTopFraction, float insetRightFraction, float insetBottomFraction) : base(drawable, insetLeftFraction, insetTopFraction, insetRightFraction, insetBottomFraction) { }

        public InsetDrawable(Drawable drawable, int insetLeft, int insetTop, int insetRight, int insetBottom) : base(drawable, insetLeft, insetTop, insetRight, insetBottom) { }

        public override int MinimumHeight => -1;

        public override int MinimumWidth => -1;

        public override bool GetPadding(Rect padding)
        {
            return false;
        }
    }

以及以下用途(用于测试目的):

代码语言:javascript
复制
someView.Background = new MaterialCardDrawable(context);

我知道有一些更简单的方法来实现CardView的外观(使用layer-list等),但是,我特别希望实现MaterialCardView的外观(在我的经验中,它们在视觉上是不同的)。我知道MaterialCardView/MaterialCardViewHelper试图将阴影与背景和其他东西混合在一起,这确实使它看起来不同(而且不同到足以让人注意到)。

我坚持这一点,因为我使用的是一个实际的MaterialCardView之前,我打算使用这个“假的”MaterialCardView。因此,我希望确保它们看起来是一样的。

我为什么要这么做?

我使用的是具有不同RecyclerView s的ViewHolder,其中一个ViewHolderMaterialCardView (只显示一次),但是其他两个不是,而这些是显示最多的ViewHolder。一个MaterialTextView (充当一个标题)和一组Chips (每个标题的数量不同)。我计划使用那个MaterialCardDrawable包装它们,以确保RecyclerView对它们进行最佳的“回收”(如果我确实使用了一个实际的MaterialCardView来包装它们,情况就不会如此)。

我想要实现什么?

精确复制MaterialCardView的视觉效果,使用一个简单的MaterialShapeDrawableRecyclerViewItemDecoration一起使用。

我很高兴有一个替代的解决方案,可以准确地复制MaterialCardView的视觉效果。

PS:我也将接受用Java编写的答案(它不需要用C#编写)。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-05-14 04:43:51

也遇到了类似的情况,就像这样:

代码语言:javascript
复制
class CardItemDecorator(
  context: Context,
  @ColorInt color: Int,
  @Px elevation: Float,
  @Px cornerRadius: Float,
) : RecyclerView.ItemDecoration() {

  private val shapeDrawable =
    MaterialShapeDrawable.createWithElevationOverlay(
        context,
        elevation,
    ).apply {
        fillColor = ColorStateList.valueOf(color)
        shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
        setShadowColor(Color.DKGRAY)
        setCornerSize(cornerRadius)
    }

  override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDraw(c, parent, state)
    if (parent.childCount == 0) {
        return
    }

    val firstChild = parent.getChildAt(0)
    val lastChild = parent.getChildAt(parent.childCount - 1)

    shapeDrawable.setBounds(
        parent.left + parent.paddingLeft,
        firstChild.top,
        parent.right - parent.paddingRight,
        lastChild.bottom
    )

    shapeDrawable.draw(c)
  }
}
票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/65459474

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档