Skip to content

Instantly share code, notes, and snippets.

@cesco89
Last active April 5, 2016 15:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cesco89/a657530be92890be7905 to your computer and use it in GitHub Desktop.
Save cesco89/a657530be92890be7905 to your computer and use it in GitHub Desktop.
A Circular Flippable ImageView (See README.md for details and credits)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircularFlipImageView">
<attr name="border" format="boolean"></attr>
<attr name="border_width" format="dimension"></attr>
<attr name="border_color" format="color"></attr>
<attr name="shadow" format="boolean"></attr>
<attr name="isAnimated" format="boolean" />
<attr name="isFlipped" format="boolean" />
<attr name="flipDrawable" format="reference" />
<attr name="flipDuration" format="integer" />
<attr name="flipInterpolator" format="reference" />
<attr name="flipRotations">
<flag name="none" value="0" />
<flag name="x" value="1" />
<flag name="y" value="2" />
<flag name="z" value="4" />
</attr>
<attr name="reverseRotation" format="boolean"></attr>
</declare-styleable>
</resources>
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.ImageView;
public class CircularFlipImageView extends ImageView implements View.OnClickListener,
Animation.AnimationListener {
private int borderWidth;
private int canvasSize;
private Bitmap image;
private Paint paint;
private Paint paintBorder;
private static final int FLAG_ROTATION_X = 1 << 0;
private static final int FLAG_ROTATION_Y = 1 << 1;
private static final int FLAG_ROTATION_Z = 1 << 2;
private static final Interpolator fDefaultInterpolator = new DecelerateInterpolator();
private static int sDefaultDuration;
private static int sDefaultRotations;
private static boolean sDefaultAnimated;
private static boolean sDefaultFlipped;
private static boolean sDefaultIsRotationReversed;
public interface OnFlipListener {
public void onClick(CircularFlipImageView view);
public void onFlipStart(CircularFlipImageView view);
public void onFlipEnd(CircularFlipImageView view);
}
private OnFlipListener mListener;
private boolean mIsFlipped;
private boolean mIsDefaultAnimated;
private Drawable mDrawable;
private Drawable mFlippedDrawable;
private FlipAnimator mAnimation;
private boolean mIsRotationXEnabled;
private boolean mIsRotationYEnabled;
private boolean mIsRotationZEnabled;
private boolean mIsFlipping;
private boolean mIsRotationReversed;
public CircularFlipImageView(final Context context) {
this(context, null);
}
public CircularFlipImageView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.circularImageViewStyle);
}
public CircularFlipImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// init paint
paint = new Paint();
paint.setAntiAlias(true);
init(context, attrs, defStyle);
paintBorder = new Paint();
paintBorder.setAntiAlias(true);
// load the styled attributes and set their properties
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.CircularFlipImageView, defStyle, 0);
if(attributes.getBoolean(R.styleable.CircularFlipImageView_border, true)) {
int defaultBorderSize = (int) (4 * getContext().getResources().getDisplayMetrics().density + 0.5f);
setBorderWidth(attributes.getDimensionPixelOffset(R.styleable.CircularFlipImageView_border_width, defaultBorderSize));
setBorderColor(attributes.getColor(R.styleable.CircularFlipImageView_border_color, Color.WHITE));
}
if(attributes.getBoolean(R.styleable.CircularFlipImageView_shadow, false))
addShadow();
}
public void setBorderWidth(int borderWidth) {
this.borderWidth = borderWidth;
this.requestLayout();
this.invalidate();
}
public void setBorderColor(int borderColor) {
if (paintBorder != null)
paintBorder.setColor(borderColor);
this.invalidate();
}
public void addShadow() {
setLayerType(LAYER_TYPE_SOFTWARE, paintBorder);
paintBorder.setShadowLayer(4.0f, 0.0f, 2.0f, Color.BLACK);
}
@Override
public void onDraw(Canvas canvas) {
// load the bitmap
image = drawableToBitmap(getDrawable());
// init shader
if (image != null) {
canvasSize = canvas.getWidth();
if(canvas.getHeight()<canvasSize)
canvasSize = canvas.getHeight();
BitmapShader shader = new BitmapShader(Bitmap.createScaledBitmap(image, canvasSize, canvasSize, false), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(shader);
// circleCenter is the x or y of the view's center
// radius is the radius in pixels of the cirle to be drawn
// paint contains the shader that will texture the shape
int circleCenter = (canvasSize - (borderWidth * 2)) / 2;
canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, ((canvasSize - (borderWidth * 2)) / 2) + borderWidth - 4.0f, paintBorder);
canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, ((canvasSize - (borderWidth * 2)) / 2) - 4.0f, paint);
//Added to restore image if loaded programmatically or Async
if(!isFlipped()) {
mDrawable = new BitmapDrawable(getContext().getResources(), image);
}
}
}
@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 = 0;
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 = 0;
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);
}
public Bitmap drawableToBitmap(Drawable drawable) {
if (drawable == null) {
return null;
} else if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap((drawable.getIntrinsicWidth() != -1 ? drawable.getIntrinsicWidth() : this.getWidth()),
(drawable.getIntrinsicHeight() != -1 ? drawable.getIntrinsicHeight() : this.getHeight()), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
private void init(Context context, AttributeSet attrs, int defStyle) {
sDefaultDuration = context.getResources().getInteger(R.integer.default_civ_duration);
sDefaultRotations = context.getResources().getInteger(R.integer.default_civ_rotations);
sDefaultAnimated = context.getResources().getBoolean(R.bool.default_civ_isAnimated);
sDefaultFlipped = context.getResources().getBoolean(R.bool.default_civ_isFlipped);
sDefaultIsRotationReversed = context.getResources().getBoolean(R.bool.default_civ_isRotationReversed);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircularFlipImageView, defStyle, 0);
mIsDefaultAnimated = a.getBoolean(R.styleable.CircularFlipImageView_isAnimated, sDefaultAnimated);
mIsFlipped = a.getBoolean(R.styleable.CircularFlipImageView_isFlipped, sDefaultFlipped);
mFlippedDrawable = a.getDrawable(R.styleable.CircularFlipImageView_flipDrawable);
int duration = a.getInt(R.styleable.CircularFlipImageView_flipDuration, sDefaultDuration);
int interpolatorResId = a.getResourceId(R.styleable.CircularFlipImageView_flipInterpolator, 0);
Interpolator interpolator = interpolatorResId > 0 ? AnimationUtils
.loadInterpolator(context, interpolatorResId) : fDefaultInterpolator;
int rotations = a.getInteger(R.styleable.CircularFlipImageView_flipRotations, sDefaultRotations);
mIsRotationXEnabled = (rotations & FLAG_ROTATION_X) != 0;
mIsRotationYEnabled = (rotations & FLAG_ROTATION_Y) != 0;
mIsRotationZEnabled = (rotations & FLAG_ROTATION_Z) != 0;
mDrawable = getDrawable();
mIsRotationReversed = a.getBoolean(R.styleable.CircularFlipImageView_reverseRotation, sDefaultIsRotationReversed);
mAnimation = new FlipAnimator();
mAnimation.setAnimationListener(this);
mAnimation.setInterpolator(interpolator);
mAnimation.setDuration(duration);
setOnClickListener(this);
setImageDrawable(mIsFlipped ? mFlippedDrawable : mDrawable);
mIsFlipping = false;
a.recycle();
}
public void setFlippedDrawable(Drawable flippedDrawable){
mFlippedDrawable = flippedDrawable;
if(mIsFlipped) setImageDrawable(mFlippedDrawable);
}
public void setDrawable(Drawable drawable){
mDrawable = drawable;
if(!mIsFlipped) setImageDrawable(mDrawable);
}
public boolean isRotationXEnabled() {
return mIsRotationXEnabled;
}
public void setRotationXEnabled(boolean enabled) {
mIsRotationXEnabled = enabled;
}
public boolean isRotationYEnabled() {
return mIsRotationYEnabled;
}
public void setRotationYEnabled(boolean enabled) {
mIsRotationYEnabled = enabled;
}
public boolean isRotationZEnabled() {
return mIsRotationZEnabled;
}
public void setRotationZEnabled(boolean enabled) {
mIsRotationZEnabled = enabled;
}
public FlipAnimator getFlipAnimation() {
return mAnimation;
}
public void setInterpolator(Interpolator interpolator) {
mAnimation.setInterpolator(interpolator);
}
public void setDuration(int duration) {
mAnimation.setDuration(duration);
}
public boolean isFlipped() {
return mIsFlipped;
}
public boolean isFlipping() {
return mIsFlipping;
}
public boolean isRotationReversed(){
return mIsRotationReversed;
}
public void setRotationReversed(boolean rotationReversed){
mIsRotationReversed = rotationReversed;
}
public boolean isAnimated() {
return mIsDefaultAnimated;
}
public void setAnimated(boolean animated) {
mIsDefaultAnimated = animated;
}
public void setFlipped(boolean flipped) {
setFlipped(flipped, mIsDefaultAnimated);
}
public void setFlipped(boolean flipped, boolean animated) {
if (flipped != mIsFlipped) {
toggleFlip(animated);
}
}
public void toggleFlip() {
toggleFlip(mIsDefaultAnimated);
}
public void toggleFlip(boolean animated) {
if (animated) {
mAnimation.setToDrawable(mIsFlipped ? mDrawable : mFlippedDrawable);
startAnimation(mAnimation);
} else {
setImageDrawable(mIsFlipped ? mDrawable : mFlippedDrawable);
}
mIsFlipped = !mIsFlipped;
}
public void setOnFlipListener(OnFlipListener listener) {
mListener = listener;
}
@Override
public void onClick(View v) {
toggleFlip();
if (mListener != null) {
mListener.onClick(this);
}
}
@Override
public void onAnimationStart(Animation animation) {
if (mListener != null) {
mListener.onFlipStart(this);
}
mIsFlipping = true;
}
@Override
public void onAnimationEnd(Animation animation) {
if (mListener != null) {
mListener.onFlipEnd(this);
}
mIsFlipping = false;
}
@Override
public void onAnimationRepeat(Animation animation) {
}
/**
* Animation part All credits goes to coomar
*/
public class FlipAnimator extends Animation {
private Camera camera;
private Drawable toDrawable;
private float centerX;
private float centerY;
private boolean visibilitySwapped;
public void setToDrawable(Drawable to) {
toDrawable = to;
visibilitySwapped = false;
}
public FlipAnimator() {
setFillAfter(true);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
camera = new Camera();
this.centerX = width / 2;
this.centerY = height / 2;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
// Angle around the y-axis of the rotation at the given time. It is
// calculated both in radians and in the equivalent degrees.
final double radians = Math.PI * interpolatedTime;
float degrees = (float) (180.0 * radians / Math.PI);
if(mIsRotationReversed){
degrees = -degrees;
}
// Once we reach the midpoint in the animation, we need to hide the
// source view and show the destination view. We also need to change
// the angle by 180 degrees so that the destination does not come in
// flipped around. This is the main problem with SDK sample, it does not
// do this.
if (interpolatedTime >= 0.5f) {
if(mIsRotationReversed){ degrees += 180.f; } else{ degrees -= 180.f; }
if (!visibilitySwapped) {
setImageDrawable(toDrawable);
visibilitySwapped = true;
}
}
final Matrix matrix = t.getMatrix();
camera.save();
camera.translate(0.0f, 0.0f, (float) (150.0 * Math.sin(radians)));
camera.rotateX(mIsRotationXEnabled ? degrees : 0);
camera.rotateY(mIsRotationYEnabled ? degrees : 0);
camera.rotateZ(mIsRotationZEnabled ? degrees : 0);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="default_civ_isAnimated">true</bool>
<bool name="default_civ_isFlipped">false</bool>
<integer name="default_civ_duration">500</integer>
<!--Rotation Y-->
<integer name="default_civ_rotations">2</integer>
<!--Default rotation is CCW-->
<bool name="default_civ_isRotationReversed">false</bool>
</resources>
icon.setOnFlipListener(new CircularFlipImageView.OnFlipListener() {
@Override
public void onClick(CircularFlipImageView view) {
}
@Override
public void onFlipStart(CircularFlipImageView view) {
}
@Override
public void onFlipEnd(CircularFlipImageView view) {
}
});
<com.your.package.CircularFlipImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:layout_gravity="center_vertical"
android:layout_marginEnd="6dip"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="6dip"
android:src="YOUR_SRC_DRAWABLE"
app:border_color="YOUR_COLOR"
app:border_width="WIDTH_IN_DP"
app:shadow="true|false"
app:flipDrawable="YOUR_FLIPPED_DRAWABLE"
app:flipDuration="DURATION_IN_MS"
app:flipInterpolator="@android:anim/SOME_INTERPOLATOR"
app:flipRotations="none|x|y|z"
app:isAnimated="true|false"
app:isFlipped="true|false"/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment