/**
* Wire
* Copyright (C) 2016 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.waz.zclient.views.images;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.waz.api.BitmapCallback;
import com.waz.api.ImageAsset;
import com.waz.api.LoadHandle;
import com.waz.api.UpdateListener;
import com.waz.zclient.R;
import com.waz.zclient.ui.animation.interpolators.penner.Expo;
import com.waz.zclient.ui.animation.interpolators.penner.Quart;
import com.waz.zclient.utils.ViewUtils;
public class ImageAssetImageView extends FrameLayout implements UpdateListener {
public static final String TAG = ImageAssetImageView.class.getName();
// swap position of the an image view
private static final int SWAP_POSITION_ONE = 0;
private static final int SWAP_POSITION_TWO = 1;
private static final int SWAP_POSITION_NONE = 2;
// default values
private static final DisplayType DEFAULT_DISPLAY_TYPE = DisplayType.REGULAR;
private static final TransitionType DEFAULT_TRANSITION_TYPE = TransitionType.FADE_EXPO_IN_OUT;
private static final int DEFAULT_TRANSITION_DURATION = 350;
private static final int DEFAULT_COLOR = Color.BLACK;
private static final int DEFAULT_BORDER_WIDTH = 0;
private static final float DEFAULT_FROM_SCALE = 0.88f;
/**
* The image asset this view works upon.
*/
private ImageAsset imageAsset;
/**
* The desired transition type when a new IamgeAsset is connected
*/
private TransitionType transitionType;
/**
* The default transition type when a new image asset is connected
*/
private TransitionType defaultTransitionType;
/**
* Blur radius for blurred images
*/
private float blurRadius;
/**
* The display type refers to the getBitmap function in the image asset
*/
private DisplayType displayType;
/**
* The handle to bitmap loading processes.
* A started request can be canceled via the handle.
*/
private LoadHandle loadHandle;
/**
* The duration of a crossfade transition;
*/
private int crossfadeDuration;
/**
* Bearer of the bitmap. Swap Position 1.
*/
private ImageView swapImageView1;
/**
* Bearer of the bitmap. Swap Position 2.
*/
private ImageView swapImageView2;
/**
* The current swap position changes when a new image asset is loaded.
*/
private int swapPosition;
/**
* This is the saturation value animatable.
*/
private float saturation;
// border of round bitmap
private int borderColor;
private int borderWidth;
private float fromScale;
/**
* The transition type when a new image asset is loaded
*/
public enum TransitionType {
NONE,
FADE,
FADE_EXPO_IN_OUT,
SCALE
}
/**
* The display type
*/
public enum DisplayType {
REGULAR,
CIRCLE
}
public enum BitmapError {
IMAGE_ASSET_IS_NULL,
IMAGE_ASSET_IS_EMPTY,
BITMAP_LOADING_FAILED
}
/**
* Default CTOR - no attributes are conveyed
*
* @param context Android's context
*/
public ImageAssetImageView(Context context) {
this(context, null);
}
/**
* CTOR - attributes are given but no style
*
* @param context Android's context
* @param attrs A set of attributes specified in XML
*/
public ImageAssetImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* CTOR - attributes and style are given
*
* @param context Android's context
* @param attrs A set of attributes specified in XML
* @param defStyleRes the style id
*/
public ImageAssetImageView(Context context,
AttributeSet attrs,
int defStyleRes) {
super(context, attrs, defStyleRes);
init();
}
private void init() {
displayType = DEFAULT_DISPLAY_TYPE;
defaultTransitionType = DEFAULT_TRANSITION_TYPE;
blurRadius = ViewUtils.toPx(getContext(), getResources().getInteger(R.integer.background__blur_radius));
crossfadeDuration = DEFAULT_TRANSITION_DURATION;
borderColor = DEFAULT_COLOR;
borderWidth = ViewUtils.toPx(getContext(), DEFAULT_BORDER_WIDTH);
fromScale = DEFAULT_FROM_SCALE;
// TODO set the passed attributes from the xml layout file.
swapImageView1 = new ImageView(getContext());
swapImageView1.setScaleType(ImageView.ScaleType.CENTER_CROP);
swapImageView2 = new ImageView(getContext());
swapImageView2.setScaleType(ImageView.ScaleType.CENTER_CROP);
addView(swapImageView1);
addView(swapImageView2);
}
public ImageView.ScaleType getScaleType() {
return swapImageView1.getScaleType();
}
/**
* Supports an animatable gray scaling with an object animator.
*
* @param saturation
*/
public void setSaturation(float saturation) {
this.saturation = saturation;
ColorMatrix matrix = new ColorMatrix();
matrix.setSaturation(saturation); //0 means grayscale
ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix);
swapImageView1.setColorFilter(cf);
swapImageView2.setColorFilter(cf);
}
/**
* Object Animator also demands a getter method
*
* @return
*/
public float getSaturation() {
return saturation;
}
/**
* Changes the display type and if the change is relevant and an image asset exists
* it updates the bitmap.
*
* @param displayType
*/
public void setDisplayType(DisplayType displayType) {
if (this.displayType != displayType) {
this.displayType = displayType;
updated();
}
}
/**
* connects a new image asset by using the default transition type
*
* @param imageAsset the image asset to be connected
*/
public void connectImageAsset(ImageAsset imageAsset) {
connectImageAsset(imageAsset, defaultTransitionType);
}
/**
* Connects an image asset taking into account if an old image asset exists.
*
* @param imageAsset the new image asset
* @param transitionType if an old image asset exist the transition that should be used
*/
public void connectImageAsset(ImageAsset imageAsset, TransitionType transitionType) {
connectImageAsset(imageAsset, transitionType, false);
}
public void connectImageAsset(ImageAsset imageAsset, TransitionType transitionType, boolean forceBlur) {
// new image asset is null
if (imageAsset == null) {
// disconnect former image asset and set image drawable to null
disconnectImageAsset();
updated();
return;
}
// old image asset exists
if (this.imageAsset != null) {
if (this.imageAsset.getId().equals(imageAsset.getId())) {
updated();
return;
} else {
// otherwise disconnect
disconnectImageAsset();
}
}
// connect image asset
this.imageAsset = imageAsset;
this.imageAsset.addUpdateListener(this);
this.transitionType = transitionType;
updated();
}
/**
* Disconnect image asset currently connected to this view.
*/
private void disconnectImageAsset() {
if (imageAsset != null) {
imageAsset.removeUpdateListener(this);
imageAsset = null;
}
if (loadHandle != null) {
loadHandle.cancel();
loadHandle = null;
}
}
/**
* Creates a drawable form a raw bitmap and sets it as
* as the source of this image view.
*
* @param bitmap the source of
*/
private void setBitmapWithTransition(Bitmap bitmap) {
Drawable newDrawable = new BitmapDrawable(getContext().getResources(), bitmap);
// no transition demanded - put image into visible view
if (transitionType == TransitionType.NONE) {
switch (swapPosition) {
case SWAP_POSITION_ONE:
swapImageView1.setImageDrawable(newDrawable);
break;
case SWAP_POSITION_TWO:
swapImageView2.setImageDrawable(newDrawable);
break;
case SWAP_POSITION_NONE:
swapPosition = SWAP_POSITION_ONE;
swapImageView1.setImageDrawable(newDrawable);
swapImageView1.animate().alpha(1).setDuration(crossfadeDuration);
break;
}
return;
}
ImageView imageViewIn;
ImageView imageViewOut;
switch (swapPosition) {
case SWAP_POSITION_NONE:
case SWAP_POSITION_ONE:
imageViewIn = swapImageView2;
imageViewOut = swapImageView1;
swapPosition = SWAP_POSITION_TWO;
break;
case SWAP_POSITION_TWO:
imageViewIn = swapImageView1;
imageViewOut = swapImageView2;
swapPosition = SWAP_POSITION_ONE;
break;
default:
return;
}
imageViewIn.setImageDrawable(newDrawable);
// the transition always works with the same views: in and out
// If you need a custom transition, define one in TransitionType
// and tell the views here how to behave during the transition.
switch (transitionType) {
case SCALE:
imageViewIn.setScaleX(fromScale);
imageViewIn.setScaleY(fromScale);
imageViewIn.setAlpha(1.0f);
imageViewIn.animate()
.scaleX(1.0f)
.scaleY(1.0f)
.setInterpolator(new Quart.EaseOut())
.setDuration(crossfadeDuration);
imageViewOut.animate()
.alpha(0)
.setDuration(crossfadeDuration);
break;
case FADE:
imageViewIn.animate()
.alpha(1)
.setDuration(crossfadeDuration);
imageViewOut.animate()
.alpha(0)
.setDuration(crossfadeDuration);
break;
case FADE_EXPO_IN_OUT:
imageViewIn.animate()
.alpha(1)
.setInterpolator(new Expo.EaseInOut())
.setDuration(550)
.setDuration(550);
imageViewOut.animate()
.alpha(0)
.setInterpolator(new Expo.EaseInOut())
.setDuration(550);
break;
}
}
/**
* Image asset callback. When this image asset gets updated this will be called.
*/
@Override
public void updated() {
if (loadHandle != null) {
loadHandle.cancel();
}
// check if image asset is null
if (imageAsset == null) {
return;
}
// check if image asset is empty
if (imageAsset.isEmpty()) {
return;
}
final int measuredWidth = getMeasuredWidth();
if (measuredWidth == 0) {
return;
}
if (displayType == null) {
return;
}
switch (displayType) {
case REGULAR:
loadHandle = imageAsset.getBitmap(measuredWidth, callback);
break;
case CIRCLE:
loadHandle = imageAsset.getRoundBitmap(measuredWidth, borderWidth, borderColor, callback);
break;
}
}
private BitmapCallback callback = new BitmapCallback() {
@Override
public void onBitmapLoaded(Bitmap bitmap) {
setBitmapWithTransition(bitmap);
// once the bitmap loaded any further updates on this image assets
// comes with no animation.
transitionType = TransitionType.NONE;
}
};
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
updated();
}
/**
* Removes current bitmaps from background.
*/
public void resetBackground() {
swapPosition = SWAP_POSITION_NONE;
if (swapImageView1 == null ||
swapImageView2 == null) {
return;
}
swapImageView1.animate()
.alpha(0)
.setDuration(crossfadeDuration)
.withEndAction(new Runnable() {
@Override
public void run() {
if (swapImageView1 != null) {
swapImageView1.setImageDrawable(null);
}
}
});
swapImageView2.animate()
.alpha(0)
.setDuration(crossfadeDuration)
.withEndAction(new Runnable() {
@Override
public void run() {
if (swapImageView2 != null) {
swapImageView2.setImageDrawable(null);
}
}
});
}
public void reset() {
swapPosition = SWAP_POSITION_NONE;
if (swapImageView1 == null ||
swapImageView2 == null) {
return;
}
swapImageView1.setImageDrawable(null);
swapImageView2.setImageDrawable(null);
}
}