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)
}
}
Related
I am trying make a game such that when user click on shooter it changes its position and and shoots and if user click on shooted balls (or shoots as named in project) they disappear CODE HERE
Shooter.JAVA
public class shooter extends View {
//Consider all variables are declared
public shooter(int color, Context c) {
super(c);
paint = new Paint();
paint.setColor(color);
mContext = c;
}
public void move() {
//Moves Cannon On Click (CODE NOT SHOWN PURPOSELY)
invalidate();
}
public float getPosition()
{
return shootPoint;
}
public void draw(Canvas canvas)
{
super.draw(canvas);
// simply draws a rectangle (shooter)
cW=getWidth();
cH=getHeight();
center=new Point(cW/2,cH);
left=0; right=center.x; shootPoint=right/2;
canvas.drawRect(left,top,right,bottom,paint);
}
}
Another Java Class Named shoot.java is also there to make shoot balls when shootbutton is clicked
but I want that when user click on those balls(drawn on canvas ) they should reset
Main Game View Class
public class GameView extends FrameLayout implements View.OnClickListener {
//Consider all objects and variables are declared as used
public GameView(Context context, AttributeSet attrs) {
super(context,attrs);
//CONTAIN INITIALIZATION OF OBJECTS AS USED OF OTHER CLASSES
bullets = new ArrayList<shoot> ();
addView(cannon);
for (int i = 0; i < bullets.size(); i++ ) {
addView(bullets.get(i));
bullets.get(i).setOnClickListener(this);// an arrays of objects of shoot class
}
cannon.setOnClickListener(this);
level=3;level++;
}
#Override
public void onClick(View view) {
switch()// Here is the MAIN PROBLEM HOW SHOULD I DIFFERENTIATE THAT CANNON IS CLICKED OR //BULLETS LIKE USING VIEW.GETTAG()
{
case ----1:// WHAT CASE SSHOULD I WRITE
cannon.move();
break;
case ----2: // HERE ALSO
bullets.remove();
break;
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawGameBoard(canvas);
try
{
Thread.sleep(5);
} catch (InterruptedException e) {
}
invalidate();
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
aliens.setBounds(0,0,width,height);
for (int i = 0; i < bullets.size(); i++ ) {
bullets.get(i).setBounds(0,0,width,height);
}
}
public void drawGameBoard(Canvas canvas) {
cannon.draw(canvas);
for ( int i = bullets.size() - 1; i >= 0; i--) {
if (bullets.get(i) != null) {
bullets.get(i).draw(canvas);
}
}
for (int i = explosions.size() - 1; i >= 0; i--) {
if (explosions.get(i) != null) {
if (!explosions.get(i).draw(canvas)) {
explosions.remove(i);
}
}
}
if (aliens != null) {
aliens.draw(canvas);
RectF guyRect = aliens.getRect();
for (int i = bullets.size() - 1; i >= 0; i--) {
if (RectF.intersects(guyRect, bullets.get(i).getRect())) {
explosions.add(new explosion(Color.RED,mContext, aliens.getX()-aliens.dst, aliens.getY()-aliens.dst));
aliens.reset();
bullets.remove(i);
break;
}
}
if (!aliens.move()) {
aliens = null;
}
}
}
// Whenever the user shoots a bullet, create a new bullet moving upwards
public void shootCannon() {
bullets.add(new shoot(Color.RED, mContext, cannon.getPosition(), (float) (height-100)));
}
}
I have showed the part of the code where I am having the problem that is the overridden function ONCLICK in GAMEVIEW.JAVA by comments like how to know what is clicked so todo their respected functions
please notify me if you didn't understand my question
If I understand the question correctly, you want to use one onClickListener function to handle the click events for both your cannon and your bullets.
Since both are different classes you may differentiate between them via '''instanceof'''.
That means your onClickListener would look something like this:
#Override
public void onClick(View view) {
if(view instanceof shooter) {
cannon.move();
}
else if(view instanceof shoot) {
bullets.remove();
}
}
I hope this helps you. If anything remains unclear I'll gladly response :)
If you want to know the coordinates of the touch which you can use to figure out, if an item is tapped,
consider using View.OnTouchListener instead of View.OnClickListener.
View.OnTouchListener has this function:
onTouch(View v, MotionEvent event)
How to use:
check if event is a tap: event.getAction() ==
MotionEvent.ACTION_DOWN
get the coordinates: event.getX() and event.getY()
And after that, replace cannon.setOnClickListener(this); with cannon.setOnTouchListener(this);
If there are any questions, do not hesitate to ask them!
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
Can anyone shed some light on this behaivor of the viewport or ortographic camera? I expect a rectangular viewport to be drawn but what I get instead is everything squeezed up in a square frame. I uploaded a picture to show you what's going on. The center of the green circle is being drawn on to the point(100,100) so clearly , it looks too disproportioned. The size of the red square is supposed to be 400x400 but judging by the way it looks, it isn't . The aspect ratio is completely off. I don't know what's causing this issue. Ignore the little mario in the center. that's being rendered by another camera soewhere else.
Does any one know what could be causing it?
/**
* Created by Omer on 11/24/2015.
*/
public class Play extends GameStateBase {
public static final int CAMERA_TYPE_SCREEN = 0;
public static final int CAMERA_TYPE_MAP = 1;
private Viewport SCREEN_VIEW;
private Viewport MAP_VIEW;
private OrthographicCamera SCREEN_CAM;
private OrthographicCamera MAP_CAM;
AssaultTrooper assaultTrooper;
GamePad GAME_PAD;
public Play(GameStateManager _gameStateManager) {
super(_gameStateManager);
SCREEN_CAM = new OrthographicCamera();
SCREEN_VIEW = new FitViewport(GameGraphics.VIRTUAL_WIDTH,GameGraphics.VIRTUAL_HEIGHT,SCREEN_CAM);
SCREEN_VIEW.apply();
SCREEN_CAM.translate(GameGraphics.VIRTUAL_WIDTH/2,GameGraphics.VIRTUAL_HEIGHT/2);
SCREEN_CAM.update();
MAP_CAM = new OrthographicCamera();
MAP_VIEW = new FitViewport(GameGraphics.VIRTUAL_HEIGHT,GameGraphics.VIRTUAL_HEIGHT,MAP_CAM);
MAP_VIEW.apply();
MAP_CAM.translate(GameGraphics.VIRTUAL_HEIGHT/2,GameGraphics.VIRTUAL_HEIGHT/2);
MAP_CAM.update();
RENDER_STATE.addCamera(CAMERA_TYPE_SCREEN,SCREEN_CAM);
RENDER_STATE.addCamera(CAMERA_TYPE_MAP,MAP_CAM);
GAME_PAD = new GamePad();
RenderStateManager.changeGameRenderState(RENDER_STATE);
GAME_PAD.setCameraType(CAMERA_TYPE_SCREEN);
GAME_PAD.addToRenderState();
assaultTrooper = new AssaultTrooper(GameSettings.TEAM_BLUE,new Location(200,200));
assaultTrooper.setCameraType(CAMERA_TYPE_MAP);
assaultTrooper.addToRenderState();
}
#Override
public void render(SpriteBatch _spriteBatch) {
try {
RenderStateManager.RENDERING_STATE.render(_spriteBatch);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
#Override
public void handleInput() {
}
#Override
public void update(float deltaTime) {
}
#Override
public void dispose() {
}
#Override
public void reSize(int _width, int _height) {
SCREEN_VIEW.update(_width,_height);
MAP_VIEW.update(_width,_height);
}
}
private ConcurrentHashMap<Long,Location> locations;
private ConcurrentHashMap<Long,Sprite> sprites;
private ConcurrentHashMap<Integer,OrthographicCamera> cameras;
private ConcurrentHashMap<Integer,List<Long>> cameraMapping;
public void render(SpriteBatch _spriteBatch) throws InterruptedException {
synchronized (this) {
for (int key : cameras.keySet()) {
_spriteBatch.setProjectionMatrix(cameras.get(key).combined);
_spriteBatch.begin();
if (cameraMapping.get(key) == null) {
_spriteBatch.end();
continue;
}
for (long MAPPER : cameraMapping.get(key)) {
sprites.get(MAPPER).draw(_spriteBatch);
}
_spriteBatch.end();
}
}
}
I need a vertical ViewPager in Android using Xamarin and the solution doesn't work. I searched some examples in java but there's an object developed by the git community that do all the work. Unfortunately there isn't in Xamarin. So, this is my code, it doesn't give me error, but it displays only a black screen. Nothing more.
I Extended the ViewPager class
public class VerticalViewPager : ViewPager {
public VerticalViewPager (Context context):base(context) {
Init ();
}
public VerticalViewPager(Context context, IAttributeSet attr):base(context, attr) {
Init();
}
public override bool OnTouchEvent (Android.Views.MotionEvent e) {
e.SetLocation (e.GetY (), e.GetX ());
return base.OnTouchEvent (e);
}
private void Init() {
SetPageTransformer (true, new PagerTransformer());
OverScrollMode = Android.Views.OverScrollMode.Never;
}
}
and create my PageTransformer
public class PagerTransformer : Java.Lang.Object, ViewPager.IPageTransformer {
int pageWidth;
int pageHeight;
float yPos;
public PagerTransformer () {}
public void TransformPage (View view, float position) {
pageWidth = view.Width;
pageHeight = view.Height;
if (position < -1) {
view.Alpha = 0;
}
else if (position <= 1) {
view.Alpha = 1;
// Counteract the default slide transition
view.TranslationX = (pageWidth * -position);
//set Y position to swipe in from top
yPos = position + pageHeight;
view.TranslationY = yPos;
}
else {
// This page is way off-screen to the right.
view.Alpha = 0;
}
}
}
In the MainActivity, onCreate method i set
page = FindViewById<VerticalViewPager> (Resource.Id.vertical_pager);
and obviously page is a VerticalViewPager object.
If I use a normale ViewPager, the app works fine. Any ideas about the reason of the black screen?
Any java code is appreciare as well!
Thanks
The reason of black screen is that you set your screen position out of visible bounds. Your visible bounds are from 0 to page height.
Try his : "yPos = position * pageHeight;" instead of "yPos = position + pageHeight;"
I am using Box2D in AndEngine (for Android).
My purpose is to create a Force joint whenever 2 objects collide with each other.
When I try to create a Mouse Joint between 2 objects (bodies) during ContactListner process. The application will hang for some time then exit, without any error, just a notification of threads ending.
The Joint creating is OK when I call mEnvironment.CreateForceJoint(..) outside the ContactListener - somewhere while app is running in some physics.UpdateHandler().
Please help me to solve the problems, or find out the reason. Thanks for any help!
This is my code:
public class MyActivity extends SimpleBaseGameActivity {
private final String DEBUG_TAG = "MyActivity";
private GEnvironment mEnvironment;
private PhysicsWorld mPhysicsWorld;
private MyFixture FIXTURE_PLANET = GlobalSettings.FIXTURE_PLANET;
private MyFixture FIXTURE_VACUUM = GlobalSettings.FIXTURE_VACUUM;
// CODE TO CREATE RESOURCES and ENGINE OPTIONS....
#Override
protected Scene onCreateScene() {
Scene scene = new Scene();
scene.setBackground(new Background(0.8627f, 0.9020f, 1.0f));
//CODE: creating physic world
//.....
//creating game environment
mEnvironment = new GEnvironment(mPhysicsWorld, scene, mEngine);
//CODE: creating objects, register and attach them into scene
GMediaPlanet sunZone = mEnvironment.CreatePlanet(x1, y1, sunTextureRegion, FIXTURE_PLANET);
GMediaPlanet earthZone = mEnvironment.CreatePlanet(x2, y2, earthTextureRegion, FIXTURE_PLANET);
// enable contact listener, detect collision between bodies
mPhysicsWorld.setContactListener(new PlanetContactHandler());
return scene;
}
// ----------------------------------------------------
// Handling collision between letter cubes
// ----------------------------------------------------
/**
* Handling the collision of GMediaPlanets
*/
public class PlanetContactHandler implements ContactListener {
private final String DEBUG_TAG = "CONTACT";
// if there's one collision, do not handle others or re-handle it
private boolean mIsColliding = false;
#Override
public void beginContact(Contact contact) {
if (mIsColliding)
return;
//-----------------------------------------------
//suppose colliding objects to be sunZone and earthZone
//-----------------------------------------------
Object aTag = contact.getFixtureA().getBody().getUserData();
Object bTag = contact.getFixtureB().getBody().getUserData();
if (aTag != null && bTag != null) {
GMediaPlanet box = null;
GMediaPlanet cube = null;
if (aTag instanceof GMediaPlanet
&& bTag instanceof GMediaPlanet) {
box = (GMediaPlanet) aTag;
cube = (GMediaPlanet) bTag;
}
if (box != null && cube != null)
{
//!!!!!!!-----------------------------------------------------
//This code will HANG the app when called inside contact listner:
MouseJoint mTestJoint = mEnvironment.CreateForceJoint(box, cube);
//!!!!!!!-----------------------------------------------------
Vector2 target = Vector2Pool.obtain(box.GetLocation());
mTestJoint.setTarget(target);
Vector2Pool.recycle(target);
}
}
mIsColliding = true;
}
#Override
public void endContact(Contact contact) {
Log.d(DEBUG_TAG, "end colliding!");
mIsColliding = false;
}
#Override
public void preSolve(Contact contact, Manifold oldManifold) {
}
#Override
public void postSolve(Contact contact, ContactImpulse impulse) {
}
}
}
public class GMediaPlanet
{
protected IAreaShape mSprite = null;
protected Body mBody = null;
public GMediaPlanet()
{ }
public Vector2 GetLocation()
{
mBody.getPosition();
}
}//end
public class GEnvironment
{
private PhysicsWorld mWorld;
private Scene mScene;
private org.andengine.engine.Engine mEngine;
public GEnvironment(PhysicsWorld pWorld, Scene pScene, org.andengine.engine.Engine pEngine)
{
mWorld = pWorld;
mScene = pScene;
mEngine = pEngine;
}
/** the constructor is hidden, available within Appearances packet only */
public GMediaPlanet CreatePlanet(float pX, float pY, ITextureRegion pTextureRegion, MyFixture pFixture)
{
GMediaPlanet entity = new GMediaPlanet();
entity.mSprite = new Sprite(pX, pY, pTextureRegion, mEngine.getVertexBufferObjectManager());
entity.mBody = PhysicsFactory.createCircleBody(mWorld, entity.mSprite, BodyType.DynamicBody,
pFixture.GetDef(), GlobalSettings.PIXEL_2_METER);
mScene.attachChild(entity.mSprite);
entity.mSprite.setUserData(entity.mBody);
entity.mBody.setUserData(entity);
mWorld.registerPhysicsConnector(new PhysicsConnector(entity.mSprite, entity.mBody, true, true));
return entity;
}
// -----------------------------
// Creating JOINTS
// -----------------------------
/**
* Creating a force joit based on type of MouseJointDef
*
* #param anchorObj
* the static object in the mTestJoint (anchor base)
* #param movingObj
* object to move forward the target
*/
public MouseJoint CreateForceJoint(GMediaPlanet anchorObj, GMediaPlanet movingObj)
{
ChangeFixture(movingObj, GlobalSettings.FIXTURE_VACUUM);
MouseJointDef jointDef = new MouseJointDef();
jointDef.dampingRatio = GlobalSettings.MOUSE_JOINT_DAMP;
jointDef.frequencyHz = GlobalSettings.MOUSE_JOINT_FREQ;
jointDef.collideConnected = true;
Vector2 initPoint = Vector2Pool.obtain(movingObj.mBody.getPosition());
jointDef.bodyA = anchorObj.mBody;
jointDef.bodyB = movingObj.mBody;
jointDef.maxForce = (GlobalSettings.MOUSE_JOINT_ACCELERATOR * movingObj.mBody.getMass());
// very important!!!, the initial target must be position of the sattelite
jointDef.target.set(initPoint);
MouseJoint joint = (MouseJoint) mWorld.createJoint(jointDef);
Vector2Pool.recycle(initPoint);
// very important!!!, the target of the joint then changed to target anchor object
Vector2 targetPoint = Vector2Pool.obtain(anchorObj.mBody.getWorldCenter());
joint.setTarget(targetPoint);
Vector2Pool.recycle(targetPoint);
return joint;
}
public void ChangeFixture(GMediaPlanet entity, MyFixture pFixture)
{
Filter fil = new Filter();
fil.categoryBits = pFixture.categoryBit;
fil.maskBits = pFixture.maskBits;
if(entity.mBody != null)
{
entity.mBody.getFixtureList().get(0).setFilterData(fil);
}
}
}
You can not modify the world in Step()-Call of Box2D because the World is locked! you should get an exception. You have to Remember which objects are colliding and do stuff after beginContact.. for example in an update function.