如何使用MaterialShapedRavable模拟materialcardview的视觉效果

ia2d9nvy  于 2021-06-29  发布在  Java
关注(0)|答案(0)|浏览(314)

我试过什么?

在浏览了一下 MaterailCardViewHelper 来源,我试图复制它绘制相关 Drawable s。不幸的是,它的结果是一个黑色的形状与一些“处理”的角落,看起来一点也不像 MaterialCardView . 我明白 MaterialCardViewHelper 在实际场景中应用背景和前景 CardView 在查看了源代码之后,它似乎并没有做什么特别的事情,也就是说,它似乎在调用 setBackgroundDrawable (这是我正在做的 someView ,如下所示)。
我使用的是xamarin,所以我的代码是用c#编写的。我基本上已经转换了 MaterialCardViewHelper )将“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 s和1 ViewHolder 是一个 MaterialCardView (仅显示一次),但是,其他两个不是,它们是 ViewHolder 显示最多的。一 MaterialTextView (作为标题)和一堆 Chip s(每个标题的数字不同)。我打算用这个包起来 MaterialCardDrawable 以确保通过 RecyclerView (如果我真的使用了 MaterialCardView 把它们包起来)。

我想达到什么目的?

复制 MaterialCardView 准确地说,使用一个简单的 MaterialShapeDrawable 与…一起使用 RecyclerViewItemDecoration .
我很高兴有一个替代的解决方案,可以准确地复制的视觉效果 MaterialCardView ,以及。
ps:我也会接受用java编写的答案(不一定要用c#)。

暂无答案!

目前还没有任何答案,快来回答吧!

相关问题