Skip to content

Instantly share code, notes, and snippets.

@ozodrukh
Last active September 7, 2016 03:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ozodrukh/dfc0d307872fcf7082a9939183e5f62b to your computer and use it in GitHub Desktop.
Save ozodrukh/dfc0d307872fcf7082a9939183e5f62b to your computer and use it in GitHub Desktop.
ImageView with extended functionality
package uz.lamuz.player.ui.widget.cover;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Target;
import com.squareup.picasso.Transformation;
import java.lang.ref.WeakReference;
import java.util.Locale;
import timber.log.Timber;
import uz.lamuz.player.BuildConfig;
import uz.lamuz.player.R;
import uz.lamuz.player.ui.main.artists.utils.CircleTransformation;
import uz.lamuz.player.util.DeviceUtil;
import uz.lamuz.player.util.FastBlur;
import uz.lamuz.player.util.Utils;
import static uz.lamuz.player.util.Utils.getViewName;
/**
* @author Ozodrukh
* @version 3.0
*
* 4th, September 2016
*/
public class CoverImageView extends ImageView {
private final static CircleTransformation TO_CIRCLE = new CircleTransformation();
private static final int BLUR_SCALE_FACTOR = 8;
private static final int BLUR_IMAGE_WIDTH = 96;
private static final int BLUR_IMAGE_HEIGHT = 96;
/**
* Theme constants, changes placeholder image depending on theme
* {@link #getThemedPlaceholderId()}
*/
public enum Theme {
DARK, LIGHT
}
/**
* Size constants, aids to modify image url to get device screen
* related image size {@link #getScaleSize()}
*/
public enum Size {
TINY(36), SMALL(64), MEDIUM(180), LARGE(360);
final int dpValue;
Size(int dpValue) {
this.dpValue = dpValue;
}
}
protected Drawable placeholderDrawable;
private int px = 0, py = 0;
private boolean isPlaceholderVisible;
protected Theme theme;
private Size size = Size.MEDIUM;
/** Flag indicates transform bitmap to circle or not */
private boolean transformToCircle = false;
private boolean debugLoadingProcess;
/** View blur controller */
private BlurTarget blurTarget;
/** Debugging target, logs every state of Bitmap loading */
private Target deferred;
/**
* Pending request that awaits View laying out on the screen (to get image size)
* and sends it immediately when size is established
*/
private RequestCreator pendingRequest;
public CoverImageView(Context context) {
super(context);
parseAttrs(null, 0, 0);
}
public CoverImageView(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttrs(attrs, 0, 0);
}
public CoverImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
parseAttrs(attrs, defStyleAttr, 0);
}
private void parseAttrs(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
if (attrs != null) {
TypedArray a =
getContext().obtainStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr,
defStyleRes);
try {
if (a.getBoolean(R.styleable.CoverImageView_cover_theme, false)) {
setTheme(Theme.LIGHT);
} else {
setTheme(Theme.DARK);
}
int sizeIndex = a.getInt(R.styleable.CoverImageView_cover_size, Size.MEDIUM.ordinal());
setSize(Size.values()[sizeIndex]);
} finally {
a.recycle();
}
} else {
setTheme(Theme.DARK);
}
if (isInEditMode()) {
setImageDrawable(placeholderDrawable);
}
}
private void updatePlaceholder() {
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
placeholderDrawable = VectorDrawableCompat.create(getResources(), getThemedPlaceholderId(),
getContext().getTheme());
} else {
placeholderDrawable =
getResources().getDrawable(getThemedPlaceholderId(), getContext().getTheme());
}
}
private void calculatePaddings() {
float size = Math.min(getWidth(), getHeight()) * 0.666f;
px = Math.round((getWidth() - size) / 2f);
py = Math.round((getHeight() - size) / 2f);
}
public final void setTheme(Theme theme) {
if (this.theme != theme) {
this.theme = theme;
updatePlaceholder();
}
}
public void setSize(Size size) {
this.size = size;
}
/**
* Indicate whether transform bitmap to circle or not
*
* @param apply True to apply circle transformation on bitmaps
*/
public void applyCircleTransformation(boolean apply) {
transformToCircle = apply;
}
public boolean isTransformingToCircle() {
return transformToCircle;
}
private void updatePaddings() {
if (isPlaceholderVisible && size != Size.TINY) {
setPadding(px, py, px, py);
} else {
setPadding(0, 0, 0, 0);
}
}
@Override
public void setImageDrawable(Drawable drawable) {
if (debugLoadingProcess) {
Timber.d("Drawable changed to " + drawable);
}
isPlaceholderVisible = drawable == placeholderDrawable;
updatePaddings();
if (!isPlaceholderVisible && drawable instanceof BitmapDrawable) {
//TODO implement material animation!
}
super.setImageDrawable(drawable);
invalidate();
}
/**
* On enabled logs image loading process every stage step by step
*
* @param loading Enable logging of image loading process
*/
public void setDebugImageLoading(boolean loading) {
this.debugLoadingProcess = loading;
}
public void setImageUrl(Picasso picasso, String url) {
setImageUrl(picasso, url, null);
}
public void setImageUrl(Picasso picasso, String url, String groupBy) {
// 1. Reset was moved to set only when awaits View layout
// 2. transform original url
final String finalImageUrl = transformUrl(url);
// 3. create image request
pendingRequest = picasso.load(finalImageUrl);
// 4. if blur requested
if (blurTarget != null) {
// create blur request, we need original bitmap to blur
blurTarget.onParentRequestedImage(picasso, url);
}
// 5. transform original request
transformRequest(pendingRequest, groupBy);
// if request were sent on transformation, we skip our logic
if (pendingRequest == null) {
if (debugLoadingProcess) {
Timber.w("Request were sent from transformation");
}
return;
}
if (debugLoadingProcess) {
Timber.d("View{laidOut=%b, w=%d, h=%d}", ViewCompat.isLaidOut(this), getWidth(), getHeight());
}
// 6. send if view is laid out, otherwise await first
if (ViewCompat.isLaidOut(this)) {
if (debugLoadingProcess) {
Timber.d("View were laid out, sending request");
}
pendingRequest.resize(getWidth(), getHeight()).into(this);
pendingRequest = null;
} else {
// Await view measuring & set placeholder
setImageDrawable(placeholderDrawable);
}
}
/**
* Used to debugging of image loading process
*
* @return Deferred target
*/
protected Target getDeferredTarget() {
if (deferred == null) {
deferred = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
if (debugLoadingProcess) {
Timber.d("Deferred bitmap loaded Bitmap(%d, %d) from ", bitmap.getWidth(),
bitmap.getHeight(), from.name());
}
setImageDrawable(new ColorDrawable(Color.WHITE));
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
if (debugLoadingProcess) {
Timber.e("Deferred bitmap failed to load");
}
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
if (debugLoadingProcess) {
Timber.d("Deferred placeholder");
}
}
};
}
return deferred;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (pendingRequest != null) {
if (debugLoadingProcess) {
Timber.w("Picasso awaited view resize(%d, %d)", getWidth(), getHeight());
}
pendingRequest.resize(getWidth(), getHeight());
if (debugLoadingProcess) {
pendingRequest.into(getDeferredTarget());
} else {
pendingRequest.into(this);
}
pendingRequest = null;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
calculatePaddings();
updatePaddings();
}
/**
* If you gonna make fully custom request, set it from
* {@link #transformRequest(RequestCreator, String)}, or if you have custom
* sending logic, null main request, and send it by self
*/
protected final void setPendingRequest(RequestCreator pendingRequest) {
this.pendingRequest = pendingRequest;
}
/**
* Transform request for your needs
*
* [+] used in ArtistsCoverImageView to transform bitmaps to circle bitmaps
*
* @param pendingRequest Picasso request builder
* @param groupBy Request tag
*/
protected void transformRequest(RequestCreator pendingRequest, String groupBy) {
pendingRequest.placeholder(placeholderDrawable).error(placeholderDrawable).noFade();
if (!TextUtils.isEmpty(groupBy)) {
pendingRequest.tag(groupBy);
}
if (transformToCircle) {
pendingRequest.transform(TO_CIRCLE);
}
}
/**
* Transform original url to desired, current implementation
* adds width prefix with scaled size
*
* @param url Original url
* @return Transformed url
*/
public String transformUrl(String url) {
return url.concat("&w=").concat(String.valueOf(getScaleSize()));
}
/**
* @return Resource id to the drawable depending on current theme
*/
public int getThemedPlaceholderId() {
switch (theme) {
case LIGHT:
return R.drawable.ic_logo_dark_64dp_v2;
default:
return R.drawable.ic_logo_light_64dp_v2;
}
}
/**
* @return Device independent pixel value of image size
*/
private int getScaleSize() {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size.dpValue,
getResources().getDisplayMetrics());
}
public final void setBlurredBackgroundOn(View target) {
if (debugLoadingProcess) {
Timber.w("%s requires Background Blur", getViewName(target));
}
blurTarget = new BlurTarget(target);
}
private static class BlurTarget implements Target, ViewTreeObserver.OnGlobalLayoutListener {
private final static Drawable TRANSPARENT = new ColorDrawable(Color.TRANSPARENT);
private final WeakReference<View> target;
private BlurBackgroundTransformation transformer;
private RequestCreator requestCreator;
BlurTarget(View target) {
if (ViewCompat.isLaidOut(target)) {
initBlurTransformation();
} else {
target.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
this.target = new WeakReference<>(target);
}
/**
* Initializes transformer for View
*/
private void initBlurTransformation() {
final View target = this.target.get();
Timber.d("%s laid out {w=%d, h=%d}", Utils.getViewName(target), target.getWidth(),
target.getHeight());
transformer = new BlurBackgroundTransformation(Utils.getViewName(target), target.getWidth(),
target.getHeight());
}
/**
* Called when {@link CoverImageView#setImageUrl(Picasso, String, String)} were invoked
* triggers us to load blur original image
*/
void onParentRequestedImage(Picasso picasso, String imageUrl) {
requestCreator = picasso.load(imageUrl);
// View is laid out
if (ViewCompat.isLaidOut(target.get())) {
onViewLaidOut();
}
}
/**
* When target view (that requested blur) were laid out, so we
* can start loading image
*/
private void onViewLaidOut() {
if (requestCreator == null) {
return;
}
// set transformer
requestCreator.resize(transformer.viewWidth, transformer.viewHeight)
.transform(transformer)
.into(this);
requestCreator = null;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
if (target.get() != null) {
final View target = this.target.get();
final Drawable blurDrawable = new BitmapDrawable(target.getResources(), bitmap);
final TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] {
target.getBackground() == null ? TRANSPARENT : target.getBackground(), blurDrawable
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
target.setBackground(transitionDrawable);
} else {
target.setBackgroundDrawable(transitionDrawable);
}
transitionDrawable.setCrossFadeEnabled(true);
transitionDrawable.startTransition(350);
}
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
@Override
public void onGlobalLayout() {
// Here we assert that reference cannot GCed
final View target = this.target.get();
// If view both width & height we can load image, otherwise it's pointless
if (target.getWidth() > 0 && target.getHeight() > 0) {
DeviceUtil.removeOnGlobalLayoutListener(target, this);
initBlurTransformation();
onViewLaidOut();
}
}
}
private static class BlurBackgroundTransformation implements Transformation {
private final String viewName;
/* View width */
private final int viewWidth;
private final int viewHeight;
/* Scale factor */
private final int scaleFactor;
/* Blur configurations */
private final float density;
private final int blurWidth;
private final int blurHeight;
BlurBackgroundTransformation(String viewName, int viewWidth, int viewHeight) {
this(viewName, viewWidth, viewHeight, 0, BLUR_SCALE_FACTOR, 0, 0);
}
BlurBackgroundTransformation(String viewName, int viewWidth, int viewHeight, int scaleFactor) {
this(viewName, viewWidth, viewHeight, 0, scaleFactor, 0, 0);
}
public BlurBackgroundTransformation(String viewName, int viewWidth, int viewHeight,
float density, int blurHeight, int blurWidth) {
this(viewName, viewWidth, viewHeight, density, 0, blurWidth, blurHeight);
}
private BlurBackgroundTransformation(String viewName, int viewWidth, int viewHeight,
float density, int scaleFactor, int blurWidth, int blurHeight) {
if (scaleFactor == 0 && (blurWidth == 0 || blurHeight == 0)) {
throw new IllegalArgumentException(
"Scale factor or blur image width height have to be initialized");
} else if (scaleFactor == 0 && density == 0) {
throw new IllegalArgumentException("On resizing blur image density is required");
}
this.viewName = viewName;
this.viewWidth = viewWidth;
this.viewHeight = viewHeight;
this.density = density;
this.scaleFactor = scaleFactor;
this.blurWidth = blurWidth;
this.blurHeight = blurHeight;
}
/**
* @return Calculates scale factor from desired width & height, important
* it will return factor for aspect ratio
*/
float resize(Bitmap source, float w, float h) {
w *= density;
h *= density;
final int sW = source.getWidth();
final int sH = source.getHeight();
return (Math.max(sW / w, sH / h));
}
@Override
public Bitmap transform(Bitmap source) {
final int scaleFactor;
if (this.scaleFactor == 0) {
scaleFactor = (int) resize(source, blurWidth, blurHeight);
} else {
scaleFactor = this.scaleFactor;
}
Timber.d("Blur image scale factor = " + scaleFactor);
final Bitmap blur = FastBlur.blur(source, viewWidth, viewHeight, scaleFactor, 20, .6f);
if (BuildConfig.DEBUG) {
if (blur != null) {
Timber.i("Blurring background of %s, bitmap{w=%d, h=%d}", viewName, blur.getWidth(),
blur.getHeight());
} else {
Timber.e("Blurring background of %s was failed", viewName);
}
}
source.recycle();
return blur;
}
@Override
public String key() {
return String.format(Locale.getDefault(), "blur(w=%d,h=%d)", viewWidth, viewHeight);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment