blob: 36f21c9d527a5124e9038b789fdc91416e565662 [file] [log] [blame]
Pankaj Garg21dad562015-07-02 17:17:24 -07001/*
2 * Copyright (c) 2015, The Linux Foundation. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above
10 * copyright notice, this list of conditions and the following
11 * disclaimer in the documentation and/or other materials provided
12 * with the distribution.
13 * * Neither the name of The Linux Foundation nor the names of its
14 * contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
18 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
21 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
24 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28*/
29package com.android.browser;
30
31import android.content.Context;
32import android.content.res.TypedArray;
33import android.graphics.Bitmap;
34import android.graphics.Canvas;
35import android.graphics.Color;
36import android.graphics.Paint;
37import android.graphics.Rect;
38import android.graphics.RectF;
39import android.graphics.drawable.BitmapDrawable;
40import android.graphics.drawable.Drawable;
41import android.util.AttributeSet;
42import android.util.DisplayMetrics;
43import android.util.TypedValue;
44import android.view.View;
45
46/**
47 * This represents a WebSite Tile that is created from a Drawable and will scale across any
48 * area this is externally layouted to. There are 3 possible looks:
49 * - just the favicon (TYPE_SMALL)
50 * - drop-shadow plus a thin overlay border (1dp) (TYPE_MEDIUM)
51 * - centered favicon, extended color, rounded base (TYPE_LARGE)
52 *
53 * By centralizing everything in this class we make customization of looks much easier.
54 *
55 * NOTES:
56 * - do not set a background from the outside; this overrides it automatically
57 */
58public class SiteTileView extends View {
59
60 // external configuration constants
61 public static final int TYPE_SMALL = 1;
62 public static final int TYPE_MEDIUM = 2;
63 public static final int TYPE_LARGE = 3;
64 private static final int TYPE_AUTO = 0;
65 private static final int COLOR_AUTO = 0;
66
67
68 // static configuration
69 private static final int THRESHOLD_MEDIUM_DP = 32;
70 private static final int THRESHOLD_LARGE_DP = 64;
71 private static final int LARGE_FAVICON_SIZE_DP = 48;
72 private static final int BACKGROUND_DRAWABLE_RES = R.drawable.img_tile_background;
73 private static final float FILLER_RADIUS_DP = 2f; // sync with the bg image radius
74 private static final int FILLER_FALLBACK_COLOR = Color.WHITE; // in case there is no favicon
75 private static final int OVERLINE_WIDTH_RES = R.dimen.SiteTileOverline;
76 private static final int OVERLINE_COLOR_RES = R.color.SiteTileOverline;
77
78
79 // configuration
80 private Bitmap mFaviconBitmap = null;
81 private Paint mFundamentalPaint = null;
82 private int mFaviconWidth = 0;
83 private int mFaviconHeight = 0;
84 private int mForcedType = TYPE_AUTO;
85 private int mForcedFundamentalColor = COLOR_AUTO;
86
87 // static objects, to be recycled amongst instances (this is an optimization)
88 private static int sMediumPxThreshold = -1;
89 private static int sLargePxThreshold = -1;
90 private static int sLargeFaviconPx = -1;
91 private static float sRoundedRadius = -1;
92 private static Paint sBitmapPaint = null;
93 private static Rect sSrcRect = new Rect();
94 private static Rect sDstRect = new Rect();
95 private static RectF sRectF = new RectF();
96 private static Paint sOverlineOutlinePaint = null;
97 private static Drawable sBackgroundDrawable = null;
98 private static Rect sBackgroundDrawablePadding = new Rect();
99
100 // runtime params set on Layout
101 private int mCurrentWidth = 0;
102 private int mCurrentHeight = 0;
103 private int mCurrentType = TYPE_MEDIUM;
104 private boolean mCurrentBackgroundDrawn = false;
105 private boolean mFloating = false;
106 private int mPaddingLeft = 0;
107 private int mPaddingTop = 0;
108 private int mPaddingRight = 0;
109 private int mPaddingBottom = 0;
110
111
112
113 /* XML constructors */
114
115 public SiteTileView(Context context) {
116 super(context);
117 xmlInit(null, 0);
118 }
119
120 public SiteTileView(Context context, AttributeSet attrs) {
121 super(context, attrs);
122 xmlInit(attrs, 0);
123 }
124
125 public SiteTileView(Context context, AttributeSet attrs, int defStyle) {
126 super(context, attrs, defStyle);
127 xmlInit(attrs, defStyle);
128 }
129
130
131 /* Programmatic Constructors */
132
133 public SiteTileView(Context context, Bitmap favicon) {
134 super(context);
135 init(favicon, COLOR_AUTO);
136 }
137
138 public SiteTileView(Context context, Bitmap favicon, int fundamentalColor) {
139 super(context);
140 init(favicon, fundamentalColor);
141 }
142
143
144 /**
145 * Changes the current favicon (and associated fundamental color) on the fly
146 */
147 public void replaceFavicon(Bitmap favicon) {
148 replaceFavicon(favicon, COLOR_AUTO);
149 }
150
151 /**
152 * Changes the current favicon (and associated fundamental color) on the fly
153 * @param favicon the new favicon
154 * @param fundamentalColor the new fudamental color, or COLOR_AUTO
155 */
156 public void replaceFavicon(Bitmap favicon, int fundamentalColor) {
157 init(favicon, fundamentalColor);
158 requestLayout();
159 }
160
161 /**
162 * Disables the automatic background and filling. Useful for things that are not really
163 * "Website Tiles", like folders.
164 * @param floating true to disable the background (defaults to false)
165 */
166 public void setFloating(boolean floating) {
167 mFloating = floating;
168 invalidate();
169 }
170
171
172 /**
173 * @return The fundamental color representing the site.
174 */
175 public int getFundamentalColor() {
176 if (mForcedFundamentalColor != COLOR_AUTO)
177 return mForcedFundamentalColor;
178 if (mFundamentalPaint == null)
179 mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, COLOR_AUTO);
180 return mFundamentalPaint.getColor();
181 }
182
183
184 /*** private stuff ahead ***/
185
186 private void xmlInit(AttributeSet attrs, int defStyle) {
187 // load attributes
188 final TypedArray a = getContext().obtainStyledAttributes(attrs,
189 R.styleable.SiteTileView, defStyle, 0);
190
191 // fetch the drawable, if defined - then just extract and use the bitmap
192 final Drawable drawable = a.getDrawable(R.styleable.SiteTileView_android_src);
193 final Bitmap favicon = drawable instanceof BitmapDrawable ?
194 ((BitmapDrawable) drawable).getBitmap() : null;
195
196 // check if we disable shading (plain favicon)
197 if (a.getBoolean(R.styleable.SiteTileView_flat, false))
198 mForcedType = TYPE_SMALL;
199
200 // check if we want it floating (disable shadow and filler)
201 if (a.getBoolean(R.styleable.SiteTileView_floating, false))
202 mFloating = true;
203
204 // delete attribute resolution
205 a.recycle();
206
207 // proceed with real initialization
208 init(favicon, COLOR_AUTO);
209 }
210
211 private void init(Bitmap favicon, int fundamentalColor) {
212 mFaviconBitmap = favicon;
213 if (mFaviconBitmap != null) {
214 mFaviconWidth = mFaviconBitmap.getWidth();
215 mFaviconHeight = mFaviconBitmap.getHeight();
216 }
217
218 // don't compute the paint right now, just save any hint for later
219 mFundamentalPaint = null;
220 mForcedFundamentalColor = fundamentalColor;
221
222 // shared (static) resources initialization; except for background, inited on-demand
223 if (sMediumPxThreshold < 0) {
224 final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
225
226 // heuristics thresholds
227 sMediumPxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
228 THRESHOLD_MEDIUM_DP, displayMetrics);
229 sLargePxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
230 THRESHOLD_LARGE_DP, displayMetrics);
231 sLargeFaviconPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
232 LARGE_FAVICON_SIZE_DP, displayMetrics);
233
234 // rounded radius
235 sRoundedRadius = FILLER_RADIUS_DP > 0 ? TypedValue.applyDimension(
236 TypedValue.COMPLEX_UNIT_DIP, FILLER_RADIUS_DP, displayMetrics) : 0;
237
238 // bitmap paint (copy, smooth scale)
239 sBitmapPaint = new Paint();
240 sBitmapPaint.setColor(Color.BLACK);
241 sBitmapPaint.setFilterBitmap(true);
242
243 // overline configuration (null if we don't need it)
244 int ovlColor = getResources().getColor(OVERLINE_COLOR_RES);
245 float ovlWidthPx = getResources().getDimension(OVERLINE_WIDTH_RES);
246 if (ovlWidthPx > 0.5 && ovlColor != Color.TRANSPARENT) {
247 sOverlineOutlinePaint = new Paint();
248 sOverlineOutlinePaint.setColor(ovlColor);
249 sOverlineOutlinePaint.setStrokeWidth(ovlWidthPx);
250 sOverlineOutlinePaint.setStyle(Paint.Style.STROKE);
251 }
252 }
253
254 // change when clicked
255 setClickable(true);
256 // disable by default the long click
257 setLongClickable(false);
258 }
259
260
261 @Override
262 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
263 mCurrentWidth = right - left;
264 mCurrentHeight = bottom - top;
265
266 // auto-determine the "TYPE_" from the physical size of the layout
267 if (mForcedType == TYPE_AUTO) {
268 if (mCurrentWidth < sMediumPxThreshold && mCurrentHeight < sMediumPxThreshold)
269 mCurrentType = TYPE_SMALL;
270 else if (mCurrentWidth < sLargePxThreshold && mCurrentHeight < sLargePxThreshold)
271 mCurrentType = TYPE_MEDIUM;
272 else
273 mCurrentType = TYPE_LARGE;
274 } else {
275 // or use the forced one, if defined
276 mCurrentType = mForcedType;
277 }
278
279 // set or remove the background (if the need changed!)
280 boolean requiresBackground = mCurrentType >= TYPE_MEDIUM;
281 if (requiresBackground && !mCurrentBackgroundDrawn) {
282 // draw the background
283 mCurrentBackgroundDrawn = true;
284
285 // load the background just the first time, on demand (it may fail too)
286 if (sBackgroundDrawable == null) {
287 sBackgroundDrawable = getResources().getDrawable(BACKGROUND_DRAWABLE_RES);
288 if (sBackgroundDrawable != null)
289 sBackgroundDrawable.getPadding(sBackgroundDrawablePadding);
290 }
291
292 // background -> padding
293 mPaddingLeft = sBackgroundDrawablePadding.left;
294 mPaddingTop = sBackgroundDrawablePadding.top;
295 mPaddingRight = sBackgroundDrawablePadding.right;
296 mPaddingBottom = sBackgroundDrawablePadding.bottom;
297 } else if (!requiresBackground && mCurrentBackgroundDrawn) {
298 // turn off background drawing
299 mCurrentBackgroundDrawn = false;
300
301 // no background -> no padding
302 mPaddingLeft = 0;
303 mPaddingTop = 0;
304 mPaddingRight = 0;
305 mPaddingBottom = 0;
306 }
307
308 // just proceed, do nothing here
309 super.onLayout(changed, left, top, right, bottom);
310 }
311
312 @Override
313 public void setPressed(boolean pressed) {
314 super.setPressed(pressed);
315 // schedule a repaint to show pressed/released
316 invalidate();
317 }
318
319 @Override
320 public void setSelected(boolean selected) {
321 super.setSelected(selected);
322 // schedule a repaint to show selected
323 invalidate();
324 }
325
326 @Override
327 protected void onDraw(Canvas canvas) {
328 super.onDraw(canvas);
329
330 // Selection State: make everything smaller
331 if (isSelected()) {
332 float scale = 0.8f;
333 canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2);
334 canvas.scale(scale, scale);
335 }
336
337 // Pressed state: make the button reach the finger
338 if (isPressed()) {
339 float scale = 1.1f;
340 canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2);
341 canvas.scale(scale, scale);
342 }
343
344 final int left = mPaddingLeft;
345 final int top = mPaddingTop;
346 final int right = mCurrentWidth - mPaddingRight;
347 final int bottom = mCurrentHeight - mPaddingBottom;
348 final int contentWidth = right - left;
349 final int contentHeight = bottom - top;
350
351 // A. the background drawable (if set)
352 boolean requiresBackground = mCurrentBackgroundDrawn && sBackgroundDrawable != null
353 && !isPressed() && !mFloating;
354 if (requiresBackground) {
355 sBackgroundDrawable.setBounds(0, 0, mCurrentWidth, mCurrentHeight);
356 sBackgroundDrawable.draw(canvas);
357 }
358
359 // B. (when needed) draw the background rectangle; sharp our rounded
360 boolean requiresFundamentalFiller = mCurrentType >= TYPE_LARGE && !mFloating;
361 if (requiresFundamentalFiller) {
362 // create the filler paint on demand (not all icons need it)
363 if (mFundamentalPaint == null)
364 mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, mForcedFundamentalColor);
365
366 // paint if not white, since requiresBackground already painted it white
367 int fundamentalColor = mFundamentalPaint.getColor();
368 if (fundamentalColor != COLOR_AUTO &&
369 (fundamentalColor != Color.WHITE || !requiresBackground)) {
370 if (sRoundedRadius >= 1.) {
371 sRectF.set(left, top, right, bottom);
372 canvas.drawRoundRect(sRectF, sRoundedRadius, sRoundedRadius, mFundamentalPaint);
373 } else
374 canvas.drawRect(left, top, right, bottom, mFundamentalPaint);
375 }
376 }
377
378 // C. (if present) draw the favicon
379 boolean requiresFavicon = mFaviconBitmap != null
380 && mFaviconWidth > 1 && mFaviconHeight > 1;
381 if (requiresFavicon) {
382 // destination can either fill, or auto-center
383 boolean fillSpace = mCurrentType <= TYPE_MEDIUM;
384 if (fillSpace || contentWidth < sLargeFaviconPx || contentHeight < sLargeFaviconPx) {
385 sDstRect.set(left, top, right, bottom);
386 } else {
387 int dstLeft = left + (contentWidth - sLargeFaviconPx) / 2;
388 int dstTop = top + (contentHeight - sLargeFaviconPx) / 2;
389 sDstRect.set(dstLeft, dstTop, dstLeft + sLargeFaviconPx, dstTop + sLargeFaviconPx);
390 }
391
392 // source has to 'crop proportionally' to keep the dest aspect ratio
393 sSrcRect.set(0, 0, mFaviconWidth, mFaviconHeight);
394 int sW = sSrcRect.width();
395 int sH = sSrcRect.height();
396 int dW = sDstRect.width();
397 int dH = sDstRect.height();
398 if (sW > 4 && sH > 4 && dW > 4 && dH > 4) {
399 float hScale = (float) dW / (float) sW;
400 float vScale = (float) dH / (float) sH;
401 if (hScale == vScale) {
402 // no transformation needed, just zoom
403 } else if (hScale < vScale) {
404 // horizontal crop
405 float hCrop = 1 - hScale / vScale;
406 int hCropPx = (int) (sW * hCrop / 2 + 0.5);
407 sSrcRect.left += hCropPx;
408 sSrcRect.right -= hCropPx;
409 canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint);
410 } else {
411 // vertical crop
412 float vCrop = 1 - vScale / hScale;
413 int vCropPx = (int) (sH * vCrop / 2 + 0.5);
414 sSrcRect.top += vCropPx;
415 sSrcRect.bottom -= vCropPx;
416 }
417 }
418
419 // blit favicon, croppped, scaled
420 canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint);
421 }
422
423 // D. (when needed) draw the thin over-line
424 boolean requiresOverline = mCurrentType == TYPE_MEDIUM
425 && sOverlineOutlinePaint != null;
426 if (requiresOverline) {
427 canvas.drawRect(left, top, right, bottom, sOverlineOutlinePaint);
428 }
429
430 /*if (true) { // DEBUG TYPE
431 Paint paint = new Paint();
432 paint.setColor(Color.BLACK);
433 paint.setTextSize(20);
434 canvas.drawText(String.valueOf(mCurrentType), 30, 30, paint);
435 }*/
436 }
437
438
439 /**
440 * Creates a fill Paint from the favicon, or using the forced color (if not COLOR_AUTO)
441 */
442 private static Paint createFundamentalPaint(Bitmap favicon, int forceFillColor) {
443 final Paint fillPaint = new Paint();
444 if (forceFillColor != COLOR_AUTO)
445 fillPaint.setColor(forceFillColor);
446 else
447 fillPaint.setColor(guessFundamentalColor(favicon));
448 return fillPaint;
449 }
450
451 /**
452 * This uses very stupid mechanism - a 9x9 grid sample on the borders and center - and selects
453 * the color with the most frequency, or the center.
454 *
455 * @param bitmap the bitmap to guesss the color about
456 * @return a Color
457 */
458 private static int guessFundamentalColor(Bitmap bitmap) {
459 if (bitmap == null)
460 return FILLER_FALLBACK_COLOR;
461 int height = bitmap.getHeight();
462 int width = bitmap.getWidth();
463 if (height < 2 || width < 2)
464 return FILLER_FALLBACK_COLOR;
465
466 // pick up to 9 colors
467 // NOTE: the order of sampling sets the precendece, in case of ties
468 int[] pxColors = new int[9];
469 int idx = 0;
470 if ((pxColors[idx] = sampleColor(bitmap, width / 2, height / 2)) != 0) idx++;
471 if ((pxColors[idx] = sampleColor(bitmap, width / 2, height - 1)) != 0) idx++;
472 if ((pxColors[idx] = sampleColor(bitmap, width - 1, height - 1)) != 0) idx++;
473 if ((pxColors[idx] = sampleColor(bitmap, width - 1, height / 2)) != 0) idx++;
474 if ((pxColors[idx] = sampleColor(bitmap, 0, 0 )) != 0) idx++;
475 if ((pxColors[idx] = sampleColor(bitmap, width / 2, 0 )) != 0) idx++;
476 if ((pxColors[idx] = sampleColor(bitmap, width - 1, 0 )) != 0) idx++;
477 if ((pxColors[idx] = sampleColor(bitmap, 0 , height / 2)) != 0) idx++;
478 if ((pxColors[idx] = sampleColor(bitmap, 0 , height - 1)) != 0) idx++;
479
480 // find the most popular
481 int popColor = -1;
482 int popCount = -1;
483 for (int i = 0; i < idx; i++) {
484 int thisColor = pxColors[i];
485 int thisCount = 0;
486 for (int j = 0; j < idx; j++) {
487 if (pxColors[j] == thisColor)
488 thisCount++;
489 }
490 if (thisCount > popCount) {
491 popColor = thisColor;
492 popCount = thisCount;
493 }
494 }
495 return popCount > -1 ? popColor : FILLER_FALLBACK_COLOR;
496 }
497
498 /**
499 * @return Color, but if it's 0, you should discard it (not representative)
500 */
501 private static int sampleColor(Bitmap bitmap, int x, int y) {
502 int color = bitmap.getPixel(x, y);
503 // discard semi-transparent pixels, because they're probably from a spurious border
504 // discard black pixels, because black is not a color (well, not a good looking one)
505 if ((color >>> 24) <= 128 || (color & 0xFFFFFF) == 0)
506 return 0;
507 return color;
508 }
509
510}