blob: 70037e5fe697ec274818eb2aac726ff110f2edcf [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.ui;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand;
import org.mozc.android.inputmethod.japanese.resources.R;
import org.mozc.android.inputmethod.japanese.view.MozcImageView;
import org.mozc.android.inputmethod.japanese.view.Skin;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationSet;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.inputmethod.CursorAnchorInfo;
/**
* Draws mode indicator for floating candidate window.
*/
@TargetApi(21)
public class FloatingModeIndicator {
/** The message to hide the mode indicator. */
@VisibleForTesting static final int HIDE_MODE_INDICATOR = 0;
private class OutAnimationListener implements AnimationListener {
@Override
public void onAnimationEnd(Animation animation) {
if (!isVisible) {
popup.getContentView().setVisibility(View.GONE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
}
}
private class ModeIndicatorMessageCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == HIDE_MODE_INDICATOR) {
hide();
}
return true;
}
}
@VisibleForTesting final Handler handler;
@VisibleForTesting final PopUpLayouter<MozcImageView> popup;
private final View parentView;
private final Drawable kanaIndicatorDrawable;
private final Drawable abcIndicatorDrawable;
private final int indicatorSize;
private final int verticalMargin;
private final Rect drawRect;
private final Animation inAnimation;
private final Animation outAnimation;
private final int displayTime;
private CursorAnchorInfo cursorAnchorInfo = new CursorAnchorInfo.Builder().build();
/** True if the mode indicator is shown and is not hiding. */
private boolean isVisible = false;
private boolean hasComposition = false;
public FloatingModeIndicator(View parent) {
parentView = Preconditions.checkNotNull(parent);
handler = new Handler(Looper.getMainLooper(), new ModeIndicatorMessageCallback());
Context context = parent.getContext();
Resources resources = context.getResources();
Skin skin = Skin.getFallbackInstance();
kanaIndicatorDrawable =
skin.getDrawable(resources, R.raw.floating_mode_indicator__kana_normal);
abcIndicatorDrawable =
skin.getDrawable(resources, R.raw.floating_mode_indicator__alphabet_normal);
indicatorSize =
resources.getDimensionPixelSize(R.dimen.floating_mode_indicator_size);
verticalMargin =
resources.getDimensionPixelSize(R.dimen.floating_mode_indicator_vertical_margin);
displayTime = resources.getInteger(R.integer.floating_mode_indicator_display_time);
MozcImageView contentView = new MozcImageView(context);
contentView.setVisibility(View.GONE);
contentView.setImageDrawable(kanaIndicatorDrawable);
popup = new PopUpLayouter<MozcImageView>(parentView, contentView);
inAnimation = createInAnimation(resources, indicatorSize / 2f, verticalMargin);
outAnimation = createOutAnimation(resources, indicatorSize / 2f, verticalMargin);
drawRect = new Rect(0, 0, indicatorSize, indicatorSize);
}
public void setCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
this.cursorAnchorInfo = Preconditions.checkNotNull(cursorAnchorInfo);
}
private void updateDrawRect() {
float[] cursorPosition = new float[] {
cursorAnchorInfo.getInsertionMarkerHorizontal(),
cursorAnchorInfo.getInsertionMarkerBottom()
};
cursorAnchorInfo.getMatrix().mapPoints(cursorPosition);
int location[] = new int[2];
parentView.getLocationOnScreen(location);
int left = Math.round(cursorPosition[0] - indicatorSize / 2) - location[0];
int top = Math.round(cursorPosition[1] + verticalMargin) - location[1];
// TODO(hsumita): Put the indicator over the cursor if there is no enough space below.
// Note: We always have enough space below thanks to the narrow frame at this time.
drawRect.offsetTo(left, top);
popup.setBounds(drawRect);
}
/**
* Updates the state of the mode indicator according to the {@code command}.
* <p>
* This method hides the indicator if there is a composition text.
*/
public void setCommand(Command command) {
Preconditions.checkNotNull(command);
Input input = command.getInput();
if (input.getType() == Input.CommandType.SEND_COMMAND
&& input.getCommand().getType() == SessionCommand.CommandType.SWITCH_INPUT_MODE) {
// Simply ignores SWITCH_INPUT_MODE since the command doesn't have a composition text.
return;
}
hasComposition = command.getOutput().getPreedit().getSegmentCount() > 0;
if (hasComposition) {
hide();
}
}
/** Shows the mode indicator according to the current composition mode. */
public void setCompositionMode(CompositionMode mode) {
Preconditions.checkNotNull(mode);
MozcImageView contentView = popup.getContentView();
contentView.setImageDrawable(mode == CompositionMode.HIRAGANA
? kanaIndicatorDrawable : abcIndicatorDrawable);
if (!hasComposition) {
show();
}
}
/**
* Shows the mode indicator with animation.
* <p>
* This method issues hide command with delay.
*/
private void show() {
resetAnimation();
updateDrawRect();
if (!isVisible) {
isVisible = true;
View contentView = popup.getContentView();
contentView.setVisibility(View.VISIBLE);
contentView.startAnimation(inAnimation);
}
handler.sendMessageDelayed(handler.obtainMessage(HIDE_MODE_INDICATOR), displayTime);
}
/** Hides the mode indicator with animation. */
public void hide() {
if (!isVisible) {
return;
}
isVisible = false;
resetAnimation();
popup.getContentView().startAnimation(outAnimation);
}
private Animation createInAnimation(Resources resources, float pivotX, float pivotY) {
AnimationSet animationSet = new AnimationSet(true);
animationSet.setDuration(resources.getInteger(R.integer.floating_mode_indicator_in_duration));
animationSet.setInterpolator(new DecelerateInterpolator());
animationSet.addAnimation(new ScaleAnimation(0f, 1f, 0f, 1f, pivotX, pivotY));
animationSet.addAnimation(new AlphaAnimation(0f, 1f));
return animationSet;
}
private Animation createOutAnimation(Resources resources, float pivotX, float pivotY) {
AnimationSet animationSet = new AnimationSet(true);
animationSet.setDuration(resources.getInteger(R.integer.floating_mode_indicator_out_duration));
animationSet.setInterpolator(new DecelerateInterpolator());
animationSet.setAnimationListener(new OutAnimationListener());
animationSet.addAnimation(new ScaleAnimation(1f, 0f, 1f, 0f, pivotX, pivotY));
animationSet.addAnimation(new AlphaAnimation(1f, 0f));
return animationSet;
}
/** Resets all ongoing and scheduled animations. */
private void resetAnimation() {
popup.getContentView().clearAnimation();
handler.removeMessages(HIDE_MODE_INDICATOR);
Preconditions.checkState(!handler.hasMessages(HIDE_MODE_INDICATOR));
}
@VisibleForTesting boolean isVisible() {
return isVisible;
}
}