blob: 6b875f907473cc93c6b22422b97c62a7e9b1d7c3 [file] [log] [blame]
// 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;
import org.mozc.android.inputmethod.japanese.InOutAnimatedFrameLayout.VisibilityChangeListener;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
import org.mozc.android.inputmethod.japanese.view.Skin;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.res.Resources;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.TranslateAnimation;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
/**
* Manages candidate views (floating, on-keyboard).
*/
class CandidateViewManager implements MemoryManageable {
/** Listener interface to handle the height change of a keyboard candidate view. */
public interface KeyboardCandidateViewHeightListener {
public void onExpanded();
public void onCollapse();
}
private static class ClearCandidateAnimationListener implements Animation.AnimationListener {
private final CandidateView candidateView;
public ClearCandidateAnimationListener(CandidateView candidateView) {
this.candidateView = Preconditions.checkNotNull(candidateView);
}
@Override
public void onAnimationEnd(Animation animation) {
candidateView.update(EMPTY_COMMAND);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
}
}
/** {@link CandidateMode#FLOATING} is only available on Lollipop or later. */
private enum CandidateMode {
KEYBOARD, NUMBER, FLOATING,
}
private static final Animation NO_ANIMATION = new Animation() {};
@VisibleForTesting static final Command EMPTY_COMMAND = Command.getDefaultInstance();
@VisibleForTesting final CandidateView keyboardCandidateView;
@VisibleForTesting final FloatingCandidateView floatingCandidateView;
/**
* SymbolInputView which number candidate view belongs to is created lazily.
* Therefore number candidate view is not accessible when CandidateViewManager is instantiated.
*/
@VisibleForTesting Optional<CandidateView> numberCandidateView = Optional.absent();
private Optional<KeyboardCandidateViewHeightListener> keyboardCandidateViewHeightListener =
Optional.absent();
/**
* Current active candidate view.
* {@link CandidateMode#FLOATING} is only available on Lollipop or later.
*/
private CandidateMode candidateMode = CandidateMode.KEYBOARD;
/** Cache of {@link EditorInfo} instance to switch candidate views. */
private EditorInfo editorInfo = new EditorInfo();
/** Cache of {@link Skin} instance to switch candidate views. */
private Skin skin = Skin.getFallbackInstance();
/** Cache of candidate text size. */
private float candidateTextSize;
/** Cache of description text size. */
private float descriptionTextSize;
/** Cache of {@link ViewEventListener}. */
private Optional<ViewEventListener> viewEventListener = Optional.absent();
/** Cache of {@link VisibilityChangeListener}. */
private Optional<VisibilityChangeListener> onVisibilityChangeListener = Optional.absent();
/**
* True if extracted mode (== fullscreen mode) is activated.
* <p>
* On extracted mode, floating candidate should be disabled in order to show extracted view
* in the screen.
*/
private boolean isExtractedMode = false;
private boolean allowFloatingMode = false;
private boolean narrowMode = false;
/**
* Cache of {@link CursorAnchorInfo} instance to switch candidate views.
* This field is null if and only-if Floating candidate view is NOT available, so we don't mark
* this value as {code @Nullable} since this field should NOT be used in that situation.
*/
private CursorAnchorInfo cursorAnchorInfo;
private Animation numberCandidateViewInAnimation = NO_ANIMATION;
private Animation numberCandidateViewOutAnimation = NO_ANIMATION;
@SuppressLint("NewApi")
public CandidateViewManager(
CandidateView keyboardCandidateView, FloatingCandidateView floatingCandidateView) {
this.keyboardCandidateView = Preconditions.checkNotNull(keyboardCandidateView);
this.floatingCandidateView = Preconditions.checkNotNull(floatingCandidateView);
if (FloatingCandidateView.isAvailable()) {
cursorAnchorInfo = new CursorAnchorInfo.Builder().build();
}
keyboardCandidateView.setOutAnimationListener(
new ClearCandidateAnimationListener(keyboardCandidateView));
}
public void setNumberCandidateView(CandidateView numberCandidateView) {
this.numberCandidateView = Optional.of(numberCandidateView);
numberCandidateView.setSkin(skin);
numberCandidateView.enableFoldButton(true);
numberCandidateView.setInAnimation(numberCandidateViewInAnimation);
numberCandidateView.setOutAnimation(numberCandidateViewOutAnimation);
numberCandidateView.setOutAnimationListener(
new ClearCandidateAnimationListener(numberCandidateView));
if (candidateTextSize > 0 && descriptionTextSize > 0) {
numberCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
}
if (viewEventListener.isPresent()) {
numberCandidateView.setViewEventListener(viewEventListener.get());
}
numberCandidateView.setOnVisibilityChangeListener(onVisibilityChangeListener.orNull());
}
/**
* Updates the candidate views by {@code outCommand} and may invoke some animations.
* <p>
* On-keyboard candidate view may animate and the animation listener may be invoked.
*/
public void update(Command outCommand) {
updateInternal(Preconditions.checkNotNull(outCommand), true);
}
private void updateWithoutAnimation(Command outCommand) {
updateInternal(outCommand, false);
}
private void updateInternal(Command outCommand, boolean withAnimation) {
if (candidateMode == CandidateMode.FLOATING) {
floatingCandidateView.setCandidates(outCommand);
return;
}
Preconditions.checkState(
candidateMode == CandidateMode.KEYBOARD
|| (candidateMode == CandidateMode.NUMBER && numberCandidateView.isPresent()));
CandidateView candidateView = (candidateMode == CandidateMode.KEYBOARD)
? keyboardCandidateView : numberCandidateView.get();
if (withAnimation) {
if (hasCandidates(outCommand)) {
candidateView.update(outCommand);
// Call CandidateView#update only if there are some candidates in the output.
// In such case the candidate view will clear its canvas.
startKeyboardCandidateViewInAnimation();
} else {
// We don't call update method here and clear candidates at the end of this animation,
// because it will clear the view's contents during the animation.
startKeyboardCandidateViewOutAnimation();
}
} else {
candidateView.update(outCommand);
if (hasCandidates(outCommand)) {
candidateView.setVisibility(View.VISIBLE);
} else {
candidateView.setVisibility(View.GONE);
}
}
}
public void setOnVisibilityChangeListener(Optional<VisibilityChangeListener> listener) {
this.onVisibilityChangeListener = Preconditions.checkNotNull(listener);
keyboardCandidateView.setOnVisibilityChangeListener(listener.orNull());
if (numberCandidateView.isPresent()) {
numberCandidateView.get().setOnVisibilityChangeListener(listener.orNull());
}
}
/**
* Enables/Disables a floating candidate view.
* <p>
* This method turned floating mode on if it is preferred.
* The floating candidate view is only available on Lollipop or later.
*/
private void updateCandiadateWindowActivation() {
boolean floatingMode = narrowMode && allowFloatingMode && !isExtractedMode;
if (floatingMode == (candidateMode == CandidateMode.FLOATING)) {
return;
}
// Clears candidates on the current candidate window.
updateWithoutAnimation(EMPTY_COMMAND);
// Updates the other candidate view.
candidateMode = floatingMode ? CandidateMode.FLOATING : CandidateMode.KEYBOARD;
updateWithoutAnimation(EMPTY_COMMAND);
setEditorInfo(editorInfo);
if (FloatingCandidateView.isAvailable()) {
setCursorAnchorInfo(cursorAnchorInfo);
}
// In order to show extracted view correctly, make the visibility GONE when it is not activated.
floatingCandidateView.setVisibility(floatingMode ? View.VISIBLE : View.GONE);
}
public void setAllowFloatingMode(boolean allowFloatingMode) {
Preconditions.checkArgument(!allowFloatingMode || FloatingCandidateView.isAvailable());
this.allowFloatingMode = allowFloatingMode;
updateCandiadateWindowActivation();
}
public void setNarrowMode(boolean narrowMode) {
this.narrowMode = narrowMode;
keyboardCandidateView.enableFoldButton(!narrowMode);
updateCandiadateWindowActivation();
}
public void setNumberMode(boolean numberMode) {
CandidateMode nextMode = numberMode ? CandidateMode.NUMBER : CandidateMode.KEYBOARD;
if (candidateMode == nextMode) {
return;
}
if (nextMode == CandidateMode.NUMBER) {
// Hide keyboard candidate view since it is higher than symbol view.
updateWithoutAnimation(EMPTY_COMMAND);
}
candidateMode = nextMode;
// Set empty command in order to clear the candidates which have been registered into the next
// view. Otherwise such candidates (typically they are obsolete) are shown unexpectedly.
// TODO(hsumita): Revisit when Mozc server returns candidates for SWITCH_INPUT_MODE command.
updateWithoutAnimation(EMPTY_COMMAND);
}
public void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) {
this.candidateTextSize = candidateTextSize;
this.descriptionTextSize = descriptionTextSize;
if (numberCandidateView.isPresent()) {
numberCandidateView.get().setCandidateTextDimension(
candidateTextSize, descriptionTextSize);
} else {
keyboardCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
}
}
public void setInputFrameFoldButtonChecked(boolean isChecked) {
switch (candidateMode) {
case KEYBOARD:
keyboardCandidateView.setInputFrameFoldButtonChecked(isChecked);
break;
case NUMBER:
numberCandidateView.get().setInputFrameFoldButtonChecked(isChecked);
break;
case FLOATING:
throw new IllegalStateException("Fold button is not available on floating mode.");
}
}
public void setEditorInfo(EditorInfo info) {
this.editorInfo = Preconditions.checkNotNull(info);
if (candidateMode == CandidateMode.FLOATING) {
floatingCandidateView.setEditorInfo(info);
}
}
@TargetApi(21)
public void setCursorAnchorInfo(CursorAnchorInfo info) {
this.cursorAnchorInfo = Preconditions.checkNotNull(info);
if (candidateMode == CandidateMode.FLOATING) {
floatingCandidateView.setCursorAnchorInfo(info);
}
}
public void setSkin(Skin skin) {
this.skin = Preconditions.checkNotNull(skin);
keyboardCandidateView.setSkin(skin);
if (numberCandidateView.isPresent()) {
numberCandidateView.get().setSkin(skin);
}
}
public void setEventListener(ViewEventListener viewEventListener,
KeyboardCandidateViewHeightListener hightListener) {
this.viewEventListener = Optional.of(viewEventListener);
this.keyboardCandidateViewHeightListener = Optional.of(hightListener);
keyboardCandidateView.setViewEventListener(viewEventListener);
floatingCandidateView.setViewEventListener(viewEventListener);
if (numberCandidateView.isPresent()) {
numberCandidateView.get().setViewEventListener(viewEventListener);
}
}
/**
* Set true if extracted mode (== fullscreen mode) is activated.
*/
public void setExtractedMode(boolean isExtractedMode) {
this.isExtractedMode = isExtractedMode;
updateCandiadateWindowActivation();
}
public void setHardwareCompositionMode(CompositionMode mode) {
if (isFloatingMode()) {
floatingCandidateView.setCompositionMode(mode);
}
}
public void reset() {
keyboardCandidateView.clearAnimation();
keyboardCandidateView.setVisibility(View.GONE);
keyboardCandidateView.reset();
if (numberCandidateView.isPresent()) {
numberCandidateView.get().clearAnimation();
numberCandidateView.get().setVisibility(View.GONE);
numberCandidateView.get().reset();
}
candidateMode = CandidateMode.KEYBOARD;
floatingCandidateView.setVisibility(View.GONE);
}
public void resetHeightDependingComponents(
Resources resources, int windowHeight, int inputFrameHeight) {
Preconditions.checkNotNull(resources);
int keyboardCandidateViewHeight = windowHeight - inputFrameHeight;
long duration = resources.getInteger(R.integer.candidate_frame_transition_duration);
float fromAlpha = 0.0f;
float toAlpha = 1.0f;
keyboardCandidateView.setInAnimation(createKeyboardCandidateViewTransitionAnimation(
keyboardCandidateViewHeight, 0, fromAlpha, toAlpha, duration));
keyboardCandidateView.setOutAnimation(createKeyboardCandidateViewTransitionAnimation(
0, keyboardCandidateViewHeight, toAlpha, fromAlpha, duration));
int numberCandidateViewHeight = resources.getDimensionPixelSize(R.dimen.button_frame_height);
numberCandidateViewInAnimation = createKeyboardCandidateViewTransitionAnimation(
numberCandidateViewHeight, 0, fromAlpha, toAlpha, duration);
numberCandidateViewOutAnimation = createKeyboardCandidateViewTransitionAnimation(
0, numberCandidateViewHeight, toAlpha, fromAlpha, duration);
if (numberCandidateView.isPresent()) {
numberCandidateView.get().setInAnimation(numberCandidateViewInAnimation);
numberCandidateView.get().setOutAnimation(numberCandidateViewOutAnimation);
}
}
public boolean isKeyboardCandidateViewVisible() {
return keyboardCandidateView.getVisibility() == View.VISIBLE;
}
private static Animation createKeyboardCandidateViewTransitionAnimation(
int fromY, int toY, float fromAlpha, float toAlpha, long duration) {
AnimationSet animation = new AnimationSet(false);
animation.setDuration(duration);
AlphaAnimation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha);
alphaAnimation.setDuration(duration);
animation.addAnimation(alphaAnimation);
TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, fromY, toY);
translateAnimation.setInterpolator(new DecelerateInterpolator());
translateAnimation.setDuration(duration);
animation.addAnimation(translateAnimation);
return animation;
}
private void startKeyboardCandidateViewInAnimation() {
switch (candidateMode) {
case KEYBOARD:
keyboardCandidateView.startInAnimation();
if (keyboardCandidateViewHeightListener.isPresent()) {
keyboardCandidateViewHeightListener.get().onExpanded();
}
break;
case NUMBER:
numberCandidateView.get().startInAnimation();
break;
case FLOATING:
throw new IllegalStateException("Floating mode doesn't support in-animation.");
}
}
private void startKeyboardCandidateViewOutAnimation() {
switch (candidateMode) {
case KEYBOARD:
if (keyboardCandidateViewHeightListener.isPresent()) {
keyboardCandidateViewHeightListener.get().onCollapse();
}
keyboardCandidateView.startOutAnimation();
break;
case NUMBER:
numberCandidateView.get().startOutAnimation();
break;
case FLOATING:
throw new IllegalStateException("Floating mode doesn't support out-animation.");
}
}
private static boolean hasCandidates(Command command) {
return command.getOutput().getAllCandidateWords().getCandidatesCount() > 0;
}
@Override
public void trimMemory() {
keyboardCandidateView.trimMemory();
if (numberCandidateView.isPresent()) {
numberCandidateView.get().trimMemory();
}
}
public boolean isFloatingMode() {
return candidateMode == CandidateMode.FLOATING;
}
}