Added funky artwork to feature image when not present

Draws two rows of triangles, each coloured randomly according to
the dominant colour in the apps icon.

Given that the colour is probably assigned to the FeatureImage in response to
a network request finally downloading an image, there is a period of no feature
image. After the colour is provided, then if it is set instantly it tends to
look jerky. This eases in the colouring of the feature image.
This commit is contained in:
Peter Serwylo 2016-12-01 22:30:43 +11:00
parent d67f23b60c
commit b2d11091a7
3 changed files with 236 additions and 9 deletions

View File

@ -0,0 +1,228 @@
package org.fdroid.fdroid.views.apps;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import java.util.Random;
/**
* A feature image can have a {@link android.graphics.drawable.Drawable} or a {@link Palette}. If
* a Drawable is available, then it will draw that, otherwise it will attempt to fall back to the
* Palette you gave it. If a Palette is given, it will draw a series of triangles like so:
*
* +_----+----_+_----+----_+
* | \_ | _/ | \_ | _/ |
* | \_|_/ | \_|_/ |
* +_----+----_+_----+----_+
* | \_ | _/ | \_ | _/ |
* | \_|_/ | \_|_/ |
* +-----+-----+-----+-----+
*
* where each triangle is filled with one of two variations of the {@link Palette#getDominantColor(int)}
* that is chosen randomly. The randomness is first seeded with the colour that has been selected.
* This is so that if this repaints itself in the future, it will have the same unique pattern rather
* than picking a new random pattern each time.
*
* It is suggested that you obtain the Palette from the icon of an app.
*/
public class FeatureImage extends AppCompatImageView {
private static final int NUM_SQUARES_WIDE = 4;
private static final int NUM_SQUARES_HIGH = 2;
// Double, because there are two triangles per square.
private final Path[] triangles = new Path[NUM_SQUARES_HIGH * NUM_SQUARES_WIDE * 2];
@Nullable
private Paint[] trianglePaints;
private static final Paint WHITE_PAINT = new Paint();
static {
WHITE_PAINT.setColor(Color.WHITE);
WHITE_PAINT.setStyle(Paint.Style.FILL);
}
public FeatureImage(Context context) {
super(context);
}
public FeatureImage(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public FeatureImage(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* Takes the {@link Palette#getDominantColor(int)} from the palette, dims it substantially, and
* then creates a second variation that is slightly dimmer still. These two colours are then
* randomly allocated to each triangle which is expected to be rendered.
*/
public void setPalette(@Nullable Palette palette) {
if (palette == null) {
trianglePaints = null;
return;
}
// It is easier to dull al colour in the HSV space, so convert to that then adjust the
// saturation down and the colour value down.
float[] hsv = new float[3];
Color.colorToHSV(palette.getDominantColor(Color.LTGRAY), hsv);
hsv[1] *= 0.5f;
hsv[2] *= 0.7f;
int colourOne = Color.HSVToColor(hsv);
hsv[2] *= 0.9f;
int colourTwo = Color.HSVToColor(hsv);
Paint paintOne = new Paint();
paintOne.setColor(colourOne);
paintOne.setAntiAlias(true);
paintOne.setStrokeWidth(2);
paintOne.setStyle(Paint.Style.FILL_AND_STROKE);
Paint paintTwo = new Paint();
paintTwo.setColor(colourTwo);
paintTwo.setAntiAlias(true);
paintTwo.setStrokeWidth(2);
paintTwo.setStyle(Paint.Style.FILL_AND_STROKE);
// Seed based on the colour, so that each time we try to render a feature image with the
// same colour, it will give the same pattern.
Random random = new Random(colourOne);
trianglePaints = new Paint[triangles.length];
for (int i = 0; i < trianglePaints.length; i++) {
trianglePaints[i] = random.nextBoolean() ? paintOne : paintTwo;
}
animageColourChange();
}
private int currentAlpha = 255;
private ValueAnimator alphaAnimator = null;
@TargetApi(11)
private void animageColourChange() {
if (Build.VERSION.SDK_INT < 11) {
return;
}
if (alphaAnimator == null) {
alphaAnimator = ValueAnimator.ofInt(0, 255);
} else {
alphaAnimator.cancel();
}
alphaAnimator = ValueAnimator.ofInt(0, 255).setDuration(150);
alphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentAlpha = (int) animation.getAnimatedValue();
invalidate();
}
});
currentAlpha = 0;
invalidate();
alphaAnimator.start();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int triangleWidth = w / NUM_SQUARES_WIDE;
int triangleHeight = h / NUM_SQUARES_HIGH;
for (int x = 0; x < NUM_SQUARES_WIDE; x++) {
for (int y = 0; y < NUM_SQUARES_HIGH; y++) {
int startX = x * triangleWidth;
int startY = y * triangleHeight;
int endX = startX + triangleWidth;
int endY = startY + triangleHeight;
// Note that the order of these points need to go in a clockwise direction, or else
// the fill will not be applied properly.
Path firstTriangle;
Path secondTriangle;
// Alternate between two different ways to split a square into two triangles. This
// results in a nicer geometric pattern (see doc comments at top of class for more
// ASCII art of the expected outcome).
if (x % 2 == 0) {
// +_----+
// | \_ 1|
// |2 \_|
// +-----+
firstTriangle = createTriangle(new Point(startX, startY), new Point(endX, startY), new Point(endX, endY));
secondTriangle = createTriangle(new Point(startX, startY), new Point(endX, endY), new Point(startX, endY));
} else {
// +----_+
// |1 _/ |
// |_/ 2|
// +-----+
firstTriangle = createTriangle(new Point(startX, startY), new Point(endX, startY), new Point(startX, endY));
secondTriangle = createTriangle(new Point(startX, endY), new Point(endX, startY), new Point(endX, endY));
}
triangles[y * (NUM_SQUARES_WIDE * 2) + (x * 2)] = firstTriangle;
triangles[y * (NUM_SQUARES_WIDE * 2) + (x * 2) + 1] = secondTriangle;
}
}
}
/**
* First try to draw whatever image was given to this view. If that doesn't exist, try to draw
* a geometric pattern based on the palette that was given to us. If we haven't had a palette
* assigned to us (using {@link FeatureImage#setPalette(Palette)}) then clear the
* view by filling it with white.
*/
@Override
protected void onDraw(Canvas canvas) {
if (getDrawable() != null) {
super.onDraw(canvas);
} else if (trianglePaints != null) {
for (Paint paint : trianglePaints) {
paint.setAlpha(currentAlpha);
}
canvas.drawRect(0, 0, getWidth(), getHeight(), WHITE_PAINT);
for (int i = 0; i < triangles.length; i++) {
canvas.drawPath(triangles[i], trianglePaints[i]);
}
} else {
canvas.drawRect(0, 0, getWidth(), getHeight(), WHITE_PAINT);
}
}
/**
* This requires the three points to be in a sequence that traces out a triangle in clockwise
* fashion. This is required for the triangle to be filled correctly when drawing, otherwise
* it will end up black.
*/
private static Path createTriangle(Point start, Point middle, Point end) {
Path path = new Path();
path.setFillType(Path.FillType.EVEN_ODD);
path.moveTo(start.x, start.y);
path.lineTo(middle.x, middle.y);
path.lineTo(end.x, end.y);
path.close();
return path;
}
}

View File

@ -27,6 +27,7 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.FeatureImage;
import java.util.Date; import java.util.Date;
@ -49,13 +50,12 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL
private final TextView status; private final TextView status;
@Nullable @Nullable
private final ImageView featuredImage; private final FeatureImage featuredImage;
@Nullable @Nullable
private App currentApp; private App currentApp;
private final Activity activity; private final Activity activity;
private final int defaultFeaturedImageColour;
private final DisplayImageOptions displayImageOptions; private final DisplayImageOptions displayImageOptions;
private final Date recentCuttoffDate; private final Date recentCuttoffDate;
@ -70,10 +70,9 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL
icon = (ImageView) findViewAndEnsureNonNull(itemView, R.id.icon); icon = (ImageView) findViewAndEnsureNonNull(itemView, R.id.icon);
summary = (TextView) findViewAndEnsureNonNull(itemView, R.id.summary); summary = (TextView) findViewAndEnsureNonNull(itemView, R.id.summary);
featuredImage = (ImageView) itemView.findViewById(R.id.featured_image); featuredImage = (FeatureImage) itemView.findViewById(R.id.featured_image);
status = (TextView) itemView.findViewById(R.id.status); status = (TextView) itemView.findViewById(R.id.status);
defaultFeaturedImageColour = activity.getResources().getColor(R.color.cardview_light_background);
displayImageOptions = Utils.getImageLoadingOptions().build(); displayImageOptions = Utils.getImageLoadingOptions().build();
itemView.setOnClickListener(this); itemView.setOnClickListener(this);
@ -113,7 +112,8 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL
} }
if (featuredImage != null) { if (featuredImage != null) {
featuredImage.setBackgroundColor(defaultFeaturedImageColour); featuredImage.setPalette(null);
featuredImage.setImageDrawable(null);
} }
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions, this); ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions, this);
@ -151,12 +151,11 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL
@Override @Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
final ImageView image = featuredImage; if (featuredImage != null) {
if (image != null) {
new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() {
@Override @Override
public void onGenerated(Palette palette) { public void onGenerated(Palette palette) {
image.setBackgroundColor(palette.getDominantColor(defaultFeaturedImageColour)); featuredImage.setPalette(palette);
} }
}); });
} }

View File

@ -7,7 +7,7 @@
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:clipToPadding="false"> android:clipToPadding="false">
<ImageView <org.fdroid.fdroid.views.apps.FeatureImage
android:id="@+id/featured_image" android:id="@+id/featured_image"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="120dp" android:layout_height="120dp"