Skip to content

Commit

Permalink
Android shadow
Browse files Browse the repository at this point in the history
  • Loading branch information
albyrock87 committed Dec 23, 2024
1 parent dde0d73 commit 2a51aee
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,26 @@ public PlatformContentViewGroup(Context context, AttributeSet attrs, int defStyl
super(context, attrs, defStyle, defStyleRes);
}

private boolean hasClip;
private boolean clipped = false;

/**
* Set by C#, determining if we need to call getClipPath()
* Intentionally invalidates the view in case clip changed
* @param hasClip
*/
protected final void setHasClip(boolean hasClip) {
this.hasClip = hasClip;
protected void setHasClip(boolean hasClip) {
this.clipped = hasClip;
invalidate();
}

protected boolean isClipped() {
return clipped;
}

@Override
protected void dispatchDraw(Canvas canvas) {
// Only call into C# if there is a Clip
if (hasClip) {
if (clipped) {
Path path = getClipPath(canvas.getWidth(), canvas.getHeight());
if (path != null) {
canvas.clipPath(path);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.microsoft.maui;

public class PlatformPaintType {
public static final int NONE = 0;
public static final int SOLID = 1;
public static final int LINEAR = 2;
public static final int RADIAL = 3;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
package com.microsoft.maui;

import android.content.Context;
import android.util.Log;

import android.graphics.BlurMaskFilter;
import android.graphics.Color;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Shader;

import android.view.View;

import androidx.annotation.NonNull;

import com.microsoft.maui.PlatformPaintType;

public abstract class PlatformWrapperView extends PlatformContentViewGroup {
private static final int MAXIMUM_SHADOW_SIZE = 100 * 3; // 100dp * 3 (3x for high density screens)

public PlatformWrapperView(Context context) {
super(context);
this.viewBounds = new Rect();
Expand All @@ -16,18 +30,74 @@ public PlatformWrapperView(Context context) {
}

private final Rect viewBounds;
private boolean hasShadow;

/**
* Set by C#, determining if we need to call drawShadow()
* Intentionally invalidates the view in case shadow definition changed
* @param hasShadow
*/
private Paint shadowPaint;
private Bitmap shadowBitmap;
private Canvas shadowCanvas;
private boolean shadowInvalidated = true;

private int paintType = PlatformPaintType.NONE;
private float offsetX = 0;
private float offsetY = 0;
private float radius = 0;
private int[] colors = new int[0];
private float[] positions = new float[0];
private float[] bounds = new float[0];

@Override
protected void setHasClip(boolean hasClip) {
super.setHasClip(hasClip);
shadowInvalidated = true;
}

protected final void setHasShadow(boolean hasShadow) {
this.hasShadow = hasShadow;
// TODO: remove this method in .NET10
}

protected final void updateShadow(int paintType, float radius, float offsetX, float offsetY, int[] colors, float[] positions, float[] bounds) {
this.paintType = paintType;
this.radius = radius;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.colors = colors;
this.positions = positions;
this.bounds = bounds;

if (paintType == PlatformPaintType.NONE) {
shadowPaint = null;
shadowCanvas = null;
if (shadowBitmap != null) {
shadowBitmap.recycle();
shadowBitmap = null;
}
} else {
shadowCanvas = new Canvas();
shadowPaint = new Paint();
shadowPaint.setAntiAlias(true);
shadowPaint.setDither(true);
shadowPaint.setFilterBitmap(true);
shadowPaint.setMaskFilter(new BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL));

if (paintType == PlatformPaintType.SOLID) {
shadowPaint.setColor(colors.length > 0 ? colors[0] : android.graphics.Color.BLACK);
}
}

shadowInvalidated = true;
invalidate();
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
shadowInvalidated = true;
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
shadowInvalidated = shadowInvalidated || changed;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() == 0) {
Expand All @@ -44,7 +114,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@Override
protected void dispatchDraw(Canvas canvas) {
// Only call into C# if there is a Shadow
if (hasShadow) {
if (paintType != PlatformPaintType.NONE) {
int viewWidth = viewBounds.width();
int viewHeight = viewBounds.height();
if (getChildCount() > 0)
Expand All @@ -55,17 +125,84 @@ protected void dispatchDraw(Canvas canvas) {
if (viewHeight == 0)
viewHeight = child.getMeasuredHeight();
}
drawShadow(canvas, viewWidth, viewHeight);

if (viewWidth > 0 && viewHeight > 0) {
drawShadow(canvas, viewWidth, viewHeight);
}
}
super.dispatchDraw(canvas);
}

/**
* Overridden in C#, for custom logic around shadows
* @param canvas
* @param viewWidth
* @param viewHeight
* @return
*/
protected abstract void drawShadow(@NonNull Canvas canvas, int viewWidth, int viewHeight);
protected void drawShadow(@NonNull Canvas canvas, int viewWidth, int viewHeight) {
if (shadowInvalidated) {
shadowInvalidated = false;

int bitmapWidth = viewHeight + MAXIMUM_SHADOW_SIZE;
int bitmapHeight = viewHeight + MAXIMUM_SHADOW_SIZE;

if (shadowBitmap != null) {
if (shadowBitmap.getWidth() == bitmapWidth && shadowBitmap.getHeight() == bitmapHeight) {
shadowBitmap.eraseColor(Color.TRANSPARENT);
} else {
shadowBitmap.recycle();
shadowBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
}
}

shadowCanvas.setBitmap(shadowBitmap);

// Create the local copy of all content to draw bitmap as a bottom layer of natural canvas.
viewGroupDispatchDraw(shadowCanvas);

// Get the alpha bounds of bitmap
Bitmap extractAlpha = shadowBitmap.extractAlpha();

// Clear the shadow canvas
shadowCanvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);

// Apply shader if needed
Shader shader = createShader(bitmapWidth, bitmapHeight);
if (shader != null) {
shadowPaint.setShader(shader);
}

if (isClipped()) {
Path clipPath = getClipPath(canvas.getWidth(), canvas.getHeight());
clipPath.offset(offsetX, offsetY);
shadowCanvas.drawPath(clipPath, shadowPaint);
} else {
Log.v("Shadow", "Drawing shadow with offset: " + offsetX + ", " + offsetY);
shadowCanvas.drawBitmap(extractAlpha, offsetX, offsetY, shadowPaint);
}

extractAlpha.recycle();
}

// Draw shadow rectangle
canvas.drawBitmap(shadowBitmap, 0, 0, shadowPaint);
}

private Shader createShader(int bitmapWidth, int bitmapHeight) {
Shader shader = null;

if (paintType == PlatformPaintType.LINEAR) {
shader = new android.graphics.LinearGradient(
bounds[0] * bitmapWidth, bounds[1] * bitmapHeight, // Start point
bounds[2] * bitmapWidth, bounds[3] * bitmapHeight, // End point
colors,
positions,
android.graphics.Shader.TileMode.CLAMP
);
} else if (paintType == PlatformPaintType.RADIAL) {
shader = new android.graphics.RadialGradient(
bounds[0] * bitmapWidth, bounds[1] * bitmapHeight, // Center point
bounds[2] * Math.max(bitmapWidth, bitmapHeight), // Radius
colors,
positions,
android.graphics.Shader.TileMode.CLAMP
);
}

return shader;
}
}
Loading

0 comments on commit 2a51aee

Please sign in to comment.