mobile/android/base/LightweightTheme.java

changeset 0
6474c204b198
     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 +}

mercurial