| // 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; |
| } |
| } |