From b2d11091a7df3e89fabb445d0baeb583457b2c16 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 1 Dec 2016 22:30:43 +1100 Subject: [PATCH 01/28] 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. --- .../fdroid/views/apps/FeatureImage.java | 228 ++++++++++++++++++ .../views/categories/AppCardController.java | 15 +- app/src/main/res/layout/app_card_featured.xml | 2 +- 3 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java b/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java new file mode 100644 index 000000000..22ccae338 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/FeatureImage.java @@ -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; + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java index b625ec1bd..f9213c5ca 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java @@ -27,6 +27,7 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.views.apps.FeatureImage; import java.util.Date; @@ -49,13 +50,12 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL private final TextView status; @Nullable - private final ImageView featuredImage; + private final FeatureImage featuredImage; @Nullable private App currentApp; private final Activity activity; - private final int defaultFeaturedImageColour; private final DisplayImageOptions displayImageOptions; private final Date recentCuttoffDate; @@ -70,10 +70,9 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL icon = (ImageView) findViewAndEnsureNonNull(itemView, R.id.icon); 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); - defaultFeaturedImageColour = activity.getResources().getColor(R.color.cardview_light_background); displayImageOptions = Utils.getImageLoadingOptions().build(); itemView.setOnClickListener(this); @@ -113,7 +112,8 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL } if (featuredImage != null) { - featuredImage.setBackgroundColor(defaultFeaturedImageColour); + featuredImage.setPalette(null); + featuredImage.setImageDrawable(null); } ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions, this); @@ -151,12 +151,11 @@ public class AppCardController extends RecyclerView.ViewHolder implements ImageL @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - final ImageView image = featuredImage; - if (image != null) { + if (featuredImage != null) { new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { - image.setBackgroundColor(palette.getDominantColor(defaultFeaturedImageColour)); + featuredImage.setPalette(palette); } }); } diff --git a/app/src/main/res/layout/app_card_featured.xml b/app/src/main/res/layout/app_card_featured.xml index 9f1da484d..e4b163856 100644 --- a/app/src/main/res/layout/app_card_featured.xml +++ b/app/src/main/res/layout/app_card_featured.xml @@ -7,7 +7,7 @@ android:paddingBottom="2dp" android:clipToPadding="false"> - Date: Mon, 5 Dec 2016 22:32:51 +1100 Subject: [PATCH 02/28] Until feature images are properly supported, use abstract art instead. As per the main screens feature image behind the "Recently added" items, also use the same abstract artwork as a placeholder for the feature image in app details. --- .../java/org/fdroid/fdroid/AppDetails2.java | 47 ++++++++++++++----- app/src/main/res/layout/app_details2.xml | 4 +- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 8c8c980ca..ffb259ce8 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -16,6 +16,7 @@ import android.support.design.widget.CoordinatorLayout; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.graphics.Palette; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; @@ -23,12 +24,13 @@ import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import android.widget.ImageView; +import android.view.View; import android.widget.Toast; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; @@ -44,6 +46,7 @@ import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter; import org.fdroid.fdroid.views.ShareChooserDialog; +import org.fdroid.fdroid.views.apps.FeatureImage; public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks { @@ -95,15 +98,37 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog recyclerView.setAdapter(adapter); // Load the feature graphic, if present - if (!TextUtils.isEmpty(app.iconUrlLarge)) { - ImageView ivFeatureGraphic = (ImageView) findViewById(R.id.feature_graphic); - DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder() - .cacheInMemory(false) - .cacheOnDisk(true) - .imageScaleType(ImageScaleType.NONE) - .bitmapConfig(Bitmap.Config.RGB_565) - .build(); - ImageLoader.getInstance().displayImage(app.iconUrlLarge, ivFeatureGraphic, displayImageOptions); + if (!TextUtils.isEmpty(app.iconUrl)) { + final FeatureImage featureImage = (FeatureImage) findViewById(R.id.feature_graphic); + DisplayImageOptions displayImageOptions = Utils.getImageLoadingOptions().build(); + ImageLoader.getInstance().loadImage(app.iconUrl, displayImageOptions, new ImageLoadingListener() { + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + if (featureImage != null) { + new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + featureImage.setPalette(palette); + } + }); + } + } + + @Override + public void onLoadingStarted(String imageUri, View view) { + + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + + } + }); } } diff --git a/app/src/main/res/layout/app_details2.xml b/app/src/main/res/layout/app_details2.xml index 31b441b7e..4e7316d0a 100644 --- a/app/src/main/res/layout/app_details2.xml +++ b/app/src/main/res/layout/app_details2.xml @@ -25,13 +25,13 @@ app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed"> - Date: Thu, 8 Dec 2016 07:04:31 +1100 Subject: [PATCH 03/28] Tweak some button styles with those added in !419. The styles used by the app details showed good padding on either side of the buttons text. This was because they had a certain amount of screen space to fill up which resulted in nice empty space on either side of the text. Other buttons do not have this type of layout, so need to have a minimum amount of padding thrust upon them. Required breaking out into values-v17 too, so refactored common styles into base style to make this easier. --- app/src/main/res/layout/category_item.xml | 1 + app/src/main/res/layout/main_tab_swap.xml | 1 + .../main/res/layout/my_apps_updates_header.xml | 1 + app/src/main/res/values-v17/styles_detail.xml | 7 +++++++ app/src/main/res/values/styles_detail.xml | 17 +++++++++++------ 5 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/values-v17/styles_detail.xml diff --git a/app/src/main/res/layout/category_item.xml b/app/src/main/res/layout/category_item.xml index 392ee0889..d2e516f80 100644 --- a/app/src/main/res/layout/category_item.xml +++ b/app/src/main/res/layout/category_item.xml @@ -24,6 +24,7 @@