|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko; |
|
7 |
|
8 import org.mozilla.gecko.gfx.BitmapUtils; |
|
9 import org.mozilla.gecko.util.GeckoEventListener; |
|
10 import org.mozilla.gecko.util.ThreadUtils; |
|
11 |
|
12 import org.json.JSONObject; |
|
13 |
|
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; |
|
30 |
|
31 import java.util.ArrayList; |
|
32 import java.util.List; |
|
33 |
|
34 public class LightweightTheme implements GeckoEventListener { |
|
35 private static final String LOGTAG = "GeckoLightweightTheme"; |
|
36 |
|
37 private Application mApplication; |
|
38 private Handler mHandler; |
|
39 |
|
40 private Bitmap mBitmap; |
|
41 private int mColor; |
|
42 private boolean mIsLight; |
|
43 |
|
44 public static interface OnChangeListener { |
|
45 // The View should change its background/text color. |
|
46 public void onLightweightThemeChanged(); |
|
47 |
|
48 // The View should reset to its default background/text color. |
|
49 public void onLightweightThemeReset(); |
|
50 } |
|
51 |
|
52 private List<OnChangeListener> mListeners; |
|
53 |
|
54 public LightweightTheme(Application application) { |
|
55 mApplication = application; |
|
56 mHandler = new Handler(Looper.getMainLooper()); |
|
57 mListeners = new ArrayList<OnChangeListener>(); |
|
58 |
|
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 } |
|
63 |
|
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 } |
|
69 |
|
70 public void removeListener(OnChangeListener listener) { |
|
71 mListeners.remove(listener); |
|
72 } |
|
73 |
|
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"); |
|
80 |
|
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); |
|
89 |
|
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 } |
|
112 |
|
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 } |
|
125 |
|
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); |
|
130 |
|
131 // The lightweight theme image's width and height. |
|
132 int bitmapWidth = bitmap.getWidth(); |
|
133 int bitmapHeight = bitmap.getHeight(); |
|
134 |
|
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); |
|
141 |
|
142 // Dominant color based on the cropped bitmap. |
|
143 mColor = BitmapUtils.getDominantColor(cropped, false); |
|
144 |
|
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; |
|
150 |
|
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); |
|
158 |
|
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); |
|
164 |
|
165 // Fill the canvas with dominant color. |
|
166 canvas.drawColor(mColor); |
|
167 |
|
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); |
|
175 |
|
176 // Draw the bitmap. |
|
177 canvas.drawBitmap(bitmap, null, rect, paint); |
|
178 } |
|
179 |
|
180 for (OnChangeListener listener : mListeners) |
|
181 listener.onLightweightThemeChanged(); |
|
182 } |
|
183 |
|
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; |
|
193 |
|
194 for (OnChangeListener listener : mListeners) |
|
195 listener.onLightweightThemeReset(); |
|
196 } |
|
197 } |
|
198 |
|
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 } |
|
207 |
|
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 } |
|
216 |
|
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; |
|
227 |
|
228 // Get the global position of the view on the entire screen. |
|
229 Rect rect = new Rect(); |
|
230 view.getGlobalVisibleRect(rect); |
|
231 |
|
232 // Get the activity's window position. This does an IPC call, may be expensive. |
|
233 Rect window = new Rect(); |
|
234 view.getWindowVisibleDisplayFrame(window); |
|
235 |
|
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; |
|
242 |
|
243 int offsetX = 0; |
|
244 int offsetY = 0; |
|
245 |
|
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 } |
|
257 |
|
258 parent = curView.getParent(); |
|
259 |
|
260 if (parent instanceof View) |
|
261 curView = (View) parent; |
|
262 |
|
263 } while(parent instanceof View && parent != null); |
|
264 |
|
265 // Adjust the coordinates for the offset. |
|
266 left -= offsetX; |
|
267 right -= offsetX; |
|
268 top -= offsetY; |
|
269 bottom -= offsetY; |
|
270 |
|
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); |
|
275 |
|
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 } |
|
286 |
|
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; |
|
297 |
|
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 } |
|
303 |
|
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 } |
|
313 |
|
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 } |
|
324 |
|
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; |
|
337 |
|
338 LightweightThemeDrawable drawable = new LightweightThemeDrawable(view.getContext().getResources(), bitmap); |
|
339 if (needsDominantColor) |
|
340 drawable.setColorWithFilter(color, (mColor & 0x22FFFFFF)); |
|
341 else |
|
342 drawable.setColor(color); |
|
343 |
|
344 return drawable; |
|
345 } |
|
346 } |