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've created a SurfaceView and I've put an onTouchEvent on it, and the MotionEvent gets the whole screen, like this:
#Override
public boolean onTouchEvent (MotionEvent event) {
if(event.getAction()==MotionEvent.ACTION_DOWN){
if (!player.getPlaying()&& newGameCreated && reset) {
player.setPlaying(true);
player.fly(true);
lives = 3;
}
if (player.getPlaying()) {
if (!started) started = true;
reset = false;
player.fly(true);
}
return true;
}
if (event.getAction()==MotionEvent.ACTION_UP) {
player.fly(false);
return true;
}
// This is inside a class that extends SurfaceView
return super.onTouchEvent(event);
}
And I need to check touch on bitmap or not using x-y position of touch and do a different action if the condition is true, how can I do this on this onTouchEvent method?
You need to know coordinates of bitmap, his height and width. If you know all of it you can declare method:
public boolean clickedOnBitmap(final int x, final int y) {
return x > bitmap.x && x < bitmap.x + bitmap.width
&& y > bitmap.y && y< bitmap.y + bitmap.height;
}
What's up everyone,
Thanks for your time.
I am making a Pong clone, and I want to restrict Box2D to two MouseJoints maximum; one MouseJoint maximum per paddle. The MouseJoints should be created only if one of the user's two touches lands within either of the two paddle's boundaries.
I am getting a weird result with my code. If my first touch lands within the left paddle and my second touch lands outside of either paddle, a second MouseJoint is created on the left paddle (see attached image).
Note: In addition to the two MouseJoints on the left paddle, there are two PrismaticJoints in the image; one attached to each paddle.
To no avail, I've tried all of the algorithms that I could think of or adapt from other people's code.
If a code solution example or link could be posted, I would be much obliged.
Here is my code:
public class MyScreen implements Screen, InputProcessor{
/*========some variables and methods omitted for clarity========*/
/*multiple mouse joint experiment*/
public MouseJoint mouseJoint[] = new MouseJoint[2];
Body hitBody[] = new Body[2];
Body tempBody;
public MyScreen(Pong game) {
this.game = game;
}
/*---------------------Screen interface methods--------------------------*/
//methods omitted for clarity
/*---------------------end Screen interface methods----------------------*/
/*---------------------InputProcessor interface methods------------------*/
#Override
public boolean keyDown(int keycode) {
return false;
}
#Override
public boolean keyUp(int keycode) {
return false;
}
#Override
public boolean keyTyped(char character) {
return false;
}
#Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
testPoint.set(screenX, screenY, 0);
camera.unproject(testPoint);
// ask the world which bodies are within the given
// bounding box around the mouse pointer
hitBody[pointer] = null;
world.QueryAABB(callback, testPoint.x - 1.0f, testPoint.y - 1.0f, testPoint.x + 1.0f, testPoint.y + 1.0f);
hitBody[pointer] = tempBody;
// if we hit something we create a new mouse joint
// and attach it to the hit body.
if (hitBody[pointer] != null) {
MouseJointDef def = new MouseJointDef();
def.bodyA = groundBody;
def.bodyB = hitBody[pointer];
def.collideConnected = true;
def.target.set(hitBody[pointer].getPosition().x, hitBody[pointer].getPosition().y);
def.maxForce = 3000.0f * hitBody[pointer].getMass();
mouseJoint[pointer] = (MouseJoint)world.createJoint(def);
hitBody[pointer].setAwake(true);
} else {
}
return false;
}
#Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
if (mouseJoint[pointer] != null) {
world.destroyJoint(mouseJoint[pointer]);
mouseJoint[pointer] = null;
}
return false;
}
/**a temporary vector for delta target destination during touchDragged() method**/
Vector2 target = new Vector2();
#Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
if (mouseJoint[pointer] != null) {
camera.unproject(testPoint.set(screenX, screenY, 0));
mouseJoint[pointer].setTarget(target.set(testPoint.x, testPoint.y));
}
return false;
}
#Override
public boolean mouseMoved(int screenX, int screenY) {
return false;
}
#Override
public boolean scrolled(int amount) {
return false;
}
/*----------------end InputProcessor interface methods------------------*/
/*------------------------helper methods------------------------------------*/
/*android screen touch vector for a mouse joint*/
Vector3 testPoint = new Vector3(); //we instantiate this vector and the callback here so we don't irritate the GC
QueryCallback callback = new QueryCallback() {
#Override public boolean reportFixture (Fixture fixture) {
// if the hit fixture's body is the ground body
// we ignore it
if (fixture.getBody() == groundBody) return true;
if (fixture.testPoint(testPoint.x, testPoint.y)) {
tempBody = fixture.getBody();
return false;
} else
return true;
}
};
/*------------------------end helper methods-------------------------------*/
}
From what I can see the tempBody is never reset to null. What that means is that the first time you touch the pad it sets the tempBody to the touched paddle and then when you press outside the body the callback will not find a new body but not reset the testBody to null, so when you assign testBody to hitBody[pointer] it is setting it to the first paddle.
The way your touch down function should look like is:
#Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
testPoint.set(screenX, screenY, 0);
camera.unproject(testPoint);
// ask the world which bodies are within the given
// bounding box around the mouse pointer
hitBody[pointer] = null;
world.QueryAABB(callback, testPoint.x - 1.0f, testPoint.y - 1.0f, testPoint.x + 1.0f, testPoint.y + 1.0f);
hitBody[pointer] = tempBody;
// if we hit something we create a new mouse joint
// and attach it to the hit body.
if (hitBody[pointer] != null) {
MouseJointDef def = new MouseJointDef();
def.bodyA = groundBody;
def.bodyB = hitBody[pointer];
def.collideConnected = true;
def.target.set(hitBody[pointer].getPosition().x, hitBody[pointer].getPosition().y);
def.maxForce = 3000.0f * hitBody[pointer].getMass();
mouseJoint[pointer] = (MouseJoint)world.createJoint(def);
hitBody[pointer].setAwake(true);
} else {
}
tempBody = null;
return false;
}
This way the tempBody is always reset to null after use.
how to perform single touch on isActionMove() because when i move finger on sprites it takes multipal touch events and update scores twice thrice
mHardware[active] = new Sprite(pX, pY, java, this.getVertexBufferObjectManager()) {
#Override
public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float X, float Y) {
if (pSceneTouchEvent.isActionMove()) {
score++;
}
}
};
i cant use isActionDown because its a game like fruit ninja in which i need to move finger across screen
now problem is score is sometimes increasse by 2 sometimes by 3 because when i move finger on sprite application notices several short movements in place of one
you should use
private static boolean isFirstMove;
public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float X, float Y) {
if (pSceneTouchEvent.isActionDown()) {
isFirstMove = true;
}
if (pSceneTouchEvent.isActionMove()) {
if(isFirstMove) {
score++;
isFirstMove = false;
}
}
if (pSceneTouchEvent.isActionUp()) {
isFirstMove = false;
}
});
If you found some what above answer correct for you then I have some suggestion in this.
You have to implement both scene and area touch event for your game scene.
SceneTouch event method contains two events isActionDown() and isActionUp() like in the following code.
public void onSceneTouchEvent(...){
if(pSceneTouchEvent.isActionDown()){
isFirstTouch=true;
return true;
} else if(pSceneTouchEvent.isActionUp()){
isFirstTouch=false;
return true;
}
return false;
}
Area Touch method contains only single event isActionMove() like in the following code.
public void onAreaTouch(...){
if(pSceneTouchEvent.isActionMove()){
if(isFirstMove) {
score++;
isFirstMove = false;
}
return true;
}
return false;
}
You have to follow above strategy because some time desire event not occur for your sprite so you don't get your desire result.
solution given by julien dumortier is working perfectly
static boolean isFirstMove=true;
public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float X, float Y) {
if (pSceneTouchEvent.isActionMove()) {
if(isFirstMove) {
score++;
isFirstMove = false;
}
}
});