blob: 53630481db453f50244b08fe06f5d35bee0202e4 [file] [log] [blame]
// Copyright 2010-2015, 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.keyboard;
import org.mozc.android.inputmethod.japanese.MemoryManageable;
import org.mozc.android.inputmethod.japanese.MozcUtil;
import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction;
import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState;
import org.mozc.android.inputmethod.japanese.view.DrawableCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Region.Op;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Implementation of the background surface for {@link KeyboardView}.
* This class takes care of double-buffering and diff-only-updating to improve the rendering
* performance for better user experiences.
*
* <p>An example usage of this class is as follows:
* <pre>{@code
* KeyboardViewBackgroundSurface backgroundSurface = new KeyboardViewBackgroundSurface();
* // Request to set the keyboard and its meta state.
* backgroundSurface.requestUpdateKeyboard(keyboard, metaState);
*
* // Request to set the size of the image.
* backgroundSurface.requestUpdateSize(viewWidth, viewHeight);
*
* // Add requests to update keys' images. Note: the actual update is not done by requests.
* backgroundSurface.requestUpdateKey(key1, Flick.Direction.CENTER);
* backgroundSurface.requestUpdateKey(key2, Flick.Direction.CENTER);
* backgroundSurface.requestUpdateKey(key3, Flick.Direction.CENTER);
*
* // Many update requests for the same key is valid.
* backgroundSurface.requestUpdateKey(key1, Flick.Direction.LEFT);
* backgroundSurface.requestUpdateKey(key3, Flick.Direction.RIGHT);
*
* // Set flick direction to null means the key is released.
* backgroundSurface.requestUpdateKey(key1, Optional.<Direction>absent());
*
* // Actual update is done below.
* backgroundSurface.update();
*
* // Render the image to canvas.
* backgroundSurface.draw(canvas);
* }</pre>
*
* This class is exposed as public in order to mock for testing purpose.
*
*/
@VisibleForTesting public class KeyboardViewBackgroundSurface implements MemoryManageable {
/**
* A simple rendering related utilities for keyboard rendering.
*/
@VisibleForTesting interface SurfaceCanvas {
/**
* Clears a rectangle region of
* {@code (x, y) -- (x + width, y + height)}.
*/
void clearRegion(int x, int y, int width, int height);
/**
* Draws the given {@code drawable} in the given region with scaling.
* Does nothing if drawable is {@code null}.
*/
void drawDrawable(Drawable drawable, int x, int y, int width, int height);
/**
* Draws the given {@code drawable} at the center of the given region,
* and this method scales the drawable to fit the given {@code height}, but it scales
* horizontal direction to keep its aspect ratio.
*/
void drawDrawableAtCenterWithKeepAspectRatio(
Drawable drawable, int x, int y, int width, int height);
}
private static class SurfaceCanvasImpl implements SurfaceCanvas {
/**
* Clear the canvas by transparent color.
*/
private static final int CLEAR_COLOR = 0x00000000;
private final Canvas canvas;
SurfaceCanvasImpl(Canvas canvas) {
this.canvas = Preconditions.checkNotNull(canvas);
}
@Override
public void clearRegion(int x, int y, int width, int height) {
int saveCount = canvas.save();
try {
canvas.clipRect(x, y, x + width, y + height, Op.REPLACE);
canvas.drawColor(CLEAR_COLOR, PorterDuff.Mode.CLEAR);
} finally {
canvas.restoreToCount(saveCount);
}
}
@Override
public void drawDrawable(@Nullable Drawable drawable, int x, int y, int width, int height) {
if (drawable == null) {
return;
}
drawDrawableInternal(drawable, x, y, width, height,
height / (float) drawable.getIntrinsicHeight());
}
@Override
public void drawDrawableAtCenterWithKeepAspectRatio(
@Nullable Drawable drawable, int x, int y, int width, int height) {
if (drawable == null) {
return;
}
float scale = Math.min(width / (float) drawable.getIntrinsicWidth(),
height / (float) drawable.getIntrinsicHeight());
int scaledWidth = Math.round(drawable.getIntrinsicWidth() * scale);
int scaledHeight = Math.round(drawable.getIntrinsicHeight() * scale);
drawDrawableInternal(drawable, x + (width - scaledWidth) / 2, y + (height - scaledHeight) / 2,
scaledWidth, scaledHeight, scale);
}
private void drawDrawableInternal(Drawable drawable, int x, int y, int width, int height,
float scale) {
int saveCount = canvas.save();
try {
canvas.translate(x, y);
if (drawable.getCurrent() instanceof PictureDrawable) {
canvas.scale(scale, scale);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
} else {
drawable.setBounds(0, 0, width, height);
}
drawable.draw(canvas);
} finally {
canvas.restoreToCount(saveCount);
}
}
}
/**
* Mapping table from Flick.Direction to appropriate DrawableType.
*/
private static final Map<Flick.Direction, DrawableType> FLICK_DRAWABLE_TYPE_MAP;
static {
Map<Flick.Direction, DrawableType> map =
new EnumMap<Flick.Direction, DrawableType>(Flick.Direction.class);
map.put(Flick.Direction.CENTER, DrawableType.TWELVEKEYS_CENTER_FLICK);
map.put(Flick.Direction.LEFT, DrawableType.TWELVEKEYS_LEFT_FLICK);
map.put(Flick.Direction.UP, DrawableType.TWELVEKEYS_UP_FLICK);
map.put(Flick.Direction.RIGHT, DrawableType.TWELVEKEYS_RIGHT_FLICK);
map.put(Flick.Direction.DOWN, DrawableType.TWELVEKEYS_DOWN_FLICK);
FLICK_DRAWABLE_TYPE_MAP = Collections.unmodifiableMap(map);
}
// Note: This array is mutable unfortunately due to language spec of Java,
// but mustn't be overwritten by any methods, and should be treated as a constant value.
private static final int[] STATE_PRESSED = { android.R.attr.state_pressed };
private static final int[] STATE_DEFAULT = {};
private final BackgroundDrawableFactory backgroundDrawableFactory;
private final DrawableCache drawableCache;
// The current image and its rendering object.
private Optional<Bitmap> surfaceBitmap = Optional.absent();
@VisibleForTesting Optional<SurfaceCanvas> surfaceCanvas = Optional.absent();
// Width and height this background surface is to be in pixels
private int requestedWidth;
private int requestedHeight;
private boolean fullUpdateRequested;
private Optional<Keyboard> requestedKeyboard = Optional.absent();
private Set<MetaState> requestedMetaState = Collections.emptySet();
public KeyboardViewBackgroundSurface(
BackgroundDrawableFactory backgroundDrawableFactory, DrawableCache drawableCache) {
this.backgroundDrawableFactory = Preconditions.checkNotNull(backgroundDrawableFactory);
this.drawableCache = Preconditions.checkNotNull(drawableCache);
}
/**
* A set of pending keys to be updated.
* Note that the null value of Flick.Direction means the Key is released. This is a hack to
* represents all the states of released, pressed(CENTER), and flicks to four directions
* in a single value.
*
* TODO(hidehiko): We should have direction state in somewhere else, not in the pending requests,
* so that we can re-render the correct image even if we have any sequence of requests.
*/
@VisibleForTesting
final Map<Key, Optional<Flick.Direction>> pendingKeys =
new HashMap<Key, Optional<Flick.Direction>>();
/**
* Resets and release the current image this instance holds.
* By this method's invocation, we assume all key's directions are also reset.
*/
public void reset() {
if (surfaceBitmap.isPresent()) {
surfaceBitmap.get().recycle();
surfaceBitmap = Optional.absent();
}
surfaceCanvas = Optional.absent();
pendingKeys.clear();
}
/**
* Adds the given {@code key} to the pending key set to render it lazily.
* In other words, the key isn't rendered until {@link #update()} is invoked.
*/
public void requestUpdateKey(Key key, Optional<Flick.Direction> flickDirection) {
if (!Preconditions.checkNotNull(key).isSpacer()) {
pendingKeys.put(key, Preconditions.checkNotNull(flickDirection));
}
}
/**
* Adds a task to update the size of the image. The actual update is done lazily (when
* {@link #update()} is invoked).
*/
public void requestUpdateSize(int width, int height) {
requestedWidth = width;
requestedHeight = height;
}
/**
* Resets the {@code keyboard} and {@code metaState} to be rendered on this surface.
* This also cancels all key's direction for now.
*/
public void requestUpdateKeyboard(Keyboard keyboard, Set<MetaState> metaState) {
requestedKeyboard = Optional.of(keyboard);
requestedMetaState = Preconditions.checkNotNull(metaState);
fullUpdateRequested = true;
// We set all keyboard update request here, and it overwrites the current pending key update
// requests.
pendingKeys.clear();
}
/**
* Requests new {@code metaState} to be rendered on this surface.
*
* This method requests redraw only the keys which are required to be redrawn according to
* metastate's change.
* This also cancels such keys' direction for now.
*/
public void requestMetaState(Set<MetaState> newMetaState) {
Preconditions.checkNotNull(newMetaState);
Set<MetaState> previousMetaState = requestedMetaState;
requestedMetaState = newMetaState;
if (newMetaState.equals(previousMetaState) || !requestedKeyboard.isPresent()) {
return;
}
// Update only the keys which should update corresponding KeyState based on given metaState.
for (Row row : requestedKeyboard.get().getRowList()) {
for (Key key : row.getKeyList()) {
KeyState previousKeyState = key.getKeyState(previousMetaState).orNull();
KeyState newKeyState = key.getKeyState(newMetaState).orNull();
// Intentionally using != operator instead of equals method.
// - Faster than full-spec equals method.
// - The values of Optional which are returned by Key#getKeyState are always
// the same object so equals is overkill.
if (previousKeyState != newKeyState) {
// Request to draw the key.
// pendingKeys may have already contained corresponding key but overwrite here.
pendingKeys.put(key, Optional.<Direction>absent());
}
}
}
}
/**
* Actually updates the image this instance holds based on pending update requests.
*/
public void update() {
if (isInitializationNeeded()) {
initialize();
// The bitmap has been re-created, so it is necessary to re-render all of the keyboard.
fullUpdateRequested = true;
}
if (!requestedKeyboard.isPresent()) {
// We have nothing to do.
return;
}
// A new keyboard or meta state is set, so we need re-render all of the keyboard.
if (fullUpdateRequested) {
clearCanvas();
renderKeyboard();
fullUpdateRequested = false;
} else {
renderPendingKeys();
}
// Clean up state.
pendingKeys.clear();
}
/**
* Returns {@code true} iff (re-)initialization is needed.
*/
@VisibleForTesting boolean isInitializationNeeded() {
// We need to re-create background buffer if
// - no initialization is done (after construction or reset).
// - the size of view has been changed.
Optional<Bitmap> bitmap = surfaceBitmap;
return (!bitmap.isPresent())
|| (bitmap.get().getWidth() != requestedWidth)
|| (bitmap.get().getHeight() != requestedHeight);
}
void initialize() {
if (surfaceBitmap.isPresent()) {
surfaceBitmap.get().recycle();
}
surfaceBitmap = Optional.of(
MozcUtil.createBitmap(requestedWidth, requestedHeight, Bitmap.Config.ARGB_8888));
surfaceCanvas = Optional.<SurfaceCanvas>of(
new SurfaceCanvasImpl(new Canvas(surfaceBitmap.get())));
}
/**
* Draws the current keyboard image to the given canvas.
* It is required to invoke update() method before this method's invocation.
*/
public void draw(Canvas canvas) {
canvas.drawBitmap(
surfaceBitmap.orNull(), requestedKeyboard.get().contentLeft,
requestedKeyboard.get().contentTop, null);
}
private void clearCanvas() {
if (surfaceBitmap.isPresent() && surfaceCanvas.isPresent()) {
Bitmap bitmap = surfaceBitmap.get();
surfaceCanvas.get().clearRegion(0, 0, bitmap.getWidth(), bitmap.getHeight());
}
}
private void renderKey(Key key, Optional<Flick.Direction> flickDirection) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(flickDirection);
Preconditions.checkState(surfaceCanvas.isPresent());
Preconditions.checkState(requestedKeyboard.isPresent());
SurfaceCanvas canvas = surfaceCanvas.get();
int horizontalGap = key.getHorizontalGap();
int leftGap = horizontalGap / 2;
boolean isPressed = flickDirection.isPresent();
// We split the gap to both sides evenly.
int x = key.getX() + leftGap - requestedKeyboard.get().contentLeft;
int y = key.getY() - requestedKeyboard.get().contentTop;
// Given width/height for the key.
// The icon is drawn inside the width/height.
int givenWidth = key.getWidth() - horizontalGap;
int givenHeight = key.getHeight();
canvas.drawDrawable(getKeyBackground(key, isPressed).orNull(), x, y, givenWidth, givenHeight);
Optional<KeyEntity> keyEntity =
getKeyEntityForRendering(key, requestedMetaState, flickDirection);
if (flickDirection.isPresent() && keyEntity.isPresent()
&& keyEntity.get().isFlickHighlightEnabled()
&& KeyEventContext.getKeyEntity(key, requestedMetaState, flickDirection)
.equals(keyEntity)) {
DrawableType drawableType = FLICK_DRAWABLE_TYPE_MAP.get(flickDirection.get());
Drawable backgroundDrawable = (drawableType != null)
? backgroundDrawableFactory.getDrawable(drawableType) : null;
canvas.drawDrawable(backgroundDrawable, x, y, givenWidth, givenHeight);
}
int horizontalPadding = keyEntity.isPresent() ? keyEntity.get().getHorizontalPadding() : 0;
int verticalPadding = keyEntity.isPresent() ? keyEntity.get().getVerticalPadding() : 0;
int iconWidth = keyEntity.isPresent() ? keyEntity.get().getIconWidth() : givenWidth;
int iconHeight = keyEntity.isPresent() ? keyEntity.get().getIconHeight() : givenHeight;
iconWidth = Math.min(iconWidth, givenWidth - horizontalPadding * 2);
iconHeight = Math.min(iconHeight, givenHeight - verticalPadding * 2);
canvas.drawDrawableAtCenterWithKeepAspectRatio(
getKeyIcon(drawableCache, keyEntity, isPressed).orNull(),
x + (givenWidth - iconWidth) / 2, y + (givenHeight - iconHeight) / 2,
iconWidth, iconHeight);
}
/**
* Draws all keys in the keyboard.
*/
private void renderKeyboard() {
for (Row row : requestedKeyboard.get().getRowList()) {
for (Key key : row.getKeyList()) {
Optional<Direction> direction = pendingKeys.get(key);
if (direction == null) {
renderKey(key, Optional.<Direction>absent());
} else {
renderKey(key, direction);
}
}
}
}
private void renderPendingKeys() {
Preconditions.checkState(surfaceCanvas.isPresent());
// The canvas object is used many times, so cache it on local stack.
SurfaceCanvas canvas = surfaceCanvas.get();
int offsetX = requestedKeyboard.get().contentLeft;
int offsetY = requestedKeyboard.get().contentTop;
for (Map.Entry<Key, Optional<Flick.Direction>> entry : pendingKeys.entrySet()) {
Key key = entry.getKey();
// Clear the key's region and then draw the key there.
canvas.clearRegion(key.getX() - offsetX, key.getY() - offsetY, key.getWidth(),
key.getHeight());
renderKey(key, entry.getValue());
}
}
/**
* Returns KeyEntity which should be used for the {@code key}'s rendering with the given state.
* {@code Optional.absent()} will be returned if we don't need to render the key.
*/
@VisibleForTesting static Optional<KeyEntity> getKeyEntityForRendering(
Key key, Set<MetaState> metaState, Optional<Flick.Direction> flickDirection) {
if (flickDirection.isPresent()) {
// If the key is under flick state, check if there is corresponding key entity for the
// direction first.
Optional<KeyEntity> keyEntity = KeyEventContext.getKeyEntity(key, metaState, flickDirection);
if (keyEntity.isPresent()) {
return keyEntity;
}
}
// Use CENTER direction for released key, or a key flicked to a unsupported direction.
return KeyEventContext.getKeyEntity(key, metaState, Optional.of(Flick.Direction.CENTER));
}
private static Optional<Drawable> setDrawableState(
Optional<Drawable> drawable, boolean isPressed) {
if (drawable.isPresent()) {
drawable.get().setState(isPressed ? STATE_PRESSED : STATE_DEFAULT);
}
return drawable;
}
/**
* Returns {@code Drawable} for the given key's background with setting appropriate state.
*/
@VisibleForTesting Optional<Drawable> getKeyBackground(Key key, boolean isPressed) {
Preconditions.checkNotNull(key);
return setDrawableState(
Optional.of(backgroundDrawableFactory.getDrawable(key.getKeyBackgroundDrawableType())),
isPressed);
}
/**
* Returns {@code Drawable} for the given key's icon with setting appropriate state.
*/
@VisibleForTesting static Optional<Drawable> getKeyIcon(
DrawableCache drawableCache, Optional<KeyEntity> keyEntity, boolean isPressed) {
if (!keyEntity.isPresent()) {
return Optional.absent();
}
return setDrawableState(
drawableCache.getDrawable(keyEntity.get().getKeyIconResourceId()), isPressed);
}
@Override
public void trimMemory() {
// drawableCache is not cleared here. It's done in KeyboardView where it is created.
reset();
}
}