Last active
July 10, 2022 21:56
-
-
Save ManzzBaria/bfacfd9ae024c8c4dbac to your computer and use it in GitHub Desktop.
CircularImageView with inner shadow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.TargetApi; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.graphics.Bitmap; | |
import android.graphics.BitmapShader; | |
import android.graphics.BlurMaskFilter; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.ColorFilter; | |
import android.graphics.Matrix; | |
import android.graphics.Paint; | |
import android.graphics.PorterDuff; | |
import android.graphics.PorterDuffColorFilter; | |
import android.graphics.PorterDuffXfermode; | |
import android.graphics.Rect; | |
import android.graphics.RectF; | |
import android.graphics.Shader; | |
import android.graphics.drawable.BitmapDrawable; | |
import android.graphics.drawable.Drawable; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.widget.ImageView; | |
import org.bethalevi.sidur2.R; | |
public class CircularImageView extends ImageView { | |
// For logging purposes | |
private static final String TAG = CircularImageView.class.getSimpleName(); | |
// Default property values | |
private static final boolean SHADOW_ENABLED = false; | |
private static final float SHADOW_RADIUS = 4f; | |
private static final float SHADOW_DX = 0f; | |
private static final float SHADOW_DY = 2f; | |
private static final int SHADOW_COLOR = Color.BLACK; | |
// Border & Selector configuration variables | |
private boolean hasBorder; | |
private boolean hasSelector; | |
private boolean isSelected; | |
private int borderWidth; | |
private int canvasSize; | |
private int selectorStrokeWidth; | |
// Shadow properties | |
private boolean shadowEnabled; | |
private float shadowRadius; | |
private float shadowDx; | |
private float shadowDy; | |
private int shadowColor; | |
// Objects used for the actual drawing | |
private BitmapShader shader; | |
private Bitmap image; | |
private Paint paint; | |
private Paint paintBorder; | |
private Paint paintSelectorBorder; | |
private ColorFilter selectorFilter; | |
public CircularImageView(Context context) { | |
this(context, null, R.styleable.CircularImageViewStyle_circularImageViewDefault); | |
} | |
public CircularImageView(Context context, AttributeSet attrs) { | |
this(context, attrs, R.styleable.CircularImageViewStyle_circularImageViewDefault); | |
} | |
public CircularImageView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(context, attrs, defStyleAttr); | |
} | |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) | |
public CircularImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | |
super(context, attrs, defStyleAttr, defStyleRes); | |
init(context, attrs, defStyleAttr); | |
} | |
/** | |
* Initializes paint objects and sets desired attributes. | |
* @param context Context | |
* @param attrs Attributes | |
* @param defStyle Default Style | |
*/ | |
private void init(Context context, AttributeSet attrs, int defStyle) { | |
// Initialize paint objects | |
paint = new Paint(); | |
paint.setAntiAlias(true); | |
paintBorder = new Paint(); | |
paintBorder.setAntiAlias(true); | |
paintBorder.setStyle(Paint.Style.STROKE); | |
paintSelectorBorder = new Paint(); | |
paintSelectorBorder.setAntiAlias(true); | |
// Enable software rendering on HoneyComb and up. (needed for shadow) | |
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) | |
setLayerType(LAYER_TYPE_SOFTWARE, null); | |
// Load the styled attributes and set their properties | |
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.CircularImageView, defStyle, 0); | |
// Check for extra features being enabled | |
hasBorder = attributes.getBoolean(R.styleable.CircularImageView_civ_border, false); | |
hasSelector = attributes.getBoolean(R.styleable.CircularImageView_civ_selector, false); | |
shadowEnabled = attributes.getBoolean(R.styleable.CircularImageView_civ_shadow, SHADOW_ENABLED); | |
// Set border properties, if enabled | |
if(hasBorder) { | |
int defaultBorderSize = (int) (2 * context.getResources().getDisplayMetrics().density + 0.5f); | |
setBorderWidth(attributes.getDimensionPixelOffset(R.styleable.CircularImageView_civ_borderWidth, defaultBorderSize)); | |
setBorderColor(attributes.getColor(R.styleable.CircularImageView_civ_borderColor, Color.WHITE)); | |
} | |
// Set selector properties, if enabled | |
if(hasSelector) { | |
int defaultSelectorSize = (int) (2 * context.getResources().getDisplayMetrics().density + 0.5f); | |
setSelectorColor(attributes.getColor(R.styleable.CircularImageView_civ_selectorColor, Color.TRANSPARENT)); | |
setSelectorStrokeWidth(attributes.getDimensionPixelOffset(R.styleable.CircularImageView_civ_selectorStrokeWidth, defaultSelectorSize)); | |
setSelectorStrokeColor(attributes.getColor(R.styleable.CircularImageView_civ_selectorStrokeColor, Color.BLUE)); | |
} | |
// Set shadow properties, if enabled | |
if(shadowEnabled) { | |
shadowRadius = attributes.getFloat(R.styleable.CircularImageView_civ_shadowRadius, SHADOW_RADIUS); | |
shadowDx = attributes.getFloat(R.styleable.CircularImageView_civ_shadowDx, SHADOW_DX); | |
shadowDy = attributes.getFloat(R.styleable.CircularImageView_civ_shadowDy, SHADOW_DY); | |
shadowColor = attributes.getColor(R.styleable.CircularImageView_civ_shadowColor, SHADOW_COLOR); | |
setShadowEnabled(true); | |
} | |
// We no longer need our attributes TypedArray, give it back to cache | |
attributes.recycle(); | |
} | |
/** | |
* Sets the CircularImageView's border width in pixels. | |
* @param borderWidth Width in pixels for the border. | |
*/ | |
public void setBorderWidth(int borderWidth) { | |
this.borderWidth = borderWidth; | |
if(paintBorder != null) | |
paintBorder.setStrokeWidth(borderWidth); | |
requestLayout(); | |
invalidate(); | |
} | |
/** | |
* Sets the CircularImageView's basic border color. | |
* @param borderColor The new color (including alpha) to set the border. | |
*/ | |
public void setBorderColor(int borderColor) { | |
if (paintBorder != null) | |
paintBorder.setColor(borderColor); | |
this.invalidate(); | |
} | |
/** | |
* Sets the color of the selector to be draw over the | |
* CircularImageView. Be sure to provide some opacity. | |
* @param selectorColor The color (including alpha) to set for the selector overlay. | |
*/ | |
public void setSelectorColor(int selectorColor) { | |
this.selectorFilter = new PorterDuffColorFilter(selectorColor, PorterDuff.Mode.SRC_ATOP); | |
this.invalidate(); | |
} | |
/** | |
* Sets the stroke width to be drawn around the CircularImageView | |
* during click events when the selector is enabled. | |
* @param selectorStrokeWidth Width in pixels for the selector stroke. | |
*/ | |
public void setSelectorStrokeWidth(int selectorStrokeWidth) { | |
this.selectorStrokeWidth = selectorStrokeWidth; | |
this.requestLayout(); | |
this.invalidate(); | |
} | |
/** | |
* Sets the stroke color to be drawn around the CircularImageView | |
* during click events when the selector is enabled. | |
* @param selectorStrokeColor The color (including alpha) to set for the selector stroke. | |
*/ | |
public void setSelectorStrokeColor(int selectorStrokeColor) { | |
if (paintSelectorBorder != null) | |
paintSelectorBorder.setColor(selectorStrokeColor); | |
this.invalidate(); | |
} | |
/** | |
* Enables a dark shadow for this CircularImageView. | |
* @param enabled Set to true to draw a shadow or false to disable it. | |
*/ | |
public void setShadowEnabled(boolean enabled) { | |
shadowEnabled = enabled; | |
updateShadow(); | |
} | |
/** | |
* Enables a dark shadow for this CircularImageView. | |
* If the radius is set to 0, the shadow is removed. | |
* @param radius Radius for the shadow to extend to. | |
* @param dx Horizontal shadow offset. | |
* @param dy Vertical shadow offset. | |
* @param color The color of the shadow to apply. | |
*/ | |
public void setShadow(float radius, float dx, float dy, int color) { | |
shadowRadius = radius; | |
shadowDx = dx; | |
shadowDy = dy; | |
shadowColor = color; | |
updateShadow(); | |
} | |
@Override | |
public void onDraw(Canvas canvas) { | |
// Don't draw anything without an image | |
if(image == null) | |
return; | |
// Nothing to draw (Empty bounds) | |
if(image.getHeight() == 0 || image.getWidth() == 0) | |
return; | |
// Update shader if canvas size has changed | |
int oldCanvasSize = canvasSize; | |
canvasSize = getWidth() < getHeight() ? getWidth() : getHeight(); | |
//canvasSize = canvas.getWidth() < canvas.getHeight() ? canvas.getWidth() : canvas.getHeight(); | |
if(oldCanvasSize != canvasSize) | |
updateBitmapShader(); | |
// Apply shader to paint | |
paint.setShader(shader); | |
// Keep track of selectorStroke/border width | |
int outerWidth = 0; | |
// Get the exact X/Y axis of the view | |
int center = canvasSize / 2; | |
int radius = getWidth(); | |
final Rect rect = new Rect(0, 0, image.getWidth(), image.getHeight()); | |
if(hasSelector && isSelected) { // Draw the selector stroke & apply the selector filter, if applicable | |
outerWidth = selectorStrokeWidth; | |
center = (canvasSize - (outerWidth * 2)) / 2; | |
paint.setColorFilter(selectorFilter); | |
canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2) + outerWidth - 4.0f, paintSelectorBorder); | |
} | |
else if(hasBorder) { // If no selector was drawn, draw a border and clear the filter instead... if enabled | |
outerWidth = borderWidth; | |
center = (canvasSize - (outerWidth * 2)) / 2; | |
paint.setColorFilter(null); | |
RectF rekt = new RectF(0 + outerWidth / 2, 0 + outerWidth / 2, canvasSize - outerWidth / 2, canvasSize - outerWidth / 2); | |
canvas.drawArc(rekt, 360, 360, false, paintBorder); | |
//canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2) + outerWidth - 4.0f, paintBorder); | |
} | |
else // Clear the color filter if no selector nor border were drawn | |
paint.setColorFilter(null); | |
Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
backgroundPaint.setColor(0xFF000000); | |
//backgroundPaint.setColor(getResources().getColor(R.color.white)); | |
canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2), backgroundPaint); | |
Shader bitmapShader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); | |
/*if(canvasSize != image.getWidth() || canvasSize != image.getHeight()) { | |
Matrix matrix = new Matrix(); | |
float scale = (float) canvasSize / (float) image.getHeight(); | |
//float scale = (float) canvasSize / (float) image.getWidth(); | |
matrix.setScale(scale, scale); | |
bitmapShader.setLocalMatrix(matrix); | |
}*/ | |
if(canvasSize != image.getWidth() || canvasSize != image.getHeight()) { | |
Matrix matrix = new Matrix(); | |
int dwidth = getDrawable().getIntrinsicWidth(); | |
int dheight = getDrawable().getIntrinsicHeight(); | |
int vwidth = getWidth() - getPaddingLeft() - getPaddingRight(); | |
int vheight = getHeight() - getPaddingTop() - getPaddingBottom(); | |
float scale; | |
float dx = 0, dy = 0; | |
if (dwidth * vheight > vwidth * dheight) { | |
scale = (float) vheight / (float) dheight; | |
dx = (vwidth - dwidth * scale) * 0.5f; | |
} else { | |
scale = (float) vwidth / (float) dwidth; | |
dy = (vheight - dheight * scale) * 0.5f; | |
} | |
matrix.setScale(scale, scale); | |
matrix.postTranslate(Math.round(dx), Math.round(dy)); | |
bitmapShader.setLocalMatrix(matrix); | |
} | |
Paint imagePaint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
imagePaint.setColor(0xFF000000); | |
imagePaint.setMaskFilter(new BlurMaskFilter(12, BlurMaskFilter.Blur.INNER)); | |
imagePaint.setShader(bitmapShader); | |
canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2), imagePaint); | |
// Draw the circular image itself | |
//canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2), paint); | |
} | |
@Override | |
public boolean dispatchTouchEvent(MotionEvent event) { | |
// Check for clickable state and do nothing if disabled | |
if(!this.isClickable()) { | |
this.isSelected = false; | |
return super.onTouchEvent(event); | |
} | |
// Set selected state based on Motion Event | |
switch(event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
this.isSelected = true; | |
break; | |
case MotionEvent.ACTION_UP: | |
case MotionEvent.ACTION_SCROLL: | |
case MotionEvent.ACTION_OUTSIDE: | |
case MotionEvent.ACTION_CANCEL: | |
this.isSelected = false; | |
break; | |
} | |
// Redraw image and return super type | |
this.invalidate(); | |
return super.dispatchTouchEvent(event); | |
} | |
@Override | |
public void setImageURI(Uri uri) { | |
super.setImageURI(uri); | |
// Extract a Bitmap out of the drawable & set it as the main shader | |
image = drawableToBitmap(getDrawable()); | |
if(canvasSize > 0) | |
updateBitmapShader(); | |
} | |
@Override | |
public void setImageResource(int resId) { | |
super.setImageResource(resId); | |
// Extract a Bitmap out of the drawable & set it as the main shader | |
image = drawableToBitmap(getDrawable()); | |
if(canvasSize > 0) | |
updateBitmapShader(); | |
} | |
@Override | |
public void setImageDrawable(Drawable drawable) { | |
super.setImageDrawable(drawable); | |
// Extract a Bitmap out of the drawable & set it as the main shader | |
image = drawableToBitmap(getDrawable()); | |
if(canvasSize > 0) | |
updateBitmapShader(); | |
} | |
@Override | |
public void setImageBitmap(Bitmap bm) { | |
super.setImageBitmap(bm); | |
// Extract a Bitmap out of the drawable & set it as the main shader | |
image = bm; | |
if(canvasSize > 0) | |
updateBitmapShader(); | |
} | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
int width = measureWidth(widthMeasureSpec); | |
int height = measureHeight(heightMeasureSpec); | |
setMeasuredDimension(width, height); | |
} | |
private int measureWidth(int measureSpec) { | |
int result; | |
int specMode = MeasureSpec.getMode(measureSpec); | |
int specSize = MeasureSpec.getSize(measureSpec); | |
if (specMode == MeasureSpec.EXACTLY) { | |
// The parent has determined an exact size for the child. | |
result = specSize; | |
} | |
else if (specMode == MeasureSpec.AT_MOST) { | |
// The child can be as large as it wants up to the specified size. | |
result = specSize; | |
} | |
else { | |
// The parent has not imposed any constraint on the child. | |
result = canvasSize; | |
} | |
return result; | |
} | |
private int measureHeight(int measureSpecHeight) { | |
int result; | |
int specMode = MeasureSpec.getMode(measureSpecHeight); | |
int specSize = MeasureSpec.getSize(measureSpecHeight); | |
if (specMode == MeasureSpec.EXACTLY) { | |
// We were told how big to be | |
result = specSize; | |
} else if (specMode == MeasureSpec.AT_MOST) { | |
// The child can be as large as it wants up to the specified size. | |
result = specSize; | |
} else { | |
// Measure the text (beware: ascent is a negative number) | |
result = canvasSize; | |
} | |
return (result + 2); | |
} | |
// TODO: Update shadow layers based on border/selector state and visibility. | |
private void updateShadow() { | |
float radius = shadowEnabled ? shadowRadius : 0; | |
//paint.setShadowLayer(radius, shadowDx, shadowDy, shadowColor); | |
paintBorder.setShadowLayer(radius, shadowDx, shadowDy, shadowColor); | |
paintSelectorBorder.setShadowLayer(radius, shadowDx, shadowDy, shadowColor); | |
} | |
/** | |
* Convert a drawable object into a Bitmap. | |
* @param drawable Drawable to extract a Bitmap from. | |
* @return A Bitmap created from the drawable parameter. | |
*/ | |
public Bitmap drawableToBitmap(Drawable drawable) { | |
if (drawable == null) // Don't do anything without a proper drawable | |
return null; | |
else if (drawable instanceof BitmapDrawable) { // Use the getBitmap() method instead if BitmapDrawable | |
return ((BitmapDrawable) drawable).getBitmap(); | |
} | |
int intrinsicWidth = drawable.getIntrinsicWidth(); | |
int intrinsicHeight = drawable.getIntrinsicHeight(); | |
if (!(intrinsicWidth > 0 && intrinsicHeight > 0)) | |
return null; | |
try { | |
// Create Bitmap object out of the drawable | |
Bitmap bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888); | |
Canvas canvas = new Canvas(bitmap); | |
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); | |
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); | |
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
paint.setColor(0xFF000000); | |
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); | |
canvas.drawBitmap(bitmap, rect, rect, paint); | |
drawable.draw(canvas); | |
return bitmap; | |
} catch (OutOfMemoryError e) { | |
// Simply return null of failed bitmap creations | |
Log.e(TAG, "Encountered OutOfMemoryError while generating bitmap!"); | |
return null; | |
} | |
} | |
// TODO TEST REMOVE | |
public void setIconModeEnabled(boolean e) {} | |
/** | |
* Re-initializes the shader texture used to fill in | |
* the Circle upon drawing. | |
*/ | |
public void updateBitmapShader() { | |
if (image == null) | |
return; | |
shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); | |
if(canvasSize != image.getWidth() || canvasSize != image.getHeight()) { | |
Matrix matrix = new Matrix(); | |
/*float scalex = (float) canvasSize / (float) image.getHeight(); | |
float scaley = (float) canvasSize / (float) image.getWidth(); | |
matrix.setScale(scalex, scalex); | |
shader.setLocalMatrix(matrix);*/ | |
int dwidth = getDrawable().getIntrinsicWidth(); | |
int dheight = getDrawable().getIntrinsicHeight(); | |
int vwidth = getWidth() - getPaddingLeft() - getPaddingRight(); | |
int vheight = getHeight() - getPaddingTop() - getPaddingBottom(); | |
float scale; | |
float dx = 0, dy = 0; | |
if (dwidth * vheight > vwidth * dheight) { | |
scale = (float) vheight / (float) dheight; | |
dx = (vwidth - dwidth * scale) * 0.5f; | |
} else { | |
scale = (float) vwidth / (float) dwidth; | |
dy = (vheight - dheight * scale) * 0.5f; | |
} | |
matrix.setScale(scale, scale); | |
matrix.postTranslate(Math.round(dx), Math.round(dy)); | |
shader.setLocalMatrix(matrix); | |
} | |
} | |
/** | |
* @return Whether or not this view is currently | |
* in its selected state. | |
*/ | |
public boolean isSelected() { | |
return this.isSelected; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
you forget R.style.CircularImageViewStyle_circularImageViewDefault