| // Copyright 2010-2014, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| package org.mozc.android.inputmethod.japanese.preference; |
| |
| import org.mozc.android.inputmethod.japanese.MozcLog; |
| import org.mozc.android.inputmethod.japanese.MozcUtil; |
| import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; |
| import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; |
| import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyboardParser; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyboardViewBackgroundSurface; |
| import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; |
| import org.mozc.android.inputmethod.japanese.resources.R; |
| import org.mozc.android.inputmethod.japanese.view.DrawableCache; |
| import org.mozc.android.inputmethod.japanese.view.Skin; |
| import com.google.common.base.Preconditions; |
| |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.Config; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.EnumMap; |
| import java.util.Map; |
| import java.util.WeakHashMap; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * A Drawable to render keyboard preview. |
| * |
| */ |
| public class KeyboardPreviewDrawable extends Drawable { |
| |
| /** |
| * The key of the activity which uses the Bitmap cache. |
| * |
| * In order to utilize memories, it is necessary to tell |
| * to the BitmapCache what activities use it now. |
| * The current heuristic is register the activity in its onStart |
| * and unregister it in its onStop. |
| * In theory, it should work well as long as the onStop |
| * corresponding to the already-invoked onStart is invoked without errors. |
| * |
| * As the last resort, we use a finalizer guardian. |
| * The instance of the key should be referred only by an Activity instance |
| * in its field. Then: |
| * - the activity can register and unregister in its onStart/onStop methods. |
| * - Even if onStop is NOT invoked accidentally, the Activity will be |
| * collected by GC, and the key is also collected at the same time. |
| * Then the finalize of the key will be invoked, and it will unregister |
| * itself from Bitmap cache. |
| * - Overriding the finalizer may cause the delay of memory collecting. |
| * However, the CacheReferenceKey is small enough (at least compared to |
| * Activity as the Activity refers many other instances, too). So the |
| * risk (or damage) of the remaining phantom instances should be low enough. |
| * Note: Regardless of the finalizer, if onStop is correctly invoked, the bitmap |
| * cache will be released correctly. |
| */ |
| static class CacheReferenceKey { |
| |
| @Override |
| protected void finalize() throws Throwable { |
| BitmapCache.getInstance().removeReference(this); |
| super.finalize(); |
| } |
| } |
| |
| /** |
| * Global cache of bitmap for the keyboard preview. |
| * |
| * Assuming that the size of each bitmap preview is same, and skin type is globally unique, |
| * we can use global bitmap cache to keep the memory usage low. |
| * This cache also manages the referencing Activities. See {@link MozcBasePreferenceActivity} |
| * for the details. |
| */ |
| static class BitmapCache { |
| |
| private static final BitmapCache INSTANCE = new BitmapCache(); |
| private static final Object DUMMY_VALUE = new Object(); |
| |
| private final Map<KeyboardLayout, Bitmap> map = |
| new EnumMap<KeyboardLayout, Bitmap>(KeyboardLayout.class); |
| private Skin skin = Skin.getFallbackInstance(); |
| private final WeakHashMap<CacheReferenceKey, Object> referenceMap = |
| new WeakHashMap<CacheReferenceKey, Object>(); |
| |
| private BitmapCache() { |
| } |
| |
| static BitmapCache getInstance() { |
| return INSTANCE; |
| } |
| |
| @Nullable Bitmap get(KeyboardLayout keyboardLayout, int width, int height, Skin skin) { |
| Preconditions.checkNotNull(skin); |
| if (keyboardLayout == null || width <= 0 || height <= 0) { |
| return null; |
| } |
| |
| if (!skin.equals(this.skin)) { |
| return null; |
| } |
| |
| Bitmap result = map.get(keyboardLayout); |
| if (result != null) { |
| // Check the size. |
| if (result.getWidth() != width || result.getHeight() != height) { |
| result = null; |
| } |
| } |
| |
| return result; |
| } |
| |
| void put(@Nullable KeyboardLayout keyboardLayout, Skin skin, @Nullable Bitmap bitmap) { |
| Preconditions.checkNotNull(skin); |
| if (keyboardLayout == null || bitmap == null) { |
| return; |
| } |
| |
| if (!skin.equals(this.skin)) { |
| clear(); |
| this.skin = skin; |
| } |
| |
| Bitmap oldBitmap = map.put(keyboardLayout, bitmap); |
| if (oldBitmap != null) { |
| // Recycle old bitmap if exists. |
| oldBitmap.recycle(); |
| } |
| } |
| |
| void addReference(CacheReferenceKey key) { |
| referenceMap.put(key, DUMMY_VALUE); |
| } |
| |
| void removeReference(CacheReferenceKey key) { |
| referenceMap.remove(key); |
| if (referenceMap.isEmpty()) { |
| // When all referring activities are gone, we don't need keep the cache. |
| // To reduce the memory usage, release all the cached bitmap. |
| clear(); |
| } |
| } |
| |
| private void clear() { |
| for (Bitmap bitmap : map.values()) { |
| if (bitmap != null) { |
| bitmap.recycle(); |
| } |
| } |
| |
| map.clear(); |
| skin = Skin.getFallbackInstance(); |
| } |
| } |
| |
| private final Resources resources; |
| private final KeyboardLayout keyboardLayout; |
| private final KeyboardSpecification specification; |
| private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| |
| private Skin skin = Skin.getFallbackInstance(); |
| private boolean enabled = true; |
| |
| KeyboardPreviewDrawable( |
| Resources resources, KeyboardLayout keyboardLayout, KeyboardSpecification specification) { |
| this.resources = resources; |
| this.keyboardLayout = keyboardLayout; |
| this.specification = specification; |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| Rect bounds = getBounds(); |
| if (bounds.isEmpty()) { |
| return; |
| } |
| |
| // Look up cache. |
| BitmapCache cache = BitmapCache.getInstance(); |
| Bitmap bitmap = cache.get(keyboardLayout, bounds.width(), bounds.height(), skin); |
| if (bitmap == null) { |
| bitmap = createBitmap( |
| resources, specification, bounds.width(), bounds.height(), |
| resources.getDimensionPixelSize(R.dimen.pref_inputstyle_reference_width), skin); |
| if (bitmap != null) { |
| cache.put(keyboardLayout, skin, bitmap); |
| } |
| } |
| |
| canvas.drawBitmap(bitmap, bounds.left, bounds.top, paint); |
| |
| if (!enabled) { |
| // To represent disabling, gray it out. |
| Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| paint.setColor(0x80000000); |
| canvas.drawRect(bounds, paint); |
| } |
| } |
| |
| /** |
| * @param width width of returned {@code Bitmap} |
| * @param height height of returned {@code Bitmap} |
| * @param virtualWidth virtual width of keyboard. This value is used when rendering. |
| * virtualHeight is internally calculated based on given arguments keeping aspect ratio. |
| */ |
| @Nullable |
| private static Bitmap createBitmap( |
| Resources resources, KeyboardSpecification specification, int width, int height, |
| int virtualWidth, Skin skin) { |
| Preconditions.checkNotNull(skin); |
| // Scaling is required because some icons are draw with specified fixed size. |
| float scale = width / (float) virtualWidth; |
| int virtualHeight = (int) (height / scale); |
| Keyboard keyboard = getParsedKeyboard(resources, specification, virtualWidth, virtualHeight); |
| if (keyboard == null) { |
| return null; |
| } |
| |
| Bitmap bitmap = MozcUtil.createBitmap(width, height, Config.ARGB_8888); |
| Canvas canvas = new Canvas(bitmap); |
| canvas.scale(scale, scale); |
| DrawableCache drawableCache = new DrawableCache(resources); |
| drawableCache.setSkin(skin); |
| |
| // Fill background. |
| { |
| Drawable keyboardBackground = |
| skin.windowBackgroundDrawable.getConstantState().newDrawable(); |
| keyboardBackground.setBounds(0, 0, virtualWidth, virtualHeight); |
| keyboardBackground.draw(canvas); |
| } |
| |
| // Draw keyboard layout. |
| { |
| BackgroundDrawableFactory backgroundDrawableFactory = |
| new BackgroundDrawableFactory(resources); |
| backgroundDrawableFactory.setSkin(skin); |
| KeyboardViewBackgroundSurface backgroundSurface = |
| new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache); |
| backgroundSurface.requestUpdateKeyboard(keyboard, Collections.<MetaState>emptySet()); |
| backgroundSurface.requestUpdateSize(virtualWidth, virtualHeight); |
| backgroundSurface.update(); |
| backgroundSurface.draw(canvas); |
| backgroundSurface.reset(); // Release the background bitmap and its canvas. |
| } |
| |
| return bitmap; |
| } |
| |
| /** Create a Keyboard instance which fits the current bitmap. */ |
| @Nullable |
| private static Keyboard getParsedKeyboard( |
| Resources resources, KeyboardSpecification specification, int width, int height) { |
| KeyboardParser parser = new KeyboardParser( |
| resources, width, height, specification); |
| try { |
| return parser.parseKeyboard(); |
| } catch (XmlPullParserException e) { |
| MozcLog.e("Failed to parse keyboard layout: ", e); |
| } catch (IOException e) { |
| MozcLog.e("Failed to parse keyboard layout: ", e); |
| } |
| |
| return null; |
| } |
| |
| void setSkin(Skin skin) { |
| this.skin = skin; |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| // Do nothing. |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter cf) { |
| // Do nothing. |
| } |
| |
| // Hard coded size to adapt the old implementation. |
| @Override |
| public int getIntrinsicWidth() { |
| return resources.getDimensionPixelSize(R.dimen.pref_inputstyle_image_width); |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return resources.getDimensionPixelSize(R.dimen.pref_inputstyle_image_width) * 348 / 480; |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return true; |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] state) { |
| boolean enabled = isEnabled(state); |
| if (this.enabled == enabled) { |
| return false; |
| } |
| this.enabled = enabled; |
| invalidateSelf(); |
| return true; |
| } |
| |
| private static boolean isEnabled(int[] state) { |
| for (int i = 0; i < state.length; ++i) { |
| if (state[i] == android.R.attr.state_enabled) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |