diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/CircularImageView.java b/app/src/main/java/cy/agorise/crystalwallet/util/CircularImageView.java new file mode 100644 index 0000000..af8ddfb --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/util/CircularImageView.java @@ -0,0 +1,450 @@ +package cy.agorise.crystalwallet.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +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.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 cy.agorise.crystalwallet.R; + +/** + * Custom ImageView for circular images in Android while maintaining the + * best draw performance and supporting custom borders & selectors. + */ +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(); + 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; + + + 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); + + // 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 + Log.i(TAG, "Bitmap drawable!"); + 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()); + 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 scale = (float) canvasSize / (float) image.getWidth(); + matrix.setScale(scale, scale); + shader.setLocalMatrix(matrix); + } + } + + /** + * @return Whether or not this view is currently + * in its selected state. + */ + public boolean isSelected() { + return this.isSelected; + } +} diff --git a/app/src/main/res/drawable/ken_code_gravatar.png b/app/src/main/res/drawable/ken_code_gravatar.png new file mode 100644 index 0000000..863bda2 Binary files /dev/null and b/app/src/main/res/drawable/ken_code_gravatar.png differ diff --git a/app/src/main/res/layout/board.xml b/app/src/main/res/layout/board.xml index 19248c0..be66098 100644 --- a/app/src/main/res/layout/board.xml +++ b/app/src/main/res/layout/board.xml @@ -25,9 +25,9 @@ app:title="Client Logo" app:titleTextColor="@color/white" > - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file