diff --git a/mobile/android/base/ThumbnailHelper.java b/mobile/android/base/ThumbnailHelper.java index 2c0603b8d790..25213068fa33 100644 --- a/mobile/android/base/ThumbnailHelper.java +++ b/mobile/android/base/ThumbnailHelper.java @@ -30,9 +30,19 @@ import java.util.concurrent.atomic.AtomicInteger; public final class ThumbnailHelper { private static final String LOGTAG = "GeckoThumbnailHelper"; - public static final float THUMBNAIL_ASPECT_RATIO = 0.571f; // this is a 4:7 ratio (as per UX decision) + public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f; + public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f; // this is a 4:7 ratio (as per UX decision) + private static final float THUMBNAIL_ASPECT_RATIO; - public static enum CachePolicy { + static { + // As we only want to generate one thumbnail for each tab, we calculate the + // largest aspect ratio required and create the thumbnail based off that. + // Any views with a smaller aspect ratio will use a cropped version of the + // same image. + THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO); + } + + public enum CachePolicy { STORE, NO_STORE } @@ -55,14 +65,10 @@ public final class ThumbnailHelper { private int mWidth; private int mHeight; private ByteBuffer mBuffer; - private final float mThumbnailAspectRatio; private ThumbnailHelper() { final Resources res = GeckoAppShell.getContext().getResources(); - final TypedValue outValue = new TypedValue(); - res.getValue(R.dimen.thumbnail_aspect_ratio, outValue, true); - mThumbnailAspectRatio = outValue.getFloat(); mPendingThumbnails = new LinkedList(); try { @@ -111,7 +117,7 @@ public final class ThumbnailHelper { private void updateThumbnailSize() { // Apply any pending width updates. mWidth = mPendingWidth.get(); - mHeight = Math.round(mWidth * mThumbnailAspectRatio); + mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO); int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; int capacity = mWidth * mHeight * pixelSize; diff --git a/mobile/android/base/home/TopSitesThumbnailView.java b/mobile/android/base/home/TopSitesThumbnailView.java index 8e26884018f4..1cbf6b8b9974 100644 --- a/mobile/android/base/home/TopSitesThumbnailView.java +++ b/mobile/android/base/home/TopSitesThumbnailView.java @@ -8,36 +8,25 @@ package org.mozilla.gecko.home; import org.mozilla.gecko.R; import org.mozilla.gecko.ThumbnailHelper; import org.mozilla.gecko.util.ColorUtils; -import org.mozilla.gecko.util.HardwareUtils; +import org.mozilla.gecko.widget.CropImageView; import android.content.Context; import android.content.res.Resources; -import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff.Mode; -import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.widget.ImageView; /** - * A height constrained ImageView to show thumbnails of top and pinned sites. + * A width constrained ImageView to show thumbnails of top and pinned sites. */ -public class TopSitesThumbnailView extends ImageView { +public class TopSitesThumbnailView extends CropImageView { private static final String LOGTAG = "GeckoTopSitesThumbnailView"; // 27.34% opacity filter for the dominant color. private static final int COLOR_FILTER = 0x46FFFFFF; - // Cache variables used in onMeasure. - // - // Note: we have two matrices because we can't change it in place - see ImageView.getImageMatrix docs. - private final RectF mLayoutRect = new RectF(); - private Matrix mLayoutCurrentMatrix = new Matrix(); - private Matrix mLayoutNextMatrix = new Matrix(); - // Default filter color for "Add a bookmark" views. private final int mDefaultColor = ColorUtils.getColor(getContext(), R.color.top_site_default); @@ -47,16 +36,11 @@ public class TopSitesThumbnailView extends ImageView { // Paint for drawing the border. private final Paint mBorderPaint; - private boolean mResize = false; - private int mWidth; - private int mHeight; - public TopSitesThumbnailView(Context context) { this(context, null); // A border will be drawn if needed. setWillNotDraw(false); - } public TopSitesThumbnailView(Context context, AttributeSet attrs) { @@ -73,69 +57,9 @@ public class TopSitesThumbnailView extends ImageView { mBorderPaint.setStyle(Paint.Style.STROKE); } - public void setImageBitmap(Bitmap bm, boolean resize) { - super.setImageBitmap(bm); - mResize = resize; - clearLayoutVars(); - - updateImageMatrix(); - } - - private void clearLayoutVars() { - mLayoutRect.setEmpty(); - } - - private void updateImageMatrix() { - if (!HardwareUtils.isTablet() || !mResize) { - return; - } - - // No work to be done here - assumes the rect gets reset when a new bitmap is set. - if (mLayoutRect.right == mWidth && mLayoutRect.bottom == mHeight) { - return; - } - - setScaleType(ScaleType.MATRIX); - - mLayoutRect.set(0, 0, mWidth, mHeight); - mLayoutNextMatrix.setRectToRect(mLayoutRect, mLayoutRect, Matrix.ScaleToFit.CENTER); - setImageMatrix(mLayoutNextMatrix); - - final Matrix swapReferenceMatrix = mLayoutCurrentMatrix; - mLayoutCurrentMatrix = mLayoutNextMatrix; - mLayoutNextMatrix = swapReferenceMatrix; - } - @Override - public void setImageResource(int resId) { - super.setImageResource(resId); - mResize = false; - } - - @Override - public void setImageDrawable(Drawable drawable) { - super.setImageDrawable(drawable); - mResize = false; - } - - /** - * Measure the view to determine the measured width and height. - * The height is constrained by the measured width. - * - * @param widthMeasureSpec horizontal space requirements as imposed by the parent. - * @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored. - */ - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // Default measuring. - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - // Force the height based on the aspect ratio. - mWidth = getMeasuredWidth(); - mHeight = (int) (mWidth * ThumbnailHelper.THUMBNAIL_ASPECT_RATIO); - setMeasuredDimension(mWidth, mHeight); - - updateImageMatrix(); + protected float getAspectRatio() { + return ThumbnailHelper.TOP_SITES_THUMBNAIL_ASPECT_RATIO; } /** diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index cafc7d63dce3..30517792d597 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -469,6 +469,7 @@ gbjar.sources += [ 'tabs/TabsLayoutItemView.java', 'tabs/TabsListLayout.java', 'tabs/TabsPanel.java', + 'tabs/TabsPanelThumbnailView.java', 'Telemetry.java', 'TelemetryContract.java', 'TextSelection.java', @@ -521,6 +522,7 @@ gbjar.sources += [ 'widget/CheckableLinearLayout.java', 'widget/ClickableWhenDisabledEditText.java', 'widget/ContentSecurityDoorHanger.java', + 'widget/CropImageView.java', 'widget/DateTimePicker.java', 'widget/DefaultDoorHanger.java', 'widget/Divider.java', diff --git a/mobile/android/base/resources/layout-v11/tablet_tabs_item_cell.xml b/mobile/android/base/resources/layout-v11/tablet_tabs_item_cell.xml index aa7fcb59d0d7..89f9aa29d45f 100644 --- a/mobile/android/base/resources/layout-v11/tablet_tabs_item_cell.xml +++ b/mobile/android/base/resources/layout-v11/tablet_tabs_item_cell.xml @@ -66,9 +66,9 @@ android:background="@drawable/tab_thumbnail" android:duplicateParentState="true"> - diff --git a/mobile/android/base/resources/layout/tabs_item_cell.xml b/mobile/android/base/resources/layout/tabs_item_cell.xml index 712bbdadb037..12ad7c38ccf5 100644 --- a/mobile/android/base/resources/layout/tabs_item_cell.xml +++ b/mobile/android/base/resources/layout/tabs_item_cell.xml @@ -25,9 +25,9 @@ android:background="@drawable/tab_thumbnail" android:duplicateParentState="true"> - + - + diff --git a/mobile/android/base/resources/values-large-v11/dimens.xml b/mobile/android/base/resources/values-large-v11/dimens.xml index d4aafaef0ff3..be4e5537cc19 100644 --- a/mobile/android/base/resources/values-large-v11/dimens.xml +++ b/mobile/android/base/resources/values-large-v11/dimens.xml @@ -35,9 +35,6 @@ 360dp - - 0.9 - 72dp diff --git a/mobile/android/base/resources/values/dimens.xml b/mobile/android/base/resources/values/dimens.xml index 861c6a4a0922..3f9a096a048e 100644 --- a/mobile/android/base/resources/values/dimens.xml +++ b/mobile/android/base/resources/values/dimens.xml @@ -213,9 +213,6 @@ 1.5dp - - 0.571 - -1 -2 diff --git a/mobile/android/base/tabs/TabsLayoutItemView.java b/mobile/android/base/tabs/TabsLayoutItemView.java index 3126de0555a6..2ef680725587 100644 --- a/mobile/android/base/tabs/TabsLayoutItemView.java +++ b/mobile/android/base/tabs/TabsLayoutItemView.java @@ -10,7 +10,6 @@ import org.mozilla.gecko.R; import org.mozilla.gecko.Tab; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.widget.TabThumbnailWrapper; -import org.mozilla.gecko.widget.ThumbnailView; import org.json.JSONException; import org.json.JSONObject; @@ -37,7 +36,7 @@ public class TabsLayoutItemView extends LinearLayout private int mTabId; private TextView mTitle; - private ThumbnailView mThumbnail; + private TabsPanelThumbnailView mThumbnail; private ImageView mCloseButton; private ImageView mAudioPlayingButton; private TabThumbnailWrapper mThumbnailWrapper; @@ -98,7 +97,7 @@ public class TabsLayoutItemView extends LinearLayout protected void onFinishInflate() { super.onFinishInflate(); mTitle = (TextView) findViewById(R.id.title); - mThumbnail = (ThumbnailView) findViewById(R.id.thumbnail); + mThumbnail = (TabsPanelThumbnailView) findViewById(R.id.thumbnail); mCloseButton = (ImageView) findViewById(R.id.close); mAudioPlayingButton = (ImageView) findViewById(R.id.audio_playing); mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper); diff --git a/mobile/android/base/tabs/TabsPanelThumbnailView.java b/mobile/android/base/tabs/TabsPanelThumbnailView.java new file mode 100644 index 000000000000..09254bf76703 --- /dev/null +++ b/mobile/android/base/tabs/TabsPanelThumbnailView.java @@ -0,0 +1,52 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tabs; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.ThumbnailHelper; +import org.mozilla.gecko.widget.CropImageView; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +/** + * A width constrained ImageView to show thumbnails of open tabs in the tabs panel. + */ +public class TabsPanelThumbnailView extends CropImageView { + public static final String LOGTAG = "Gecko" + TabsPanelThumbnailView.class.getSimpleName(); + + + public TabsPanelThumbnailView(final Context context) { + this(context, null); + } + + public TabsPanelThumbnailView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public TabsPanelThumbnailView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected float getAspectRatio() { + return ThumbnailHelper.TABS_PANEL_THUMBNAIL_ASPECT_RATIO; + } + + @Override + public void setImageDrawable(Drawable drawable) { + boolean resize = true; + + if (drawable == null) { + drawable = getResources().getDrawable(R.drawable.tab_panel_tab_background); + resize = false; + setScaleType(ScaleType.FIT_XY); + } + + super.setImageDrawable(drawable, resize); + } +} diff --git a/mobile/android/base/widget/CropImageView.java b/mobile/android/base/widget/CropImageView.java new file mode 100644 index 000000000000..0c2cbcf7d0d6 --- /dev/null +++ b/mobile/android/base/widget/CropImageView.java @@ -0,0 +1,142 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.nineoldandroids.view.ViewHelper; + +/** + * An ImageView which will always display at the given width and calculated height (based on the width and + * the supplied aspect ratio), drawn starting from the top left hand corner. A supplied drawable will be resized to fit + * the width of the view; if the resized drawable is too tall for the view then the drawable will be cropped at the + * bottom, however if the resized drawable is too short for the view to display whilst honouring it's given width and + * height then the drawable will be displayed at full height with the right hand side cropped. + */ +public abstract class CropImageView extends ThemedImageView { + public static final String LOGTAG = "Gecko" + CropImageView.class.getSimpleName(); + + private int viewWidth; + private int viewHeight; + private int drawableWidth; + private int drawableHeight; + + private boolean resize = true; + private Matrix layoutCurrentMatrix = new Matrix(); + private Matrix layoutNextMatrix = new Matrix(); + + + public CropImageView(final Context context) { + this(context, null); + } + + public CropImageView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public CropImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + protected abstract float getAspectRatio(); + + protected void init() { + // Setting the pivots means that the image will be drawn from the top left hand corner. There are + // issues in Android 4.1 (16) which mean setting these values to 0 may not work. + // http://stackoverflow.com/questions/26658124/setpivotx-doesnt-work-on-android-4-1-1-nineoldandroids + ViewHelper.setPivotX(this, 1); + ViewHelper.setPivotY(this, 1); + } + + /** + * Measure the view to determine the measured width and height. + * The height is constrained by the measured width. + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent. + * @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored. + */ + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + // Default measuring. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Force the height based on the aspect ratio. + viewWidth = getMeasuredWidth(); + viewHeight = (int) (viewWidth * getAspectRatio()); + + setMeasuredDimension(viewWidth, viewHeight); + + updateImageMatrix(); + } + + protected void updateImageMatrix() { + if (!resize || getDrawable() == null) { + return; + } + + setScaleType(ImageView.ScaleType.MATRIX); + + getDrawable().setBounds(0, 0, viewWidth, viewHeight); + + final float horizontalScaleValue = (float) viewWidth / (float) drawableWidth; + final float verticalScaleValue = (float) viewHeight / (float) drawableHeight; + + final float scale = Math.max(verticalScaleValue, horizontalScaleValue); + + layoutNextMatrix.setScale(scale, scale); + setImageMatrix(layoutNextMatrix); + + // You can't modify the matrix in place and we want to avoid allocation, so let's keep two references to two + // different matrix objects that we can swap when the values need to change + final Matrix swapReferenceMatrix = layoutCurrentMatrix; + layoutCurrentMatrix = layoutNextMatrix; + layoutNextMatrix = swapReferenceMatrix; + } + + public void setImageBitmap(final Bitmap bm, final boolean resize) { + super.setImageBitmap(bm); + + this.resize = resize; + updateImageMatrix(); + } + + @Override + public void setImageResource(final int resId) { + super.setImageResource(resId); + setImageMatrix(null); + resize = false; + } + + @Override + public void setImageDrawable(final Drawable drawable) { + this.setImageDrawable(drawable, false); + } + + public void setImageDrawable(final Drawable drawable, final boolean resize) { + super.setImageDrawable(drawable); + + if (drawable != null) { + // Reset the matrix to ensure that any previous changes aren't carried through. + setImageMatrix(null); + + drawableWidth = drawable.getIntrinsicWidth(); + drawableHeight = drawable.getIntrinsicHeight(); + } else { + drawableWidth = -1; + drawableHeight = -1; + } + + this.resize = resize; + + updateImageMatrix(); + } +}