mobile/android/base/LightweightTheme.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial