michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import org.json.JSONObject; michael@0: michael@0: import android.app.Application; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.Canvas; michael@0: import android.graphics.Paint; michael@0: import android.graphics.Rect; michael@0: import android.graphics.Shader; michael@0: import android.graphics.drawable.BitmapDrawable; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.os.Build; michael@0: import android.os.Handler; michael@0: import android.os.Looper; michael@0: import android.util.DisplayMetrics; michael@0: import android.util.Log; michael@0: import android.view.Gravity; michael@0: import android.view.View; michael@0: import android.view.ViewParent; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: michael@0: public class LightweightTheme implements GeckoEventListener { michael@0: private static final String LOGTAG = "GeckoLightweightTheme"; michael@0: michael@0: private Application mApplication; michael@0: private Handler mHandler; michael@0: michael@0: private Bitmap mBitmap; michael@0: private int mColor; michael@0: private boolean mIsLight; michael@0: michael@0: public static interface OnChangeListener { michael@0: // The View should change its background/text color. michael@0: public void onLightweightThemeChanged(); michael@0: michael@0: // The View should reset to its default background/text color. michael@0: public void onLightweightThemeReset(); michael@0: } michael@0: michael@0: private List mListeners; michael@0: michael@0: public LightweightTheme(Application application) { michael@0: mApplication = application; michael@0: mHandler = new Handler(Looper.getMainLooper()); michael@0: mListeners = new ArrayList(); michael@0: michael@0: // unregister isn't needed as the lifetime is same as the application. michael@0: GeckoAppShell.getEventDispatcher().registerEventListener("LightweightTheme:Update", this); michael@0: GeckoAppShell.getEventDispatcher().registerEventListener("LightweightTheme:Disable", this); michael@0: } michael@0: michael@0: public void addListener(final OnChangeListener listener) { michael@0: // Don't inform the listeners that attached late. michael@0: // Their onLayout() will take care of them before their onDraw(); michael@0: mListeners.add(listener); michael@0: } michael@0: michael@0: public void removeListener(OnChangeListener listener) { michael@0: mListeners.remove(listener); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals("LightweightTheme:Update")) { michael@0: JSONObject lightweightTheme = message.getJSONObject("data"); michael@0: final String headerURL = lightweightTheme.getString("headerURL"); michael@0: michael@0: // Move any heavy lifting off the Gecko thread michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: String croppedURL = headerURL; michael@0: int mark = croppedURL.indexOf('?'); michael@0: if (mark != -1) michael@0: croppedURL = croppedURL.substring(0, mark); michael@0: michael@0: // Get the image and convert it to a bitmap. michael@0: final Bitmap bitmap = BitmapUtils.decodeUrl(croppedURL); michael@0: mHandler.post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: setLightweightTheme(bitmap); michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: } else if (event.equals("LightweightTheme:Disable")) { michael@0: mHandler.post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: resetLightweightTheme(); michael@0: } michael@0: }); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Set a new lightweight theme with the given bitmap. michael@0: * Note: This should be called on the UI thread to restrict accessing the michael@0: * bitmap to a single thread. michael@0: * michael@0: * @param bitmap The bitmap used for the lightweight theme. michael@0: */ michael@0: private void setLightweightTheme(Bitmap bitmap) { michael@0: if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { michael@0: mBitmap = null; michael@0: return; michael@0: } michael@0: michael@0: // To find the dominant color only once, take the bottom 25% of pixels. michael@0: DisplayMetrics dm = mApplication.getResources().getDisplayMetrics(); michael@0: int maxWidth = Math.max(dm.widthPixels, dm.heightPixels); michael@0: int height = (int) (bitmap.getHeight() * 0.25); michael@0: michael@0: // The lightweight theme image's width and height. michael@0: int bitmapWidth = bitmap.getWidth(); michael@0: int bitmapHeight = bitmap.getHeight(); michael@0: michael@0: // A cropped bitmap of the bottom 25% of pixels. michael@0: Bitmap cropped = Bitmap.createBitmap(bitmap, michael@0: bitmapWidth > maxWidth ? bitmapWidth - maxWidth : 0, michael@0: bitmapHeight - height, michael@0: bitmapWidth > maxWidth ? maxWidth : bitmapWidth, michael@0: height); michael@0: michael@0: // Dominant color based on the cropped bitmap. michael@0: mColor = BitmapUtils.getDominantColor(cropped, false); michael@0: michael@0: // Calculate the luminance to determine if it's a light or a dark theme. michael@0: double luminance = (0.2125 * ((mColor & 0x00FF0000) >> 16)) + michael@0: (0.7154 * ((mColor & 0x0000FF00) >> 8)) + michael@0: (0.0721 * (mColor &0x000000FF)); michael@0: mIsLight = (luminance > 110) ? true : false; michael@0: michael@0: // The bitmap image might be smaller than the device's width. michael@0: // If it's smaller, fill the extra space on the left with the dominant color. michael@0: if (bitmap.getWidth() >= maxWidth) { michael@0: mBitmap = bitmap; michael@0: } else { michael@0: Paint paint = new Paint(); michael@0: paint.setAntiAlias(true); michael@0: michael@0: // Create a bigger image that can fill the device width. michael@0: // By creating a canvas for the bitmap, anything drawn on the canvas michael@0: // will be drawn on the bitmap. michael@0: mBitmap = Bitmap.createBitmap(maxWidth, bitmapHeight, Bitmap.Config.ARGB_8888); michael@0: Canvas canvas = new Canvas(mBitmap); michael@0: michael@0: // Fill the canvas with dominant color. michael@0: canvas.drawColor(mColor); michael@0: michael@0: // The image should be top-right aligned. michael@0: Rect rect = new Rect(); michael@0: Gravity.apply(Gravity.TOP | Gravity.RIGHT, michael@0: bitmapWidth, michael@0: bitmapHeight, michael@0: new Rect(0, 0, maxWidth, bitmapHeight), michael@0: rect); michael@0: michael@0: // Draw the bitmap. michael@0: canvas.drawBitmap(bitmap, null, rect, paint); michael@0: } michael@0: michael@0: for (OnChangeListener listener : mListeners) michael@0: listener.onLightweightThemeChanged(); michael@0: } michael@0: michael@0: /** michael@0: * Reset the lightweight theme. michael@0: * Note: This should be called on the UI thread to restrict accessing the michael@0: * bitmap to a single thread. michael@0: */ michael@0: private void resetLightweightTheme() { michael@0: if (mBitmap != null) { michael@0: // Reset the bitmap. michael@0: mBitmap = null; michael@0: michael@0: for (OnChangeListener listener : mListeners) michael@0: listener.onLightweightThemeReset(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * A lightweight theme is enabled only if there is an active bitmap. michael@0: * michael@0: * @return True if the theme is enabled. michael@0: */ michael@0: public boolean isEnabled() { michael@0: return (mBitmap != null); michael@0: } michael@0: michael@0: /** michael@0: * Based on the luminance of the domanint color, a theme is classified as light or dark. michael@0: * michael@0: * @return True if the theme is light. michael@0: */ michael@0: public boolean isLightTheme() { michael@0: return mIsLight; michael@0: } michael@0: michael@0: /** michael@0: * Crop the image based on the position of the view on the window. michael@0: * Either the View or one of its ancestors might have scrolled or translated. michael@0: * This value should be taken into account while mapping the View to the Bitmap. michael@0: * michael@0: * @param view The view requesting a cropped bitmap. michael@0: */ michael@0: private Bitmap getCroppedBitmap(View view) { michael@0: if (mBitmap == null || view == null) michael@0: return null; michael@0: michael@0: // Get the global position of the view on the entire screen. michael@0: Rect rect = new Rect(); michael@0: view.getGlobalVisibleRect(rect); michael@0: michael@0: // Get the activity's window position. This does an IPC call, may be expensive. michael@0: Rect window = new Rect(); michael@0: view.getWindowVisibleDisplayFrame(window); michael@0: michael@0: // Calculate the coordinates for the cropped bitmap. michael@0: int screenWidth = view.getContext().getResources().getDisplayMetrics().widthPixels; michael@0: int left = mBitmap.getWidth() - screenWidth + rect.left; michael@0: int right = mBitmap.getWidth() - screenWidth + rect.right; michael@0: int top = rect.top - window.top; michael@0: int bottom = rect.bottom - window.top; michael@0: michael@0: int offsetX = 0; michael@0: int offsetY = 0; michael@0: michael@0: // Find if this view or any of its ancestors has been translated or scrolled. michael@0: ViewParent parent; michael@0: View curView = view; michael@0: do { michael@0: if (Build.VERSION.SDK_INT >= 11) { michael@0: offsetX += (int) curView.getTranslationX() - curView.getScrollX(); michael@0: offsetY += (int) curView.getTranslationY() - curView.getScrollY(); michael@0: } else { michael@0: offsetX -= curView.getScrollX(); michael@0: offsetY -= curView.getScrollY(); michael@0: } michael@0: michael@0: parent = curView.getParent(); michael@0: michael@0: if (parent instanceof View) michael@0: curView = (View) parent; michael@0: michael@0: } while(parent instanceof View && parent != null); michael@0: michael@0: // Adjust the coordinates for the offset. michael@0: left -= offsetX; michael@0: right -= offsetX; michael@0: top -= offsetY; michael@0: bottom -= offsetY; michael@0: michael@0: // The either the required height may be less than the available image height or more than it. michael@0: // If the height required is more, crop only the available portion on the image. michael@0: int width = right - left; michael@0: int height = (bottom > mBitmap.getHeight() ? mBitmap.getHeight() - top : bottom - top); michael@0: michael@0: // There is a chance that the view is not visible or doesn't fall within the phone's size. michael@0: // In this case, 'rect' will have all values as '0'. Hence 'top' and 'bottom' may be negative, michael@0: // and createBitmap() will fail. michael@0: // The view will get a background in its next layout pass. michael@0: try { michael@0: return Bitmap.createBitmap(mBitmap, left, top, width, height); michael@0: } catch (Exception e) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Converts the cropped bitmap to a BitmapDrawable and returns the same. michael@0: * michael@0: * @param view The view for which a background drawable is required. michael@0: * @return Either the cropped bitmap as a Drawable or null. michael@0: */ michael@0: public Drawable getDrawable(View view) { michael@0: Bitmap bitmap = getCroppedBitmap(view); michael@0: if (bitmap == null) michael@0: return null; michael@0: michael@0: BitmapDrawable drawable = new BitmapDrawable(view.getContext().getResources(), bitmap); michael@0: drawable.setGravity(Gravity.TOP|Gravity.RIGHT); michael@0: drawable.setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); michael@0: return drawable; michael@0: } michael@0: michael@0: /** michael@0: * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the dominant color. michael@0: * michael@0: * @param view The view for which a background drawable is required. michael@0: * @return Either the cropped bitmap as a Drawable or null. michael@0: */ michael@0: public LightweightThemeDrawable getColorDrawable(View view) { michael@0: return getColorDrawable(view, mColor, false); michael@0: } michael@0: michael@0: /** michael@0: * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color. michael@0: * michael@0: * @param view The view for which a background drawable is required. michael@0: * @param color The color over which the drawable should be drawn. michael@0: * @return Either the cropped bitmap as a Drawable or null. michael@0: */ michael@0: public LightweightThemeDrawable getColorDrawable(View view, int color) { michael@0: return getColorDrawable(view, color, false); michael@0: } michael@0: michael@0: /** michael@0: * Converts the cropped bitmap to a LightweightThemeDrawable, placing it over the required color. michael@0: * michael@0: * @param view The view for which a background drawable is required. michael@0: * @param color The color over which the drawable should be drawn. michael@0: * @param needsDominantColor A layer of dominant color is needed or not. michael@0: * @return Either the cropped bitmap as a Drawable or null. michael@0: */ michael@0: public LightweightThemeDrawable getColorDrawable(View view, int color, boolean needsDominantColor) { michael@0: Bitmap bitmap = getCroppedBitmap(view); michael@0: if (bitmap == null) michael@0: return null; michael@0: michael@0: LightweightThemeDrawable drawable = new LightweightThemeDrawable(view.getContext().getResources(), bitmap); michael@0: if (needsDominantColor) michael@0: drawable.setColorWithFilter(color, (mColor & 0x22FFFFFF)); michael@0: else michael@0: drawable.setColor(color); michael@0: michael@0: return drawable; michael@0: } michael@0: }