Last active
September 7, 2016 03:47
-
-
Save ozodrukh/dfc0d307872fcf7082a9939183e5f62b to your computer and use it in GitHub Desktop.
ImageView with extended functionality
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
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