I'm attempting to extend ImageView and add a shadow. I'm running into a problem where the shadow is being clipped by the view bounds and looks quite bad:
I've attempted to set the width/height via LayoutParams
programmatically as well as trying different XML properties like android:adjustViewBounds
, but there is no change in the display. Similarly, setting a android:layout_margin
is ineffective in preventing the shadow from being clipped.
Can anyone help me figure out how to avoid this clipping? I'm sure there is something obvious I'm overlooking here.
My code is very specific at this time to exactly one case: I'm trying to draw a circle "shadow" underneath a circle bitmap. It is obvious that the view bounds are causing the clipping, but I have not been able to find a solution that will allow me to expand the view bounds.
It has been claimed on #android-dev that my math is simply wrong. I am accounting for screen density, which is a common problem. I have triple checked my math on all counts and cannot find where it might be wrong.
Initially, on an xxhdpi screen, density 3.0, the 56dp image is exactly 168px wide and 168px high. After adding 2dp to the width and height to account for the offset, the layoutParams have width=174 and height=174.
My basic approach is to let the super ImageView do its thing and draw the bitmap specified in xml and all I want to do is draw a little something additionally. Is this approach fundamentally flawed?
I use the largest of width or height in onLayout
to determine what the radius of my shadow circle should be: radius = Max(width, height) / 2. I draw a circle with this radius and center point at (Cx, Cy) where Cx is the midpoint of the width plus a x-offset and Cy is the midpoint of the height plus a y-offset to create the shadow effect. I draw the additional circle using a canvas to a bitmap and later in onDraw
I place my circle on the canvas before allowing the ImageView
super onDraw
to take care of the source bitmap.
Additionally, in my onLayout
I attempt to account for the x- and y-offset distances and add those to my view's width and height via LayoutParams
, but no change in the size of the view is evidenced when the view is drawn.
Here is the code that I'm using: https://gitlab.com/dm78/ImageViewWithShadowExample/
Here is the relevant code:
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<dm78.example.imageviewwithshadowexample.CustomShadowImageView
android:id="@+id/circle"
android:layout_gravity="center"
android:src="@drawable/circle"
android:layout_margin="16dp"
android:layout_width="56dp"
android:layout_height="56dp"/>
</FrameLayout>
CustomShadowImageView.java
package dm78.example.imageviewwithshadowexample;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.widget.FrameLayout;
import android.widget.ImageView;
public class CustomShadowImageView extends ImageView {
public static final String TAG = CustomShadowImageView.class.getSimpleName();
public static final float SHADOW_RADIUS_DP = 3f;
public static final float SHADOW_X_OFFSET_DP = 2f;
public static final float SHADOW_Y_OFFSET_DP = 2f;
private Paint mPaint;
private float mShadowRadius;
private float radius;
private float cx;
private float cy;
private float mShadowXOffset;
private float mShadowYOffset;
private Bitmap mShadowBitmap;
private FrameLayout.LayoutParams layoutParams;
private boolean expanded;
public CustomShadowImageView(Context context) {
super(context);
init();
}
public CustomShadowImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomShadowImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
Log.d(TAG, "init " + this.hashCode());
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
mShadowRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_RADIUS_DP, dm);
mShadowXOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_X_OFFSET_DP, dm);
mShadowYOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_Y_OFFSET_DP, dm);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//noinspection deprecation
int shadowColor = getContext().getResources().getColor(R.color.shadow);
mPaint.setColor(shadowColor);
expanded = false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, String.format("onMeasure %d w: %d, h: %d", this.hashCode(), MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(TAG, String.format("onLayout %d changed: %b, l: %d, t: %d, r: %d, b: %d", this.hashCode(), changed, left, top, right, bottom));
super.onLayout(changed, left, top, right, bottom);
if (changed) {
if (!expanded) {
layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
layoutParams.width = (int) (layoutParams.width + mShadowXOffset);
layoutParams.height = (int) (layoutParams.height + mShadowYOffset);
expanded = true;
}
cx = (right - left) / 2 + mShadowXOffset;
cy = (bottom - top) / 2 + mShadowYOffset;
boolean widthGreater = (right - left) > (bottom - top);
radius = (widthGreater ? right - left : bottom - top) / 2;
if (mShadowBitmap == null) {
Bitmap bitmap = Bitmap.createBitmap(right - left, bottom - top, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(cx, cy, radius, mPaint);
if (Build.VERSION.SDK_INT >= 17 && !isInEditMode()) {
RenderScript rs = RenderScript.create(getContext());
Allocation input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
Allocation output = Allocation.createTyped(rs, input.getType());
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(mShadowRadius);
script.setInput(input);
script.forEach(output);
output.copyTo(bitmap);
}
mShadowBitmap = bitmap;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
Log.d(TAG, "onDraw " + this.hashCode());
canvas.drawBitmap(mShadowBitmap, mShadowXOffset, mShadowYOffset, null);
super.onDraw(canvas);
}
}
android:clipChildren="false"
in the parent layout worked for me
If the cause is the view parent's padding, add the following line to the parent xml element:
android:clipToPadding="false"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With