Related
What I've tried?
After having a brief look through the MaterailCardViewHelper source, I tried to replicate the way it draws the associated Drawables. Unfortunately, it results in a black shape with some "treated" corners and looks nothing like the MaterialCardView. I understand the MaterialCardViewHelper applies the background and foreground on the actual CardView and after having looked at the source for that, it doesn't appear to be doing anything special, that is, it just seems to call setBackgroundDrawable (which I am doing on someView, as shown below).
I am using Xamarin so my code is written in C#. I've essentially converted the Java source (of the MaterialCardViewHelper) to its C# equivalent, replacing references of "materialCardView" to MaterialCardDrawable where appropriate.
I've tried to keep the code as close to the original Java source to ensure anyone reading this can easily compare the original with mine. I've changed only enough to make the code compile. The main difference is the "Draw" method which I assume is where my issue lies.
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;
}
}
And usage as follows (for testing purposes):
someView.Background = new MaterialCardDrawable(context);
I know there are simpler ways to achieve the look of a CardView (using layer-list, etc), however, I specifically want to achieve the look of the MaterialCardView (as they do visually differ, in my experience). I know the MaterialCardView/MaterialCardViewHelper attempt to blend shadows with the background and other stuff which does make it look different (and different enough to be noticeable).
I am adamant on this as I am using an actual MaterialCardView just before where I intend to use this "fake" MaterialCardView. And, as such, I wish to ensure they look identical.
Why am I doing this?
I am using a RecyclerView with varying ViewHolders and one ViewHolder is a MaterialCardView (only shown once), however, the other two are not and these are the ViewHolders that are shown the most. A MaterialTextView (which acts as a title) and a bunch of Chips (which vary in number, per title).
I plan to wrap them using that MaterialCardDrawable to ensure optimal "recycling" by the RecyclerView (which wouldn't be case if I did use an actual MaterialCardView to wrap them).
What I'm trying to achieve?
Replicate the visuals of the MaterialCardView accurately, using a simple MaterialShapeDrawable to be used with RecyclerView's ItemDecoration.
I am happy for an alternative solution that can accurately replicate the visuals of the MaterialCardView, as well.
PS: I will also accept answers written in Java (it doesn't have to be written in C#).
Had a similar situation and got it working with something like this:
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)
}
}
How can this code be broken down to follow the principles of single responsibility principle? Even though I understand the SOLID principles and have read through many materials especially Uncle Bob's articles on SOLID principles I have unfortunately been unable to split the following code to two different classes to follow Single Responsibility Principle. I would highly appreciate help from StackOverflow
/** The only subclass the fully utilizes the
Entity superclass (no other class requires
movement in a tile based map).
Contains all the gameplay associated with
the Player.**/
package com.neet.DiamondHunter.Entity;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import com.neet.DiamondHunter.Manager.Content;
import com.neet.DiamondHunter.Manager.JukeBox;
import com.neet.DiamondHunter.TileMap.TileMap;
public class Player extends Entity {
// sprites
private BufferedImage[] downSprites;
private BufferedImage[] leftSprites;
private BufferedImage[] rightSprites;
private BufferedImage[] upSprites;
private BufferedImage[] downBoatSprites;
private BufferedImage[] leftBoatSprites;
private BufferedImage[] rightBoatSprites;
private BufferedImage[] upBoatSprites;
// animation
private final int DOWN = 0;
private final int LEFT = 1;
private final int RIGHT = 2;
private final int UP = 3;
private final int DOWNBOAT = 4;
private final int LEFTBOAT = 5;
private final int RIGHTBOAT = 6;
private final int UPBOAT = 7;
// gameplay
private int numDiamonds;
private int totalDiamonds;
private boolean hasBoat;
private boolean hasAxe;
private boolean onWater;
private long ticks;
// player status
private int healthPoints;
private boolean invincible;
private boolean powerUp;
private boolean speedUp;
public Player(TileMap tm) {
super(tm);
width = 16;
height = 16;
cwidth = 12;
cheight = 12;
moveSpeed = 2;
numDiamonds = 0;
downSprites = Content.PLAYER[0];
leftSprites = Content.PLAYER[1];
rightSprites = Content.PLAYER[2];
upSprites = Content.PLAYER[3];
downBoatSprites = Content.PLAYER[4];
leftBoatSprites = Content.PLAYER[5];
rightBoatSprites = Content.PLAYER[6];
upBoatSprites = Content.PLAYER[7];
animation.setFrames(downSprites);
animation.setDelay(10);
}
private void setAnimation(int i, BufferedImage[] bi, int d) {
setAnimation(i, bi, d, false);
}
private void setAnimation(int i, BufferedImage[] bi, int d, boolean slowMotion) {
currentAnimation = i;
animation.setFrames(bi);
animation.setDelay(d);
slowMotion = true;
}
public void collectedDiamond() { numDiamonds++; }
public int numDiamonds() { return numDiamonds; }
public int getTotalDiamonds() { return totalDiamonds; }
public void setTotalDiamonds(int i) { totalDiamonds = i; }
public int getx() { return x; }
public int gety() { return y; }
public int getRow() { return rowTile; }
public int getCol() { return colTile; }
public void gotBoat() { hasBoat = true; tileMap.replace(22, 4); }
public void gotAxe() { hasAxe = true; }
public boolean hasBoat() { return hasBoat; }
public boolean hasAxe() { return hasAxe; }
public int getHealthPoints() { return healthPoints; }
// Used to update time.
public long getTicks() { return ticks; }
// Keyboard input. Moves the player.
public void setDown() {
super.setDown();
}
public void setLeft() {
super.setLeft();
}
public void setRight() {
super.setRight();
}
public void setUp() {
super.setUp();
}
// Keyboard input.
// If Player has axe, dead trees in front
// of the Player will be chopped down.
public void setAction() {
final boolean pressUPKEY = currentAnimation == UP && tileMap.getIndex(rowTile - 1, colTile) == 21;
final boolean pressDOWNKEY = currentAnimation == DOWN && tileMap.getIndex(rowTile + 1, colTile) == 21;
final boolean pressLEFTKEY = currentAnimation == LEFT && tileMap.getIndex(rowTile, colTile - 1) == 21;
final boolean pressRIGHTKEY = currentAnimation == RIGHT && tileMap.getIndex(rowTile, colTile + 1) == 21;
if(hasAxe) {
if(pressUPKEY) {
tileMap.setTile(rowTile - 1, colTile, 1);
}
if(pressDOWNKEY) {
tileMap.setTile(rowTile + 1, colTile, 1);
}
if(pressLEFTKEY) {
tileMap.setTile(rowTile, colTile - 1, 1);
}
if(pressRIGHTKEY) {
tileMap.setTile(rowTile, colTile + 1, 1);
}
JukeBox.play("tilechange");
}
}
public void update() {
ticks++;
boolean current = onWater;
onWater = CheckIfOnWater();
//if going from land to water
if(!current && onWater){
JukeBox.play("splash");
}
// set animation
setAnimationDown();
setAnimationLeft();
setAnimationRight();
setAnimationUp();
// update position
super.update();
}
public void setAnimationUp() {
if(up) {
if(onWater && currentAnimation != UPBOAT) {
setAnimation(UPBOAT, upBoatSprites, 10);
}
else if(!onWater && currentAnimation != UP) {
setAnimation(UP, upSprites, 10);
}
}
}
public void setAnimationRight() {
if(right) {
if(onWater && currentAnimation != RIGHTBOAT) {
setAnimation(RIGHTBOAT, rightBoatSprites, 10);
}
else if(!onWater && currentAnimation != RIGHT) {
setAnimation(RIGHT, rightSprites, 10);
}
}
}
public void setAnimationLeft() {
if(left) {
if(onWater && currentAnimation != LEFTBOAT) {
setAnimation(LEFTBOAT, leftBoatSprites, 10);
}
else if(!onWater && currentAnimation != LEFT) {
setAnimation(LEFT, leftSprites, 10);
}
}
}
public void setAnimationDown() {
if(down) {
if(onWater && currentAnimation != DOWNBOAT) {
setAnimation(DOWNBOAT, downBoatSprites, 10);
}
else if(!onWater && currentAnimation != DOWN) {
setAnimation(DOWN, downSprites, 10);
}
}
}
public boolean CheckIfOnWater(){
int index = tileMap.getIndex(ydest / tileSize, xdest / tileSize);
if(index == 4) {
return true;
}
else {
return false;
}
}
// Draw Player.
public void draw(Graphics2D g)
{
super.draw(g);
}
}
Try to implement your components in a Model View Controller style, for example move all the code related to the update the view in a package yourapp.view for example, and move the code of your current class Player into a new class for example, GameBoard or GameView or whaterver, where the single responsability of this class is updating the model representations drawing the animations/images etc., in the gameboard screen. Another class for example, PlayerMovement and move all the code related to de keyboard events, currenly in your Player class, where the responsability of this class is catching the keys that makes the player move.
Move all the code related with game orders and desitions and move in another package yourapp.controller or actions or whatever, and create new classes for example PlayerController, GameController or whatever, where the single responsability of this class is receive the player requests to update the game models state, addressing commands and saying to the model classes be updated and saying to de view classes that gets the new model state every time that the model changes; for example when the player has a new location in the game board, or the location of some missil or some character die etc.
Put your model classes for example in other package for example yourapp.character or actors or whatever, and move the code related to the model state creating new classes that represent the game characters or actors or live elements that defining the behabior or roles of your game. For example the player or a Ship or a Cannon etc. This classes their responsability is only define the game character and their characteristis and behavior por example the location in the gameboard, their weapons, their powers, if is live or dead etc., and other info neded about their role into the game.
Try to identify and apply in a second stage, GoF pattern this can help you to refactor your code and make it more acurate to SOLID principles.
Another of thinking about SRP is each unit should have a Single Reason for Change.
Take your class, what are the reasons one might change it in future:
The sprites change.
The animation changes.
The gameplay changes.
The player status logic changes.
Each of those you have organised fields for, so you spotted them yourself. Now just go one step further and create classes for each of those. So that you can change, for example, animation without touching code that affects gameplay. This:
Makes changes safer (fewer side effects in the absence of a good test suite)
Makes it easier to work on, you need to read less code, just the part you're interested in
Makes it easier for your team to review changes (they will know you haven't affected gameplay while changing animation, for example)
Makes it easier for a team to work on different parts of the system with less chance of merge conflicts
Makes it easy to swap out one component for another implementation, you could, for example, re-skin your game by replacing the sprites component with another
I have a HorizontalBarChart from MPAndroidChart library (version v3.0.0-beta1) in which I display the monthly spending of the user's accounts.
So i implemented this method:
List<Account> accounts = getAccounts();
final ArrayList<BarEntry> entries = new ArrayList<>();
Float count = 0F;
for (Account account : accounts) {
entries.add(new BarEntry(count++, new float[]{Float.valueOf(account.getBalance())}, account.getName()));
}
BarDataSet dataset = new BarDataSet(entries, " ");
dataset.setColors(ColorTemplate.PASTEL_COLORS);
dataset.setValueTextSize(10F);
BarData data = new BarData(dataset);
horizontalBarChartMonthlySpending.setData(data);
horizontalBarChartMonthlySpending.setDescription("Gastos por conta neste mês!");
horizontalBarChartMonthlySpending.getAxisLeft().setDrawLabels(false);
horizontalBarChartMonthlySpending.getAxisRight().setDrawLabels(false);
horizontalBarChartMonthlySpending.setFitBars(true);
horizontalBarChartMonthlySpending.setTouchEnabled(false);
And this is what i got:
What i want is, beside every bar, put the description of the related account. I tried to do this in line 6 with the account.getName() but it didn't appear anywhere in the report.
Is there a way to do it?
I had this problem and correct putting this code:
horizontalBarChartMonthlySpending.getXAxis().setValueFormatter(new AxisValueFormatter() {
#Override
public String getFormattedValue(float value, AxisBase axis) {
return entries.get((int) value).getData().toString();
}
#Override
public int getDecimalDigits() {
return 0;
}
});
XAxis xAxis = horizontalBarChartMonthlySpending.getXAxis();
xAxis.setGranularity(1f);
xAxis.setGranularityEnabled(true);
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 8 years ago.
Improve this question
Update: Just to specify, depending on how I change the rules I can set it so within a couple of generations all cells are either permanently alive or dead. I have checked this by echoing statements to console. HOWEVER, this doesn't reflect in the GUI which shows all cells as always the same color.
I am trying to implement a simple Cellular Automaton to replicate the game of life. This uses the MASON library. My three classes:
Cell.java
package sim.app.gol;
import sim.engine.SimState;
import sim.engine.Steppable;
import sim.field.grid.IntGrid2D;
import sim.util.IntBag;
public class Cell implements Steppable {
public IntGrid2D grid = new IntGrid2D(0,0);
public void step(SimState state) {
Matrix matrix = (Matrix) state;
grid.setTo(matrix.matrix);
for(int x = 0; x < grid.getWidth(); x++) {
for(int y = 0; y < grid.getHeight(); y++) {
IntBag nei = grid.getMooreNeighbors(x, y, 2, 0, false, new IntBag(), new IntBag(), new IntBag());
int count = 0;
for(int i = 0; i < nei.size(); i++) {
count += nei.get(i);
}
int currentState = grid.get(x, y);
if(currentState == 0) {
if(count > 3)
matrix.matrix.set(x, y, 1);
} else if(currentState == 1) {
matrix.matrix.set(x,y,0);
}
}
}
}
}
Matrix.java
package sim.app.gol;
import ec.util.MersenneTwisterFast;
import sim.engine.SimState;
import sim.field.grid.IntGrid2D;
public class Matrix extends SimState {
public final int HEIGHT = 10;
public final int WIDTH = 10;
public IntGrid2D matrix = new IntGrid2D(HEIGHT, WIDTH);
public final int NUM_CELLS = 80;
public Matrix(long seed) {
super(seed);
}
public void start() {
super.start();
// Utils for random number generator
MersenneTwisterFast g = new MersenneTwisterFast();
// We set everything to 0, no cells are active
matrix.setTo(0);
// Populating
for(int i = 0; i < NUM_CELLS; i++) {
int x = 0;
int y = 0;
// We don't want to mark as 'active' a cell that is already active
do {
x = g.nextInt(WIDTH);
y = g.nextInt(HEIGHT);
} while(matrix.get(x, y) == 1);
matrix.set(x, y, 1);
}
schedule.scheduleRepeating(new Cell());
}
public static void main(String[] args) {
doLoop(Matrix.class, args);
System.exit(0);
}
}
MatrixWithUI.java
package sim.app.gol;
import java.awt.Color;
import javax.swing.JFrame;
import sim.app.students.Students;
import sim.display.Console;
import sim.display.Controller;
import sim.display.Display2D;
import sim.display.GUIState;
import sim.engine.SimState;
import sim.portrayal.continuous.ContinuousPortrayal2D;
import sim.portrayal.grid.ObjectGridPortrayal2D;
import sim.portrayal.grid.ValueGridPortrayal2D;
import sim.portrayal.simple.OvalPortrayal2D;
public class MatrixWithUI extends GUIState {
public Display2D display;
public JFrame displayFrame;
public ValueGridPortrayal2D matrixPortrayal = new ValueGridPortrayal2D();
public static void main(String[] args) {
MatrixWithUI mwu = new MatrixWithUI();
Console c = new Console(mwu);
c.setVisible(true);
}
public void start() {
super.start();
setupPortrayals();
}
public void load(SimState state) {
super.load(state);
setupPortrayals();
}
public void setupPortrayals() {
Matrix matrix = (Matrix) state;
matrixPortrayal.setField(matrix.matrix);
matrixPortrayal.setPortrayalForAll(new OvalPortrayal2D());
display.reset();
display.setBackdrop(Color.white);
display.repaint();
}
public void init(Controller c) {
super.init(c);
display = new Display2D(600,600,this);
display.setClipping(true);
displayFrame = display.createFrame();
displayFrame.setTitle("Schoolyard Display");
c.registerFrame(displayFrame);
displayFrame.setVisible(true);
display.attach(matrixPortrayal, "Yard");
}
public void quit() {
super.quit();
if (displayFrame != null) displayFrame.dispose();
displayFrame = null;
display = null;
}
public MatrixWithUI() {
super(new Matrix (System.currentTimeMillis()));
}
public MatrixWithUI(SimState state) {
super(state);
}
public static String getName() {
return "Student Schoolyard Cliques";
}
}
However, for some reason all cells are continuously set to 0 (or off). Any thoughts?
Note: this is a tentative answer as I have no way of verifying it at the moment.
First, let's look at the documentation of ValueGridPortrayal2D. It says:
Like other FieldPortrayal2Ds, this class uses an underlying SimplePortrayal2D to draw each separate element in the grid. A default SimplePortrayal2D is provided which draws squares. In the default, the color for the square is determined by looking up the value of the square in a user-provided color-table, or if there is none, by interpolating it between two user-provided colors. See the setColorTable() and setLevels() methods.
So, if you settle for squares rather than ovals, you can drop this line:
matrixPortrayal.setPortrayalForAll(new OvalPortrayal2D());
And instead, add:
java.awt.Color[] colorTable = new java.awt.Color[2];
colorTable[0] = new java.awt.Color(1.0F,0.0F,0.0F,0.0F);
colorTable[1] = new java.awt.Color(1.0F,0.0F,0.0F,1.0F);
matrixPortrayal.setMap( new SimpleColorMap(colorTable) );
This should give you white squares (transparent on a white backdrop) for 0, and red squares for 1.
If you want to draw ovals, this default implementation of a SimplePortrayal2D that uses a map is not available. The documentation goes further to say:
You can also provide your own custom SimplePortrayal2D (use setPortrayalForAll(...) ) to draw elements as you see fit rather than as rectangles. Your SimplePortrayal2D should expect objects passed to its draw method to be of type MutableDouble.
So we need to override the draw() method and treat the passed object - the cell value - as a MutableDouble (by which I assume they mean the one from org.apache.commons.lang):
matrixPortrayal.setPortrayalForAll(new OvalPortrayal2D() {
public void draw(Object object, Graphics2D graphics, DrawInfo2D info) {
MutableDouble valueObj = (MutableDouble)object;
if ( valueObj.intValue() == 0 ) {
paint = new java.awt.Color(1.0F,0.0F,0.0F,0.0F);
} else {
paint = new java.awt.Color(1.0F,0.0F,0.0F,1.0F);
}
filled = true;
super.draw(object, graphics, info);
}
});
So we have created an anonymous subclass of OvalPortrayal2D. It inherits the fields paint, filled and scale from AbstractShapePortrayal2D. So we override paint (java.awt.Paint, which java.awt.Color extends) with the color we need for the particular value, and make sure the oval is filled.
I'm struggeling with the Java FX BarChart.. My own implementation of the chart is a class that extends the Java FX GridPane and holds a BarChart as a member variable.
If I initialize the whole thing everything works perfect, but if I change the data dynamically (add one or remove one data) the layout will be destroyed.
Speaking in pictures this means: (sorry i can't upload picture at the moment)
pic1 - initialization
after adding one element
So the 1st pictures shows the chart after initalization, the 2nd after one element has been added and after deleting one element the categories aren't shown anymore. (I Ccan't upload a picture of this)
So here's my code:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import someCompanyThings.IMyBarChart;
import someCompanyThings.LocaleService;
import someCompanyThings.INlsKey;
/**
* A Chart with vertical or horizontal bars. It is assumed that the Bars represent positive integer numbers.
* Data may be added or removed dynamically but on the first intent it should display static.
*/
public class MyBarChart extends GridPane implements IMyBarChart {
/*
* Due to data binding problems with a generic bar chart, we hold the two possible bar charts as member variables.
* Also each of them get's a list of Series<?, ?>
*/
private BarChart<String, Number> _barChartVertical;
private BarChart<Number, String> _barChartHorizontal;
private final ObservableList<Series<String, Number>> _dataVertical = FXCollections.observableArrayList();
private final ObservableList<Series<Number, String>> _dataHorizontal = FXCollections.observableArrayList();
private long _maxValue = 0;
private boolean _numberAxisInPercent = false;
private boolean _horizontal = false;
public MyBarChart(INlsKey pTitle, INlsKey pXLabel, INlsKey pYLabel, boolean pNumberAxisInPercent, boolean pHorizontal) {
super();
CategoryAxis categoryAxis = new CategoryAxis();
categoryAxis.setId("bar-chart-category-axis");
NumberAxis numberAxis = new NumberAxis(0.0, 1.0, 1.0);
numberAxis.setId("bar-chart-number-axis");
// create bar chart
// horizontal means that the x-axis is a number axis and the y-axis is a category axis
if (pHorizontal) {
categoryAxis.setLabel(LocaleService.getMessage(pYLabel));
numberAxis.setLabel(LocaleService.getMessage(pXLabel));
_barChartHorizontal = new BarChart<Number, String>(numberAxis, categoryAxis);
_barChartHorizontal.setData(_dataHorizontal);
_barChartHorizontal.setTitle(LocaleService.getMessage(pTitle));
getChildren().add(_barChartHorizontal);
}
else {
categoryAxis.setLabel(LocaleService.getMessage(pXLabel));
numberAxis.setLabel(LocaleService.getMessage(pYLabel));
_barChartVertical = new BarChart<String, Number>(categoryAxis, numberAxis);
_barChartVertical.setData(_dataVertical);
_barChartVertical.setTitle(LocaleService.getMessage(pTitle));
getChildren().add(_barChartVertical);
}
_numberAxisInPercent = pNumberAxisInPercent;
_horizontal = pHorizontal;
/*
* layout
*/
setHgrow(getChildren().get(0), Priority.ALWAYS);
setVgrow(getChildren().get(0), Priority.ALWAYS);
}
#Override
public IMyBarChart addSeries(INlsKey pSeriesName, ObservableList<Data<String, Number>> pDataSet) {
final Series<String, Number> series = new Series<String, Number>(LocaleService.getMessage(pSeriesName), pDataSet);
_dataVertical.add(series);
// iterate over the whole data segment and add it to the series
for (final Data<String, Number> data : pDataSet) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getXValue());
Tooltip.install(data.getNode(), tooltip);
if (data.getYValue().longValue() > _maxValue) {
_maxValue = data.getYValue().longValue();
}
}
setNumberAxisScale();
return this;
}
#Override
public IMyBarChart addSeriesHorizontal(INlsKey pSeriesName, ObservableList<Data<Number, String>> pDataSet) {
final Series<Number, String> series = new Series<Number, String>(LocaleService.getMessage(pSeriesName), pDataSet);
_dataHorizontal.add(series);
// iterate over the whole data segment and add it to the series
for (final Data<Number, String> data : pDataSet) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getYValue());
Tooltip.install(data.getNode(), tooltip);
if (data.getXValue().longValue() > _maxValue) {
_maxValue = data.getXValue().longValue();
}
}
setNumberAxisScale();
return this;
}
private void setNumberAxisScale() {
NumberAxis numberAxis = getNumberAxis();
// set the number axis as a percent axis
if (_numberAxisInPercent) {
numberAxis.setUpperBound(100);
numberAxis.setTickUnit(10);
}
else {
numberAxis.setUpperBound(_maxValue + 1);
numberAxis.setTickUnit(1);
}
}
#Override
public void setLegendVisible(boolean pVisible) {
if (_barChartHorizontal != null) {
_barChartHorizontal.setLegendVisible(pVisible);
}
else {
_barChartVertical.setLegendVisible(pVisible);
}
}
#Override
public void setCategories(ObservableList<String> pCategories) {
getCategoryAxis().getCategories().setAll(pCategories);
}
/**
*
* #return the category axis of the used bar chart
*/
private CategoryAxis getCategoryAxis() {
if (_horizontal) {
return (CategoryAxis)_barChartHorizontal.getYAxis();
}
else {
return (CategoryAxis)_barChartVertical.getXAxis();
}
}
/**
*
* #return the number axis of the used bar chart
*/
private NumberAxis getNumberAxis() {
if (_horizontal) {
return (NumberAxis)_barChartHorizontal.getXAxis();
}
else {
return (NumberAxis)_barChartVertical.getYAxis();
}
}
}
The initialization process:
final IMyBarChart tablespacesChart = MyFactory.createBarChart(NlsKeys.tablespacesTitle, NlsKeys.tablespacesXAxis,
NlsKeys.tablespacesYAxis, true, true);
// first bool -> numberAxisInPercent, second bool -> horizontal ortientation
tablespacesChart.setLegendVisible(false);
tablespacesChart.setCategories(model.getListCategories());
tablespacesChart.addSeriesHorizontal(NlsKeys.tablespacesLegendYAxis, model.getListDataUsedMax());
The data changes are realised by another class that just uses
model.getCategories().setAll(MyNewCatList); // or
model.getListDataUsedMax().setAll(MyNewList);
Well, i also tried to implement the chart with just one member variable (like BarChart _barChart) but this didn't work.
Now i have those layout issues and i dunno where they come from. So i hope you can give me a hint :-)
Here's my solution:
First, create a subclass of bar chart to access the private method updateAxisRange:
class MyBarChart<X, Y> extends BarChart<X, Y> {
public MyBarChart(Axis xAxis, Axis yAxis) {
super(xAxis, yAxis);
}
public void relayout() {
updateAxisRange();
}
}
Next, instantiate your bar chart as MyBarChart:
MyBarChart<String, Number> barChart = new MyBarChart<String, Number>(xAxis, yAxis);
And Lastly, you need to listen to resize events on the parent containing the chart, and when they occur, invoke the relayout of the chart.
For example:
BorderPane pane = new BorderPane(barChart);
pane.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) {
barChart.relayout();
}
});