/*
*
* * Copyright 2015. Appsi Mobile
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package com.appsimobile.paintjob;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.support.annotation.IdRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.graphics.Palette;
import android.util.SparseArray;
import android.view.View;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* Created by nick on 27/04/15.
*/
public class PaintJob {
public static final int SWATCH_VIBRANT = 0;
public static final int SWATCH_DARK_VIBRANT = 1;
public static final int SWATCH_LIGHT_VIBRANT = 2;
public static final int SWATCH_MUTED = 3;
public static final int SWATCH_DARK_MUTED = 4;
public static final int SWATCH_LIGHT_MUTED = 5;
private static ArgbEvaluator sArgbEvaluator;
final View mRootView;
final List<ViewPainter> mViewPainters;
final BitmapCallback mBitmapCallback;
final CountDownLatch mCountDownLatch;
final SparseArray<View> mViews = new SparseArray<>();
private final BitmapSource mBitmapSource;
FallBackColors mFallBackColors;
// volatile to ensure reference is visible after countdown.
volatile Bitmap mBitmap;
boolean mCancelled;
boolean mExecuted;
AsyncTask<BitmapSource, ?, ?> mLoadTask;
AsyncTask<?, ?, ?> mPaletteTask;
int mDuration;
Palette mPalette;
PaintJob(
@NonNull View rootView,
@NonNull BitmapSource bitmapSource,
@Nullable FallBackColors fallBackColors,
@NonNull List<ViewPainter> viewPainters,
@Nullable BitmapCallback bitmapCallback) {
mRootView = rootView;
mFallBackColors = fallBackColors;
mBitmapSource = bitmapSource;
mViewPainters = viewPainters;
mBitmapCallback = bitmapCallback;
// provide a simple future by implementing call using a countdown-latch.
mCountDownLatch = new CountDownLatch(1);
}
public static Builder newBuilder(View view, BitmapSource bitmapSource) {
return new ViewBuilderImpl(view, bitmapSource);
}
public static Builder newBuilder(View view, Bitmap bitmap) {
return new ViewBuilderImpl(view, new PlainBitmapSource(bitmap));
}
public void execute(int duration) {
if (mExecuted) throw new IllegalStateException("PaintJob can only execute once");
mDuration = duration;
if (mBitmapSource instanceof PlainBitmapSource) {
Bitmap bitmap = ((PlainBitmapSource) mBitmapSource).mBitmap;
onBitmapLoaded(bitmap, true /* immediate */);
} else {
loadBitmap(mBitmapSource);
}
}
public DerivedBuilder derive(View view) {
BitmapSource source = new BitmapSource() {
@Override
public Bitmap loadBitmapAsync() {
try {
mCountDownLatch.await();
} catch (InterruptedException e) {
return null;
}
return mBitmap;
}
};
return new ViewBuilderImpl(view, source);
}
private void loadBitmap(final BitmapSource bitmapSource) {
mLoadTask = new AsyncTask<BitmapSource, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(BitmapSource... params) {
BitmapSource bitmapSource = params[0];
return bitmapSource.loadBitmapAsync();
}
@Override
protected void onPostExecute(Bitmap bitmap) {
onBitmapLoaded(bitmap, false /* immediate */);
}
};
mLoadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
new BitmapSource[]{bitmapSource});
}
void onBitmapLoaded(Bitmap bitmap, final boolean immediate) {
mBitmap = bitmap;
mLoadTask = null;
mCountDownLatch.countDown();
if (mBitmap == null) {
onPaletteGenerated(null, false);
} else {
mPaletteTask = Palette.from(bitmap).generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
onPaletteGenerated(palette, immediate);
}
});
}
if (mBitmapCallback != null) {
mBitmapCallback.onBitmapLoaded(bitmap, immediate);
}
}
void onPaletteGenerated(@Nullable Palette palette, boolean immediate) {
// when we are cancelled, don't do anything
if (mCancelled) return;
mPalette = palette;
applyPalette(palette, immediate);
}
private void applyPalette(@Nullable Palette palette, boolean immediate) {
int N = mViewPainters.size();
for (int i = 0; i < N; i++) {
ViewPainter viewPainter = mViewPainters.get(i);
viewPainter.apply(palette, mRootView, this, immediate, mDuration);
}
}
public void cancel() {
mCancelled = true;
if (mLoadTask != null) {
mLoadTask.cancel(true);
mLoadTask = null;
}
}
FallBackColors getFallbackColors() {
if (mFallBackColors == null) {
mFallBackColors = FallBackColors.fromContext(mRootView.getContext());
}
return mFallBackColors;
}
View findViewById(@IdRes int viewId) {
View result = mViews.get(viewId);
if (result == null) {
result = mRootView.findViewById(viewId);
mViews.put(viewId, result);
}
return result;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({SWATCH_VIBRANT, SWATCH_DARK_VIBRANT, SWATCH_LIGHT_VIBRANT,
SWATCH_MUTED, SWATCH_DARK_MUTED, SWATCH_LIGHT_MUTED})
public @interface Swatch {
}
public interface ViewPainter {
void apply(@Nullable Palette palette, View rootView, PaintJob paintJob, boolean immediate,
int duration);
void setSwatch(@Swatch int swatch);
boolean canAnimate();
}
public interface DerivedBuilder {
Builder setFallBackColors(FallBackColors fallBackColors);
Builder paintWithSwatch(@Swatch int swatch, ViewPainter... viewPainters);
PaintJob build();
}
public interface Builder extends DerivedBuilder {
Builder setBitmapCallback(BitmapCallback bitmapCallback);
}
public interface BitmapSource {
Bitmap loadBitmapAsync();
}
public interface BitmapCallback {
void onBitmapLoaded(Bitmap bitmap, boolean immediate);
}
static class RgbViewPainter extends BaseViewPainter {
RgbViewPainter(int alpha, int[] viewIds) {
super(alpha, viewIds);
}
@Override
protected int getTargetColorFromSwatch(Palette.Swatch swatch) {
return swatch.getRgb();
}
@Override
protected int getCurrentColorFromView(View view) {
Drawable drawable = view.getBackground();
if (drawable == null) return Color.TRANSPARENT;
if (drawable instanceof ColorDrawable) {
return ((ColorDrawable) drawable).getColor();
}
return Color.TRANSPARENT;
}
@Override
protected void applyColorToView(View view, int color) {
view.setBackgroundColor(color);
}
}
static class TitleViewPainter extends BaseViewPainter {
TitleViewPainter(int alpha, int[] viewIds) {
super(alpha, viewIds);
}
@Override
protected int getCurrentColorFromView(View view) {
return ((TextView) view).getCurrentTextColor();
}
@Override
protected int getTargetColorFromSwatch(Palette.Swatch swatch) {
return swatch.getTitleTextColor();
}
@Override
protected void applyColorToView(View view, int color) {
((TextView) view).setTextColor(color);
}
}
static class BodyTextViewPainter extends TitleViewPainter {
BodyTextViewPainter(int alpha, int[] viewIds) {
super(alpha, viewIds);
}
@Override
protected int getTargetColorFromSwatch(Palette.Swatch swatch) {
return swatch.getBodyTextColor();
}
}
public static abstract class BaseViewPainter implements ViewPainter {
final int[] mViewIds;
final int mAlpha;
@Swatch
int mSwatch;
protected BaseViewPainter(int alpha, int... viewIds) {
mAlpha = alpha;
mViewIds = viewIds;
}
@Override
public void apply(@Nullable Palette palette, View rootView, PaintJob paintJob,
boolean immediate,
int duration) {
if (mViewIds == null) return;
if (mViewIds.length == 0) return;
Palette.Swatch swatch = getSwatch(mSwatch, palette);
if (swatch == null) {
FallBackColors fallBackColors = paintJob.getFallbackColors();
swatch = getFallbackSwatch(mSwatch, fallBackColors);
}
int N = mViewIds.length;
for (int i = 0; i < N; i++) {
int viewId = mViewIds[i];
View view = paintJob.findViewById(viewId);
animateViewToColor(view, swatch, immediate, duration);
}
}
@Override
public void setSwatch(int swatch) {
mSwatch = swatch;
}
@Override
public boolean canAnimate() {
return true;
}
private static Palette.Swatch getSwatch(@Swatch int swatch, @Nullable Palette palette) {
if (palette == null) return null;
switch (swatch) {
case SWATCH_MUTED:
return palette.getMutedSwatch();
case SWATCH_DARK_MUTED:
return palette.getDarkMutedSwatch();
case SWATCH_LIGHT_MUTED:
return palette.getLightMutedSwatch();
default:
case SWATCH_VIBRANT:
return palette.getVibrantSwatch();
case SWATCH_DARK_VIBRANT:
return palette.getDarkVibrantSwatch();
case SWATCH_LIGHT_VIBRANT:
return palette.getLightVibrantSwatch();
}
}
private static Palette.Swatch getFallbackSwatch(@Swatch int swatch,
FallBackColors fallbacks) {
switch (swatch) {
case SWATCH_MUTED:
return fallbacks.getMutedSwatch();
case SWATCH_DARK_MUTED:
return fallbacks.getDarkMutedSwatch();
case SWATCH_LIGHT_MUTED:
return fallbacks.getLightMutedSwatch();
default:
case SWATCH_VIBRANT:
return fallbacks.getVibrantSwatch();
case SWATCH_DARK_VIBRANT:
return fallbacks.getDarkVibrantSwatch();
case SWATCH_LIGHT_VIBRANT:
return fallbacks.getLightVibrantSwatch();
}
}
private void animateViewToColor(final View view, Palette.Swatch swatch, boolean immediate,
int duration) {
int targetColor = getTargetColorFromSwatch(swatch);
targetColor = ColorUtils.setAlphaComponent(targetColor, mAlpha);
if (!canAnimate() || immediate || duration <= 0) {
if (!canAnimate() && !applyColorsToView(view, swatch)) {
applyColorToView(view, targetColor);
}
} else {
final int currentColor = getCurrentColorFromView(view);
ValueAnimator colorAnimation = new ValueAnimator();
if (sArgbEvaluator == null) {
sArgbEvaluator = new ArgbEvaluator();
}
colorAnimation.setIntValues(currentColor, targetColor);
final int finalTargetColor = targetColor;
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int color = evaluate(animation.getAnimatedFraction(), currentColor,
finalTargetColor);
applyColorToView(view, color);
}
});
colorAnimation.setDuration(duration);
colorAnimation.start();
}
}
protected abstract int getTargetColorFromSwatch(Palette.Swatch swatch);
protected boolean applyColorsToView(View view, Palette.Swatch swatch) {
return false;
}
protected abstract void applyColorToView(View view, int color);
protected abstract int getCurrentColorFromView(View view);
static int evaluate(float fraction, int startInt, int endInt) {
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int) ((startA + (int) (fraction * (endA - startA))) << 24) |
(int) ((startR + (int) (fraction * (endR - startR))) << 16) |
(int) ((startG + (int) (fraction * (endG - startG))) << 8) |
(int) ((startB + (int) (fraction * (endB - startB))));
}
}
static class ViewBuilderImpl implements Builder {
final View mView;
final List<ViewPainter> mViewPainters = new ArrayList<>();
boolean mBuilt;
FallBackColors mFallBackColors;
BitmapSource mBitmapSource;
BitmapCallback mBitmapCallback;
public ViewBuilderImpl(View view, BitmapSource bitmapSource) {
mView = view;
mBitmapSource = bitmapSource;
}
@Override
public Builder setFallBackColors(FallBackColors fallBackColors) {
mFallBackColors = fallBackColors;
return this;
}
@Override
public Builder paintWithSwatch(@Swatch int swatch, ViewPainter... viewPainters) {
if (viewPainters != null) {
int N = viewPainters.length;
for (int i = 0; i < N; i++) {
ViewPainter viewPainter = viewPainters[i];
viewPainter.setSwatch(swatch);
mViewPainters.add(viewPainter);
}
}
return this;
}
@Override
public PaintJob build() {
if (mBuilt) throw new IllegalStateException("Can only call build once");
mBuilt = true;
return new PaintJob(mView, mBitmapSource, mFallBackColors, mViewPainters,
mBitmapCallback);
}
@Override
public Builder setBitmapCallback(BitmapCallback bitmapCallback) {
mBitmapCallback = bitmapCallback;
return this;
}
}
static class PlainBitmapSource implements BitmapSource {
final Bitmap mBitmap;
PlainBitmapSource(Bitmap bitmap) {
mBitmap = bitmap;
}
@Override
public Bitmap loadBitmapAsync() {
return mBitmap;
}
}
public static class FallBackColors {
final int mVibrantFallbackColor;
final int mMutedFallbackColor;
Palette.Swatch mVibrantSwatch;
Palette.Swatch mDarkVibrantSwatch;
Palette.Swatch mLightVibrantSwatch;
Palette.Swatch mMutedSwatch;
Palette.Swatch mDarkMutedSwatch;
Palette.Swatch mLightMutedSwatch;
public FallBackColors(int vibrantFallbackColor, int mutedFallbackColor) {
mVibrantFallbackColor = vibrantFallbackColor;
mMutedFallbackColor = mutedFallbackColor;
}
public static FallBackColors fromContext(Context context) {
TypedArray a = context.obtainStyledAttributes(
new int[]{R.attr.colorPrimary, R.attr.colorAccent});
int vibrantFallback = a.getColor(1, 0 /* TODO: define smart default */);
int mutedFallback = a.getColor(0, 0 /* TODO: define smart default */);
a.recycle();
return new FallBackColors(vibrantFallback, mutedFallback);
}
public Palette.Swatch getVibrantSwatch() {
if (mVibrantSwatch == null) {
mVibrantSwatch = new Palette.Swatch(mVibrantFallbackColor, 0);
}
return mVibrantSwatch;
}
public Palette.Swatch getDarkVibrantSwatch() {
if (mDarkVibrantSwatch == null) {
int color = ColorUtils.compositeColors(0x20000000, mVibrantFallbackColor);
mDarkVibrantSwatch = new Palette.Swatch(color, 0);
}
return mDarkVibrantSwatch;
}
public Palette.Swatch getLightVibrantSwatch() {
if (mLightVibrantSwatch == null) {
int color = ColorUtils.compositeColors(0x20FFFFFF, mVibrantFallbackColor);
mLightVibrantSwatch = new Palette.Swatch(color, 0);
}
return mLightVibrantSwatch;
}
public Palette.Swatch getDarkMutedSwatch() {
if (mDarkMutedSwatch == null) {
int color = ColorUtils.compositeColors(0x20000000, mMutedFallbackColor);
mDarkMutedSwatch = new Palette.Swatch(color, 0);
}
return mDarkMutedSwatch;
}
public Palette.Swatch getLightMutedSwatch() {
if (mLightMutedSwatch == null) {
int color = ColorUtils.compositeColors(0x20FFFFFF, mMutedFallbackColor);
mLightMutedSwatch = new Palette.Swatch(color, 0);
}
return mLightMutedSwatch;
}
public Palette.Swatch getMutedSwatch() {
if (mMutedSwatch == null) {
mMutedSwatch = new Palette.Swatch(mMutedFallbackColor, 0);
}
return mMutedSwatch;
}
}
}