1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/LightweightTheme.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,346 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.gfx.BitmapUtils; 1.12 +import org.mozilla.gecko.util.GeckoEventListener; 1.13 +import org.mozilla.gecko.util.ThreadUtils; 1.14 + 1.15 +import org.json.JSONObject; 1.16 + 1.17 +import android.app.Application; 1.18 +import android.graphics.Bitmap; 1.19 +import android.graphics.Canvas; 1.20 +import android.graphics.Paint; 1.21 +import android.graphics.Rect; 1.22 +import android.graphics.Shader; 1.23 +import android.graphics.drawable.BitmapDrawable; 1.24 +import android.graphics.drawable.Drawable; 1.25 +import android.os.Build; 1.26 +import android.os.Handler; 1.27 +import android.os.Looper; 1.28 +import android.util.DisplayMetrics; 1.29 +import android.util.Log; 1.30 +import android.view.Gravity; 1.31 +import android.view.View; 1.32 +import android.view.ViewParent; 1.33 + 1.34 +import java.util.ArrayList; 1.35 +import java.util.List; 1.36 + 1.37 +public class LightweightTheme implements GeckoEventListener { 1.38 + private static final String LOGTAG = "GeckoLightweightTheme"; 1.39 + 1.40 + private Application mApplication; 1.41 + private Handler mHandler; 1.42 + 1.43 + private Bitmap mBitmap; 1.44 + private int mColor; 1.45 + private boolean mIsLight; 1.46 + 1.47 + public static interface OnChangeListener { 1.48 + // The View should change its background/text color. 1.49 + public void onLightweightThemeChanged(); 1.50 + 1.51 + // The View should reset to its default background/text color. 1.52 + public void onLightweightThemeReset(); 1.53 + } 1.54 + 1.55 + private List<OnChangeListener> mListeners; 1.56 + 1.57 + public LightweightTheme(Application application) { 1.58 + mApplication = application; 1.59 + mHandler = new Handler(Looper.getMainLooper()); 1.60 + mListeners = new ArrayList<OnChangeListener>(); 1.61 + 1.62 + // unregister isn't needed as the lifetime is same as the application. 1.63 + GeckoAppShell.getEventDispatcher().registerEventListener("LightweightTheme:Update", this); 1.64 + GeckoAppShell.getEventDispatcher().registerEventListener("LightweightTheme:Disable", this); 1.65 + } 1.66 + 1.67 + public void addListener(final OnChangeListener listener) { 1.68 + // Don't inform the listeners that attached late. 1.69 + // Their onLayout() will take care of them before their onDraw(); 1.70 + mListeners.add(listener); 1.71 + } 1.72 + 1.73 + public void removeListener(OnChangeListener listener) { 1.74 + mListeners.remove(listener); 1.75 + } 1.76 + 1.77 + @Override 1.78 + public void handleMessage(String event, JSONObject message) { 1.79 + try { 1.80 + if (event.equals("LightweightTheme:Update")) { 1.81 + JSONObject lightweightTheme = message.getJSONObject("data"); 1.82 + final String headerURL = lightweightTheme.getString("headerURL"); 1.83 + 1.84 + // Move any heavy lifting off the Gecko thread 1.85 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.86 + @Override 1.87 + public void run() { 1.88 + String croppedURL = headerURL; 1.89 + int mark = croppedURL.indexOf('?'); 1.90 + if (mark != -1) 1.91 + croppedURL = croppedURL.substring(0, mark); 1.92 + 1.93 + // Get the image and convert it to a bitmap. 1.94 + final Bitmap bitmap = BitmapUtils.decodeUrl(croppedURL); 1.95 + mHandler.post(new Runnable() { 1.96 + @Override 1.97 + public void run() { 1.98 + setLightweightTheme(bitmap); 1.99 + } 1.100 + }); 1.101 + } 1.102 + }); 1.103 + } else if (event.equals("LightweightTheme:Disable")) { 1.104 + mHandler.post(new Runnable() { 1.105 + @Override 1.106 + public void run() { 1.107 + resetLightweightTheme(); 1.108 + } 1.109 + }); 1.110 + } 1.111 + } catch (Exception e) { 1.112 + Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); 1.113 + } 1.114 + } 1.115 + 1.116 + /** 1.117 + * Set a new lightweight theme with the given bitmap. 1.118 + * Note: This should be called on the UI thread to restrict accessing the 1.119 + * bitmap to a single thread. 1.120 + * 1.121 + * @param bitmap The bitmap used for the lightweight theme. 1.122 + */ 1.123 + private void setLightweightTheme(Bitmap bitmap) { 1.124 + if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { 1.125 + mBitmap = null; 1.126 + return; 1.127 + } 1.128 + 1.129 + // To find the dominant color only once, take the bottom 25% of pixels. 1.130 + DisplayMetrics dm = mApplication.getResources().getDisplayMetrics(); 1.131 + int maxWidth = Math.max(dm.widthPixels, dm.heightPixels); 1.132 + int height = (int) (bitmap.getHeight() * 0.25); 1.133 + 1.134 + // The lightweight theme image's width and height. 1.135 + int bitmapWidth = bitmap.getWidth(); 1.136 + int bitmapHeight = bitmap.getHeight(); 1.137 + 1.138 + // A cropped bitmap of the bottom 25% of pixels. 1.139 + Bitmap cropped = Bitmap.createBitmap(bitmap, 1.140 + bitmapWidth > maxWidth ? bitmapWidth - maxWidth : 0, 1.141 + bitmapHeight - height, 1.142 + bitmapWidth > maxWidth ? maxWidth : bitmapWidth, 1.143 + height); 1.144 + 1.145 + // Dominant color based on the cropped bitmap. 1.146 + mColor = BitmapUtils.getDominantColor(cropped, false); 1.147 + 1.148 + // Calculate the luminance to determine if it's a light or a dark theme. 1.149 + double luminance = (0.2125 * ((mColor & 0x00FF0000) >> 16)) + 1.150 + (0.7154 * ((mColor & 0x0000FF00) >> 8)) + 1.151 + (0.0721 * (mColor &0x000000FF)); 1.152 + mIsLight = (luminance > 110) ? true : false; 1.153 + 1.154 + // The bitmap image might be smaller than the device's width. 1.155 + // If it's smaller, fill the extra space on the left with the dominant color. 1.156 + if (bitmap.getWidth() >= maxWidth) { 1.157 + mBitmap = bitmap; 1.158 + } else { 1.159 + Paint paint = new Paint(); 1.160 + paint.setAntiAlias(true); 1.161 + 1.162 + // Create a bigger image that can fill the device width. 1.163 + // By creating a canvas for the bitmap, anything drawn on the canvas 1.164 + // will be drawn on the bitmap. 1.165 + mBitmap = Bitmap.createBitmap(maxWidth, bitmapHeight, Bitmap.Config.ARGB_8888); 1.166 + Canvas canvas = new Canvas(mBitmap); 1.167 + 1.168 + // Fill the canvas with dominant color. 1.169 + canvas.drawColor(mColor); 1.170 + 1.171 + // The image should be top-right aligned. 1.172 + Rect rect = new Rect(); 1.173 + Gravity.apply(Gravity.TOP | Gravity.RIGHT, 1.174 + bitmapWidth, 1.175 + bitmapHeight, 1.176 + new Rect(0, 0, maxWidth, bitmapHeight), 1.177 + rect); 1.178 + 1.179 + // Draw the bitmap. 1.180 + canvas.drawBitmap(bitmap, null, rect, paint); 1.181 + } 1.182 + 1.183 + for (OnChangeListener listener : mListeners) 1.184 + listener.onLightweightThemeChanged(); 1.185 + } 1.186 + 1.187 + /** 1.188 + * Reset the lightweight theme. 1.189 + * Note: This should be called on the UI thread to restrict accessing the 1.190 + * bitmap to a single thread. 1.191 + */ 1.192 + private void resetLightweightTheme() { 1.193 + if (mBitmap != null) { 1.194 + // Reset the bitmap. 1.195 + mBitmap = null; 1.196 + 1.197 + for (OnChangeListener listener : mListeners) 1.198 + listener.onLightweightThemeReset(); 1.199 + } 1.200 + } 1.201 + 1.202 + /** 1.203 + * A lightweight theme is enabled only if there is an active bitmap. 1.204 + * 1.205 + * @return True if the theme is enabled. 1.206 + */ 1.207 + public boolean isEnabled() { 1.208 + return (mBitmap != null); 1.209 + } 1.210 + 1.211 + /** 1.212 + * Based on the luminance of the domanint color, a theme is classified as light or dark. 1.213 + * 1.214 + * @return True if the theme is light. 1.215 + */ 1.216 + public boolean isLightTheme() { 1.217 + return mIsLight; 1.218 + } 1.219 + 1.220 + /** 1.221 + * Crop the image based on the position of the view on the window. 1.222 + * Either the View or one of its ancestors might have scrolled or translated. 1.223 + * This value should be taken into account while mapping the View to the Bitmap. 1.224 + * 1.225 + * @param view The view requesting a cropped bitmap. 1.226 + */ 1.227 + private Bitmap getCroppedBitmap(View view) { 1.228 + if (mBitmap == null || view == null) 1.229 + return null; 1.230 + 1.231 + // Get the global position of the view on the entire screen. 1.232 + Rect rect = new Rect(); 1.233 + view.getGlobalVisibleRect(rect); 1.234 + 1.235 + // Get the activity's window position. This does an IPC call, may be expensive. 1.236 + Rect window = new Rect(); 1.237 + view.getWindowVisibleDisplayFrame(window); 1.238 + 1.239 + // Calculate the coordinates for the cropped bitmap. 1.240 + int screenWidth = view.getContext().getResources().getDisplayMetrics().widthPixels; 1.241 + int left = mBitmap.getWidth() - screenWidth + rect.left; 1.242 + int right = mBitmap.getWidth() - screenWidth + rect.right; 1.243 + int top = rect.top - window.top; 1.244 + int bottom = rect.bottom - window.top; 1.245 + 1.246 + int offsetX = 0; 1.247 + int offsetY = 0; 1.248 + 1.249 + // Find if this view or any of its ancestors has been translated or scrolled. 1.250 + ViewParent parent; 1.251 + View curView = view; 1.252 + do { 1.253 + if (Build.VERSION.SDK_INT >= 11) { 1.254 + offsetX += (int) curView.getTranslationX() - curView.getScrollX(); 1.255 + offsetY += (int) curView.getTranslationY() - curView.getScrollY(); 1.256 + } else { 1.257 + offsetX -= curView.getScrollX(); 1.258 + offsetY -= curView.getScrollY(); 1.259 + } 1.260 + 1.261 + parent = curView.getParent(); 1.262 + 1.263 + if (parent instanceof View) 1.264 + curView = (View) parent; 1.265 + 1.266 + } while(parent instanceof View && parent != null); 1.267 + 1.268 + // Adjust the coordinates for the offset. 1.269 + left -= offsetX; 1.270 + right -= offsetX; 1.271 + top -= offsetY; 1.272 + bottom -= offsetY; 1.273 + 1.274 + // The either the required height may be less than the available image height or more than it. 1.275 + // If the height required is more, crop only the available portion on the image. 1.276 + int width = right - left; 1.277 + int height = (bottom > mBitmap.getHeight() ? mBitmap.getHeight() - top : bottom - top); 1.278 + 1.279 + // There is a chance that the view is not visible or doesn't fall within the phone's size. 1.280 + // In this case, 'rect' will have all values as '0'. Hence 'top' and 'bottom' may be negative, 1.281 + // and createBitmap() will fail. 1.282 + // The view will get a background in its next layout pass. 1.283 + try { 1.284 + return Bitmap.createBitmap(mBitmap, left, top, width, height); 1.285 + } catch (Exception e) { 1.286 + return null; 1.287 + } 1.288 + } 1.289 + 1.290 + /** 1.291 + * Converts the cropped bitmap to a BitmapDrawable and returns the same. 1.292 + * 1.293 + * @param view The view for which a background drawable is required. 1.294 + * @return Either the cropped bitmap as a Drawable or null. 1.295 + */ 1.296 + public Drawable getDrawable(View view) { 1.297 + Bitmap bitmap = getCroppedBitmap(view); 1.298 + if (bitmap == null) 1.299 + return null; 1.300 + 1.301 + BitmapDrawable drawable = new BitmapDrawable(view.getContext().getResources(), bitmap); 1.302 + drawable.setGravity(Gravity.TOP|Gravity.RIGHT); 1.303 + drawable.setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 1.304 + return drawable; 1.305 + } 1.306 + 1.307 + /** 1.308 + * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the dominant color. 1.309 + * 1.310 + * @param view The view for which a background drawable is required. 1.311 + * @return Either the cropped bitmap as a Drawable or null. 1.312 + */ 1.313 + public LightweightThemeDrawable getColorDrawable(View view) { 1.314 + return getColorDrawable(view, mColor, false); 1.315 + } 1.316 + 1.317 + /** 1.318 + * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color. 1.319 + * 1.320 + * @param view The view for which a background drawable is required. 1.321 + * @param color The color over which the drawable should be drawn. 1.322 + * @return Either the cropped bitmap as a Drawable or null. 1.323 + */ 1.324 + public LightweightThemeDrawable getColorDrawable(View view, int color) { 1.325 + return getColorDrawable(view, color, false); 1.326 + } 1.327 + 1.328 + /** 1.329 + * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color. 1.330 + * 1.331 + * @param view The view for which a background drawable is required. 1.332 + * @param color The color over which the drawable should be drawn. 1.333 + * @param needsDominantColor A layer of dominant color is needed or not. 1.334 + * @return Either the cropped bitmap as a Drawable or null. 1.335 + */ 1.336 + public LightweightThemeDrawable getColorDrawable(View view, int color, boolean needsDominantColor) { 1.337 + Bitmap bitmap = getCroppedBitmap(view); 1.338 + if (bitmap == null) 1.339 + return null; 1.340 + 1.341 + LightweightThemeDrawable drawable = new LightweightThemeDrawable(view.getContext().getResources(), bitmap); 1.342 + if (needsDominantColor) 1.343 + drawable.setColorWithFilter(color, (mColor & 0x22FFFFFF)); 1.344 + else 1.345 + drawable.setColor(color); 1.346 + 1.347 + return drawable; 1.348 + } 1.349 +}