mobile/android/base/LightweightTheme.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial