Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko;
8 import org.mozilla.gecko.gfx.BitmapUtils;
9 import org.mozilla.gecko.util.GeckoEventListener;
10 import org.mozilla.gecko.util.ThreadUtils;
12 import org.json.JSONObject;
14 import android.app.Application;
15 import android.graphics.Bitmap;
16 import android.graphics.Canvas;
17 import android.graphics.Paint;
18 import android.graphics.Rect;
19 import android.graphics.Shader;
20 import android.graphics.drawable.BitmapDrawable;
21 import android.graphics.drawable.Drawable;
22 import android.os.Build;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.util.DisplayMetrics;
26 import android.util.Log;
27 import android.view.Gravity;
28 import android.view.View;
29 import android.view.ViewParent;
31 import java.util.ArrayList;
32 import java.util.List;
34 public class LightweightTheme implements GeckoEventListener {
35 private static final String LOGTAG = "GeckoLightweightTheme";
37 private Application mApplication;
38 private Handler mHandler;
40 private Bitmap mBitmap;
41 private int mColor;
42 private boolean mIsLight;
44 public static interface OnChangeListener {
45 // The View should change its background/text color.
46 public void onLightweightThemeChanged();
48 // The View should reset to its default background/text color.
49 public void onLightweightThemeReset();
50 }
52 private List<OnChangeListener> mListeners;
54 public LightweightTheme(Application application) {
55 mApplication = application;
56 mHandler = new Handler(Looper.getMainLooper());
57 mListeners = new ArrayList<OnChangeListener>();
59 // unregister isn't needed as the lifetime is same as the application.
60 GeckoAppShell.getEventDispatcher().registerEventListener("LightweightTheme:Update", this);
61 GeckoAppShell.getEventDispatcher().registerEventListener("LightweightTheme:Disable", this);
62 }
64 public void addListener(final OnChangeListener listener) {
65 // Don't inform the listeners that attached late.
66 // Their onLayout() will take care of them before their onDraw();
67 mListeners.add(listener);
68 }
70 public void removeListener(OnChangeListener listener) {
71 mListeners.remove(listener);
72 }
74 @Override
75 public void handleMessage(String event, JSONObject message) {
76 try {
77 if (event.equals("LightweightTheme:Update")) {
78 JSONObject lightweightTheme = message.getJSONObject("data");
79 final String headerURL = lightweightTheme.getString("headerURL");
81 // Move any heavy lifting off the Gecko thread
82 ThreadUtils.postToBackgroundThread(new Runnable() {
83 @Override
84 public void run() {
85 String croppedURL = headerURL;
86 int mark = croppedURL.indexOf('?');
87 if (mark != -1)
88 croppedURL = croppedURL.substring(0, mark);
90 // Get the image and convert it to a bitmap.
91 final Bitmap bitmap = BitmapUtils.decodeUrl(croppedURL);
92 mHandler.post(new Runnable() {
93 @Override
94 public void run() {
95 setLightweightTheme(bitmap);
96 }
97 });
98 }
99 });
100 } else if (event.equals("LightweightTheme:Disable")) {
101 mHandler.post(new Runnable() {
102 @Override
103 public void run() {
104 resetLightweightTheme();
105 }
106 });
107 }
108 } catch (Exception e) {
109 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
110 }
111 }
113 /**
114 * Set a new lightweight theme with the given bitmap.
115 * Note: This should be called on the UI thread to restrict accessing the
116 * bitmap to a single thread.
117 *
118 * @param bitmap The bitmap used for the lightweight theme.
119 */
120 private void setLightweightTheme(Bitmap bitmap) {
121 if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
122 mBitmap = null;
123 return;
124 }
126 // To find the dominant color only once, take the bottom 25% of pixels.
127 DisplayMetrics dm = mApplication.getResources().getDisplayMetrics();
128 int maxWidth = Math.max(dm.widthPixels, dm.heightPixels);
129 int height = (int) (bitmap.getHeight() * 0.25);
131 // The lightweight theme image's width and height.
132 int bitmapWidth = bitmap.getWidth();
133 int bitmapHeight = bitmap.getHeight();
135 // A cropped bitmap of the bottom 25% of pixels.
136 Bitmap cropped = Bitmap.createBitmap(bitmap,
137 bitmapWidth > maxWidth ? bitmapWidth - maxWidth : 0,
138 bitmapHeight - height,
139 bitmapWidth > maxWidth ? maxWidth : bitmapWidth,
140 height);
142 // Dominant color based on the cropped bitmap.
143 mColor = BitmapUtils.getDominantColor(cropped, false);
145 // Calculate the luminance to determine if it's a light or a dark theme.
146 double luminance = (0.2125 * ((mColor & 0x00FF0000) >> 16)) +
147 (0.7154 * ((mColor & 0x0000FF00) >> 8)) +
148 (0.0721 * (mColor &0x000000FF));
149 mIsLight = (luminance > 110) ? true : false;
151 // The bitmap image might be smaller than the device's width.
152 // If it's smaller, fill the extra space on the left with the dominant color.
153 if (bitmap.getWidth() >= maxWidth) {
154 mBitmap = bitmap;
155 } else {
156 Paint paint = new Paint();
157 paint.setAntiAlias(true);
159 // Create a bigger image that can fill the device width.
160 // By creating a canvas for the bitmap, anything drawn on the canvas
161 // will be drawn on the bitmap.
162 mBitmap = Bitmap.createBitmap(maxWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
163 Canvas canvas = new Canvas(mBitmap);
165 // Fill the canvas with dominant color.
166 canvas.drawColor(mColor);
168 // The image should be top-right aligned.
169 Rect rect = new Rect();
170 Gravity.apply(Gravity.TOP | Gravity.RIGHT,
171 bitmapWidth,
172 bitmapHeight,
173 new Rect(0, 0, maxWidth, bitmapHeight),
174 rect);
176 // Draw the bitmap.
177 canvas.drawBitmap(bitmap, null, rect, paint);
178 }
180 for (OnChangeListener listener : mListeners)
181 listener.onLightweightThemeChanged();
182 }
184 /**
185 * Reset the lightweight theme.
186 * Note: This should be called on the UI thread to restrict accessing the
187 * bitmap to a single thread.
188 */
189 private void resetLightweightTheme() {
190 if (mBitmap != null) {
191 // Reset the bitmap.
192 mBitmap = null;
194 for (OnChangeListener listener : mListeners)
195 listener.onLightweightThemeReset();
196 }
197 }
199 /**
200 * A lightweight theme is enabled only if there is an active bitmap.
201 *
202 * @return True if the theme is enabled.
203 */
204 public boolean isEnabled() {
205 return (mBitmap != null);
206 }
208 /**
209 * Based on the luminance of the domanint color, a theme is classified as light or dark.
210 *
211 * @return True if the theme is light.
212 */
213 public boolean isLightTheme() {
214 return mIsLight;
215 }
217 /**
218 * Crop the image based on the position of the view on the window.
219 * Either the View or one of its ancestors might have scrolled or translated.
220 * This value should be taken into account while mapping the View to the Bitmap.
221 *
222 * @param view The view requesting a cropped bitmap.
223 */
224 private Bitmap getCroppedBitmap(View view) {
225 if (mBitmap == null || view == null)
226 return null;
228 // Get the global position of the view on the entire screen.
229 Rect rect = new Rect();
230 view.getGlobalVisibleRect(rect);
232 // Get the activity's window position. This does an IPC call, may be expensive.
233 Rect window = new Rect();
234 view.getWindowVisibleDisplayFrame(window);
236 // Calculate the coordinates for the cropped bitmap.
237 int screenWidth = view.getContext().getResources().getDisplayMetrics().widthPixels;
238 int left = mBitmap.getWidth() - screenWidth + rect.left;
239 int right = mBitmap.getWidth() - screenWidth + rect.right;
240 int top = rect.top - window.top;
241 int bottom = rect.bottom - window.top;
243 int offsetX = 0;
244 int offsetY = 0;
246 // Find if this view or any of its ancestors has been translated or scrolled.
247 ViewParent parent;
248 View curView = view;
249 do {
250 if (Build.VERSION.SDK_INT >= 11) {
251 offsetX += (int) curView.getTranslationX() - curView.getScrollX();
252 offsetY += (int) curView.getTranslationY() - curView.getScrollY();
253 } else {
254 offsetX -= curView.getScrollX();
255 offsetY -= curView.getScrollY();
256 }
258 parent = curView.getParent();
260 if (parent instanceof View)
261 curView = (View) parent;
263 } while(parent instanceof View && parent != null);
265 // Adjust the coordinates for the offset.
266 left -= offsetX;
267 right -= offsetX;
268 top -= offsetY;
269 bottom -= offsetY;
271 // The either the required height may be less than the available image height or more than it.
272 // If the height required is more, crop only the available portion on the image.
273 int width = right - left;
274 int height = (bottom > mBitmap.getHeight() ? mBitmap.getHeight() - top : bottom - top);
276 // There is a chance that the view is not visible or doesn't fall within the phone's size.
277 // In this case, 'rect' will have all values as '0'. Hence 'top' and 'bottom' may be negative,
278 // and createBitmap() will fail.
279 // The view will get a background in its next layout pass.
280 try {
281 return Bitmap.createBitmap(mBitmap, left, top, width, height);
282 } catch (Exception e) {
283 return null;
284 }
285 }
287 /**
288 * Converts the cropped bitmap to a BitmapDrawable and returns the same.
289 *
290 * @param view The view for which a background drawable is required.
291 * @return Either the cropped bitmap as a Drawable or null.
292 */
293 public Drawable getDrawable(View view) {
294 Bitmap bitmap = getCroppedBitmap(view);
295 if (bitmap == null)
296 return null;
298 BitmapDrawable drawable = new BitmapDrawable(view.getContext().getResources(), bitmap);
299 drawable.setGravity(Gravity.TOP|Gravity.RIGHT);
300 drawable.setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
301 return drawable;
302 }
304 /**
305 * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the dominant color.
306 *
307 * @param view The view for which a background drawable is required.
308 * @return Either the cropped bitmap as a Drawable or null.
309 */
310 public LightweightThemeDrawable getColorDrawable(View view) {
311 return getColorDrawable(view, mColor, false);
312 }
314 /**
315 * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color.
316 *
317 * @param view The view for which a background drawable is required.
318 * @param color The color over which the drawable should be drawn.
319 * @return Either the cropped bitmap as a Drawable or null.
320 */
321 public LightweightThemeDrawable getColorDrawable(View view, int color) {
322 return getColorDrawable(view, color, false);
323 }
325 /**
326 * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color.
327 *
328 * @param view The view for which a background drawable is required.
329 * @param color The color over which the drawable should be drawn.
330 * @param needsDominantColor A layer of dominant color is needed or not.
331 * @return Either the cropped bitmap as a Drawable or null.
332 */
333 public LightweightThemeDrawable getColorDrawable(View view, int color, boolean needsDominantColor) {
334 Bitmap bitmap = getCroppedBitmap(view);
335 if (bitmap == null)
336 return null;
338 LightweightThemeDrawable drawable = new LightweightThemeDrawable(view.getContext().getResources(), bitmap);
339 if (needsDominantColor)
340 drawable.setColorWithFilter(color, (mColor & 0x22FFFFFF));
341 else
342 drawable.setColor(color);
344 return drawable;
345 }
346 }