Как имитировать визуальные эффекты MaterialCardView с помощью MaterialShapeDrawable

Что я пробовал?

После краткого просмотра источника MaterailCardViewHelper я попытался воспроизвести то, как он рисует связанные Drawables. К сожалению, это приводит к черной форме с некоторыми «обработанными» углами и совсем не похоже на MaterialCardView. Я понимаю, что MaterialCardViewHelper применяет фон и передний план к фактическому CardView, и после просмотра источника, похоже, он не делает ничего особенного, то есть он просто вызывает setBackgroundDrawable (что я делаю на someView , как показано ниже).

Я использую Xamarin, поэтому мой код написан на C#. Я по существу преобразовал исходный код Java (из MaterialCardViewHelper) в его эквивалент C#, заменив ссылки «materialCardView» на MaterialCardDrawable, где это уместно.

Я старался, чтобы код был как можно ближе к исходному коду Java, чтобы каждый, кто читает это, мог легко сравнить оригинал с моим. Я изменил только достаточно, чтобы код скомпилировался. Основное отличие заключается в методе «Рисование», в котором, как я полагаю, заключается моя проблема.

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

И использование следующим образом (для целей тестирования):

someView.Background = new MaterialCardDrawable(context);

Я знаю, что есть более простые способы добиться внешнего вида CardView (используя layer-list и т. д.), однако я специально хочу добиться внешнего вида MaterialCardView (по моему опыту, они визуально различаются). Я знаю MaterialCardView/MaterialCardViewHelper попытку смешать тени с фоном и другими вещами, которые делают его другим (и достаточно другим, чтобы быть заметным).

Я непреклонен в этом, так как использую настоящую MaterialCardView как раз перед тем, как собираюсь использовать эту «подделку» MaterialCardView. И поэтому я хочу убедиться, что они выглядят одинаково.

Почему я это делаю?

Я использую RecyclerView с разными ViewHolder, и один ViewHolder является MaterialCardView (показан только один раз), однако два других нет, и это ViewHolder, которые отображаются чаще всего. MaterialTextView (который выступает в качестве названия) и набор Chip (количество которых зависит от названия). Я планирую обернуть их, используя этот MaterialCardDrawable, чтобы обеспечить оптимальную «переработку» RecyclerView (чего бы не было, если бы я использовал настоящий MaterialCardView для их обертывания).

Чего я пытаюсь достичь?

Точно воспроизведите визуальные эффекты MaterialCardView, используя простой MaterialShapeDrawable для использования с RecyclerViewItemDecoration.

Я рад за альтернативное решение, которое также может точно воспроизвести визуальные эффекты MaterialCardView.

PS: я также приму ответы, написанные на Java (это не обязательно должно быть написано на C#).

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
706
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Была аналогичная ситуация, и она работала с чем-то вроде этого:

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

Хитрость была в MaterialShapeDrawable.createWithElevationOverlay, большое спасибо за это :)

user959631 01.11.2021 13:08

Это работает по большей части, но при ближайшем рассмотрении кажется, что есть очень небольшая разница. Верхний край MaterialShapeDrawable кажется невидимым (в отличие от настоящего MaterialCardView). Есть ли способ исправить это? Я пробовал setTopEdge безрезультатно.

user959631 02.11.2021 18:08

Другие вопросы по теме