summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
blob: 4652345b4dd54c6bf62a96dd45c09cc7d4e72d6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/* -*- 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 org.mozilla.gecko.R;
import org.mozilla.gecko.icons.IconCallback;
import org.mozilla.gecko.icons.IconResponse;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.widget.ImageView;

import java.lang.ref.WeakReference;

/**
 * Special version of ImageView for favicons.
 * Displays solid colour background around Favicon to fill space not occupied by the icon. Colour
 * selected is the dominant colour of the provided Favicon.
 */
public class FaviconView extends ImageView {
    private static final String LOGTAG = "GeckoFaviconView";

    private static String DEFAULT_FAVICON_KEY = FaviconView.class.getSimpleName() + "DefaultFavicon";

    // Default x/y-radius of the oval used to round the corners of the background (dp)
    private static final int DEFAULT_CORNER_RADIUS_DP = 4;

    private Bitmap mIconBitmap;

    // Reference to the unscaled bitmap, if any, to prevent repeated assignments of the same bitmap
    // to the view from causing repeated rescalings (Some of the callers do this)
    private Bitmap mUnscaledBitmap;

    private int mActualWidth;
    private int mActualHeight;

    // Flag indicating if the most recently assigned image is considered likely to need scaling.
    private boolean mScalingExpected;

    // Dominant color of the favicon.
    private int mDominantColor;

    // Paint for drawing the background.
    private static final Paint sBackgroundPaint;

    // Size of the background rectangle.
    private final RectF mBackgroundRect;

    // The x/y-radius of the oval used to round the corners of the background (pixels)
    private final float mBackgroundCornerRadius;

    // Type of the border whose value is defined in attrs.xml .
    private final boolean isDominantBorderEnabled;

    // boolean switch for overriding scaletype, whose value is defined in attrs.xml .
    private final boolean isOverrideScaleTypeEnabled;

    // boolean switch for disabling rounded corners, value defined in attrs.xml .
    private final boolean areRoundCornersEnabled;

    // Initializing the static paints.
    static {
        sBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        sBackgroundPaint.setStyle(Paint.Style.FILL);
    }

    public FaviconView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FaviconView, 0, 0);

        try {
            isDominantBorderEnabled = a.getBoolean(R.styleable.FaviconView_dominantBorderEnabled, true);
            isOverrideScaleTypeEnabled = a.getBoolean(R.styleable.FaviconView_overrideScaleType, true);
            areRoundCornersEnabled = a.getBoolean(R.styleable.FaviconView_enableRoundCorners, true);
        } finally {
            a.recycle();
        }

        if (isOverrideScaleTypeEnabled) {
            setScaleType(ImageView.ScaleType.CENTER);
        }

        final DisplayMetrics metrics = getResources().getDisplayMetrics();

        mBackgroundRect = new RectF(0, 0, 0, 0);
        mBackgroundCornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CORNER_RADIUS_DP, metrics);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // No point rechecking the image if there hasn't really been any change.
        if (w == mActualWidth && h == mActualHeight) {
            return;
        }

        mActualWidth = w;
        mActualHeight = h;

        mBackgroundRect.right = w;
        mBackgroundRect.bottom = h;

        formatImage();
    }

    @Override
    public void onDraw(Canvas canvas) {
        if (isDominantBorderEnabled) {
            sBackgroundPaint.setColor(mDominantColor & 0x7FFFFFFF);

            if (areRoundCornersEnabled) {
                canvas.drawRoundRect(mBackgroundRect, mBackgroundCornerRadius, mBackgroundCornerRadius, sBackgroundPaint);
            } else {
                canvas.drawRect(mBackgroundRect, sBackgroundPaint);
            }
        }

        super.onDraw(canvas);
    }

    /**
     * Formats the image for display, if the prerequisite data are available. Upscales tiny Favicons to
     * normal sized ones, replaces null bitmaps with the default Favicon, and fills all remaining space
     * in this view with the coloured background.
     */
    private void formatImage() {
        // We're waiting for both onSizeChanged and updateImage to be called before scaling.
        if (mIconBitmap == null || mActualWidth == 0 || mActualHeight == 0) {
            showNoImage();
            return;
        }

        if (mScalingExpected && mActualWidth != mIconBitmap.getWidth()) {
            scaleBitmap();
            // Don't scale the image every time something changes.
            mScalingExpected = false;
        }

        setImageBitmap(mIconBitmap);

        // After scaling, determine if we have empty space around the scaled image which we need to
        // fill with the coloured background. If applicable, show it.
        // We assume Favicons are still squares and only bother with the background if more than 3px
        // of it would be displayed.
        if (Math.abs(mIconBitmap.getWidth() - mActualWidth) < 3) {
            mDominantColor = 0;
        }
    }

    private void scaleBitmap() {
        // If the Favicon can be resized to fill the view exactly without an enlargment of more than
        // a factor of two, do so.
        int doubledSize = mIconBitmap.getWidth() * 2;
        if (mActualWidth > doubledSize) {
            // If the view is more than twice the size of the image, just double the image size
            // and do the rest with padding.
            mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, doubledSize, doubledSize, true);
        } else {
            // Otherwise, scale the image to fill the view.
            mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, mActualWidth, mActualWidth, true);
        }
    }

    /**
     * Sets the icon displayed in this Favicon view to the bitmap provided. If the size of the view
     * has been set, the display will be updated right away, otherwise the update will be deferred
     * until then. The key provided is used to cache the result of the calculation of the dominant
     * colour of the provided image - this value is used to draw the coloured background in this view
     * if the icon is not large enough to fill it.
     *
     * @param allowScaling If true, allows the provided bitmap to be scaled by this FaviconView.
     *                     Typically, you should prefer using Favicons obtained via the caching system
     *                     (Favicons class), so as to exploit caching.
     */
    private void updateImageInternal(IconResponse response, boolean allowScaling) {
        // Reassigning the same bitmap? Don't bother.
        if (mUnscaledBitmap == response.getBitmap()) {
            return;
        }
        mUnscaledBitmap = response.getBitmap();
        mIconBitmap = response.getBitmap();
        mDominantColor = response.getColor();
        mScalingExpected = allowScaling;

        // Possibly update the display.
        formatImage();
    }

    private void showNoImage() {
        setImageDrawable(null);
        mDominantColor = 0;
    }

    /**
     * Clear image and background shown by this view.
     */
    public void clearImage() {
        showNoImage();
        mUnscaledBitmap = null;
        mIconBitmap = null;
        mDominantColor = 0;
        mScalingExpected = false;
    }

    /**
     * Update the displayed image and apply the scaling logic.
     * The scaling logic will attempt to resize the image to fit correctly inside the view in a way
     * that avoids unreasonable levels of loss of quality.
     * Scaling is necessary only when the icon being provided is not drawn from the Favicon cache
     * introduced in Bug 914296.
     *
     * Due to Bug 913746, icons bundled for search engines are not available to the cache, so must
     * always have the scaling logic applied here. At the time of writing, this is the only case in
     * which the scaling logic here is applied.
     */
    public void updateAndScaleImage(IconResponse response) {
        updateImageInternal(response, true);
    }

    /**
     * Update the image displayed in the Favicon view without scaling. Images larger than the view
     * will be centrally cropped. Images smaller than the view will be placed centrally and the
     * extra space filled with the dominant colour of the provided image.
     */
    public void updateImage(IconResponse response) {
        updateImageInternal(response, false);
    }

    public Bitmap getBitmap() {
        return mIconBitmap;
    }

    /**
     * Create an IconCallback implementation that will update this view after an icon has been loaded.
     */
    public IconCallback createIconCallback() {
        return new Callback(this);
    }

    private static class Callback implements IconCallback {
        private final WeakReference<FaviconView> viewReference;

        private Callback(FaviconView view) {
            this.viewReference = new WeakReference<FaviconView>(view);
        }

        @Override
        public void onIconResponse(IconResponse response) {
            final FaviconView view = viewReference.get();
            if (view == null) {
                return;
            }

            view.updateImage(response);
        }
    }
}