| // 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; |
| |
| import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; |
| import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; |
| import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory; |
| import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler; |
| import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; |
| import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyboardFactory; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView; |
| import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage; |
| import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; |
| import org.mozc.android.inputmethod.japanese.model.SymbolMinorCategory; |
| import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.DescriptionLayoutPolicy; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.ValueScalingPolicy; |
| import org.mozc.android.inputmethod.japanese.ui.ScrollGuideView; |
| import org.mozc.android.inputmethod.japanese.ui.SpanFactory; |
| import org.mozc.android.inputmethod.japanese.ui.SymbolCandidateLayouter; |
| import org.mozc.android.inputmethod.japanese.view.MozcImageButton; |
| import org.mozc.android.inputmethod.japanese.view.MozcImageView; |
| import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable; |
| import org.mozc.android.inputmethod.japanese.view.Skin; |
| import org.mozc.android.inputmethod.japanese.view.SymbolMajorCategoryButtonDrawableFactory; |
| import org.mozc.android.inputmethod.japanese.view.TabSelectedBackgroundDrawable; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| |
| import android.app.AlertDialog; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.InsetDrawable; |
| import android.graphics.drawable.LayerDrawable; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.preference.PreferenceManager; |
| import android.support.v4.view.PagerAdapter; |
| import android.support.v4.view.ViewPager; |
| import android.support.v4.view.ViewPager.OnPageChangeListener; |
| import android.util.AttributeSet; |
| import android.view.InflateException; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.animation.Animation; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TabHost; |
| import android.widget.TabHost.OnTabChangeListener; |
| import android.widget.TabHost.TabSpec; |
| import android.widget.TabWidget; |
| import android.widget.TextView; |
| |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * This class is used to show symbol input view on which an user can |
| * input emoticon/symbol/emoji. |
| * |
| * In this class, |
| * "Emoticon" means "Kaomoji", like \(^o^)/ |
| * "Symbol" means symbol character, like $ % ! € |
| * "Emoji" means a more graphical character of face, building, food etc. |
| * |
| * This class treats all Emoticon, Symbol and Emoji category as "SymbolMajorCategory". |
| * A major category has several minor categories. |
| * Each minor category belongs to only one major category. |
| * Major-Minor relation is defined by using R.layout.symbol_minor_category_*. |
| * |
| */ |
| public class SymbolInputView extends InOutAnimatedFrameLayout implements MemoryManageable { |
| |
| /** |
| * Adapter for symbol candidate selection. |
| */ |
| // TODO(hidehiko): make this class static. |
| @VisibleForTesting class SymbolCandidateSelectListener implements CandidateSelectListener { |
| @Override |
| public void onCandidateSelected(CandidateWord candidateWord, Optional<Integer> row) { |
| Preconditions.checkNotNull(candidateWord); |
| // When current major category is NUMBER, CandidateView.ConversionCandidateSelectListener |
| // should handle candidate selection event. |
| Preconditions.checkState(currentMajorCategory != SymbolMajorCategory.NUMBER); |
| if (viewEventListener.isPresent()) { |
| // If we are on password field, history shouldn't be updated to protect privacy. |
| viewEventListener.get().onSymbolCandidateSelected( |
| currentMajorCategory, candidateWord.getValue(), !isPasswordField); |
| } |
| } |
| } |
| |
| /** |
| * Click handler of major category buttons. |
| */ |
| @VisibleForTesting class MajorCategoryButtonClickListener implements OnClickListener { |
| private final SymbolMajorCategory majorCategory; |
| |
| MajorCategoryButtonClickListener(SymbolMajorCategory majorCategory) { |
| this.majorCategory = Preconditions.checkNotNull(majorCategory); |
| } |
| |
| @Override |
| public void onClick(View majorCategorySelectorButton) { |
| if (viewEventListener.isPresent()) { |
| viewEventListener.get().onFireFeedbackEvent( |
| FeedbackEvent.SYMBOL_INPUTVIEW_MAJOR_CATEGORY_SELECTED); |
| } |
| |
| if (emojiEnabled |
| && majorCategory == SymbolMajorCategory.EMOJI |
| && emojiProviderType == EmojiProviderType.NONE) { |
| // Ask the user which emoji provider s/he'd like to use. |
| // If the user cancels the dialog, do nothing. |
| maybeInitializeEmojiProviderDialog(getContext()); |
| if (emojiProviderDialog.isPresent()) { |
| IBinder token = getWindowToken(); |
| if (token != null) { |
| MozcUtil.setWindowToken(token, emojiProviderDialog.get()); |
| } else { |
| MozcLog.w("Unknown window token."); |
| } |
| |
| // If a user selects a provider, the dialog handler will set major category |
| // to EMOJI automatically. If s/he cancels, nothing will be happened. |
| emojiProviderDialog.get().show(); |
| return; |
| } |
| } |
| |
| setMajorCategory(majorCategory); |
| } |
| } |
| |
| private static class SymbolTabWidgetViewPagerAdapter extends PagerAdapter |
| implements OnTabChangeListener, OnPageChangeListener { |
| |
| private static final int HISTORY_INDEX = 0; |
| |
| private final Context context; |
| private final SymbolCandidateStorage symbolCandidateStorage; |
| private final Optional<ViewEventListener> viewEventListener; |
| private final CandidateSelectListener candidateSelectListener; |
| private final SymbolMajorCategory majorCategory; |
| private Skin skin; |
| private final EmojiProviderType emojiProviderType; |
| private final TabHost tabHost; |
| private final ViewPager viewPager; |
| private final float candidateTextSize; |
| private final float descriptionTextSize; |
| |
| private Optional<View> historyViewCache = Optional.absent(); |
| private int scrollState = ViewPager.SCROLL_STATE_IDLE; |
| private boolean feedbackEnabled = true; |
| |
| SymbolTabWidgetViewPagerAdapter( |
| Context context, SymbolCandidateStorage symbolCandidateStorage, |
| Optional<ViewEventListener> viewEventListener, |
| CandidateSelectListener candidateSelectListener, SymbolMajorCategory majorCategory, |
| Skin skin, EmojiProviderType emojiProviderType, TabHost tabHost, ViewPager viewPager, |
| float candidateTextSize, float descriptionTextSize) { |
| this.context = Preconditions.checkNotNull(context); |
| this.symbolCandidateStorage = Preconditions.checkNotNull(symbolCandidateStorage); |
| this.viewEventListener = Preconditions.checkNotNull(viewEventListener); |
| this.candidateSelectListener = Preconditions.checkNotNull(candidateSelectListener); |
| this.majorCategory = Preconditions.checkNotNull(majorCategory); |
| this.skin = Preconditions.checkNotNull(skin); |
| this.emojiProviderType = Preconditions.checkNotNull(emojiProviderType); |
| this.tabHost = Preconditions.checkNotNull(tabHost); |
| this.viewPager = Preconditions.checkNotNull(viewPager); |
| this.candidateTextSize = Preconditions.checkNotNull(candidateTextSize); |
| this.descriptionTextSize = Preconditions.checkNotNull(descriptionTextSize); |
| } |
| |
| public void setSkin(Skin skin) { |
| Preconditions.checkNotNull(skin); |
| this.skin = skin; |
| } |
| |
| public void setFeedbackEnabled(boolean enabled) { |
| feedbackEnabled = enabled; |
| } |
| |
| private void maybeResetHistoryView() { |
| if (viewPager.getCurrentItem() != HISTORY_INDEX && historyViewCache.isPresent()) { |
| resetHistoryView(); |
| } |
| } |
| |
| private void resetHistoryView() { |
| if (!historyViewCache.isPresent()) { |
| return; |
| } |
| CandidateList candidateList = |
| symbolCandidateStorage.getCandidateList(majorCategory.minorCategories.get(0)); |
| View noHistoryView = historyViewCache.get().findViewById(R.id.symbol_input_no_history); |
| if (candidateList.getCandidatesCount() == 0) { |
| noHistoryView.setVisibility(View.VISIBLE); |
| TextView.class.cast(historyViewCache.get().findViewById(R.id.symbol_input_no_history_text)) |
| .setTextColor(skin.candidateValueTextColor); |
| } else { |
| noHistoryView.setVisibility(View.GONE); |
| } |
| SymbolCandidateView.class.cast(historyViewCache.get().findViewById( |
| R.id.symbol_input_candidate_view)).update(candidateList); |
| } |
| |
| @Override |
| public void onPageScrollStateChanged(int state) { |
| if (scrollState == ViewPager.SCROLL_STATE_IDLE) { |
| maybeResetHistoryView(); |
| } |
| scrollState = state; |
| } |
| |
| @Override |
| public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { |
| // Do nothing. |
| } |
| |
| @Override |
| public void onPageSelected(int position) { |
| tabHost.setOnTabChangedListener(null); |
| tabHost.setCurrentTab(position); |
| tabHost.setOnTabChangedListener(this); |
| } |
| |
| @Override |
| public void onTabChanged(String tabId) { |
| int position = Integer.parseInt(tabId); |
| |
| if (position == HISTORY_INDEX) { |
| maybeResetHistoryView(); |
| } |
| viewPager.setCurrentItem(position, false); |
| |
| if (feedbackEnabled && viewEventListener.isPresent()) { |
| viewEventListener.get().onFireFeedbackEvent( |
| FeedbackEvent.SYMBOL_INPUTVIEW_MINOR_CATEGORY_SELECTED); |
| } |
| } |
| |
| @Override |
| public int getCount() { |
| return majorCategory.minorCategories.size(); |
| } |
| |
| @Override |
| public boolean isViewFromObject(View view, Object item) { |
| return view == item; |
| } |
| |
| @Override |
| public Object instantiateItem(ViewGroup container, int position) { |
| LayoutInflater inflater = LayoutInflater.from(context); |
| inflater = inflater.cloneInContext(context); |
| |
| View view = MozcUtil.inflateWithOutOfMemoryRetrial( |
| View.class, inflater, R.layout.symbol_candidate_view, Optional.<ViewGroup>absent(), |
| false); |
| SymbolCandidateView symbolCandidateView = |
| SymbolCandidateView.class.cast(view.findViewById(R.id.symbol_input_candidate_view)); |
| symbolCandidateView.setCandidateSelectListener(candidateSelectListener); |
| symbolCandidateView.setMinColumnWidth( |
| context.getResources().getDimension(majorCategory.minColumnWidthResourceId)); |
| symbolCandidateView.setSkin(skin); |
| symbolCandidateView.setEmojiProviderType(emojiProviderType); |
| if (majorCategory.layoutPolicy == DescriptionLayoutPolicy.GONE) { |
| // As it's guaranteed for descriptions not to be shown, |
| // show values using additional space where is reserved for descriptions. |
| // This makes Emoji bigger. |
| symbolCandidateView.setCandidateTextDimension(candidateTextSize + descriptionTextSize, 0); |
| } else { |
| symbolCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize); |
| } |
| symbolCandidateView.setDescriptionLayoutPolicy(majorCategory.layoutPolicy); |
| |
| // Set candidate contents. |
| if (position == HISTORY_INDEX) { |
| historyViewCache = Optional.of(view); |
| resetHistoryView(); |
| } else { |
| symbolCandidateView.update(symbolCandidateStorage.getCandidateList( |
| majorCategory.minorCategories.get(position))); |
| symbolCandidateView.updateScrollPositionBasedOnFocusedIndex(); |
| } |
| |
| ScrollGuideView scrollGuideView = |
| ScrollGuideView.class.cast(view.findViewById(R.id.symbol_input_scroll_guide_view)); |
| scrollGuideView.setSkin(skin); |
| |
| // Connect guide and candidate view. |
| scrollGuideView.setScroller(symbolCandidateView.scroller); |
| symbolCandidateView.setScrollIndicator(scrollGuideView); |
| |
| container.addView(view); |
| return view; |
| } |
| |
| @Override |
| public void destroyItem(ViewGroup collection, int position, Object view) { |
| if (position == HISTORY_INDEX) { |
| historyViewCache = Optional.absent(); |
| } |
| collection.removeView(View.class.cast(view)); |
| } |
| } |
| |
| /** |
| * An event listener for the menu dialog window. |
| */ |
| private class EmojiProviderDialogListener implements DialogInterface.OnClickListener { |
| private final Context context; |
| |
| EmojiProviderDialogListener(Context context) { |
| this.context = Preconditions.checkNotNull(context); |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| String value; |
| TypedArray typedArray = |
| context.getResources().obtainTypedArray(R.array.pref_emoji_provider_type_values); |
| try { |
| value = typedArray.getString(which); |
| } finally { |
| typedArray.recycle(); |
| } |
| sharedPreferences.edit() |
| .putString(PreferenceUtil.PREF_EMOJI_PROVIDER_TYPE, value) |
| .commit(); |
| setMajorCategory(SymbolMajorCategory.EMOJI); |
| } |
| } |
| |
| /** |
| * The candidate view for SymbolInputView. |
| * |
| * The differences from CandidateView.CandidateWordViewForConversion are |
| * 1) this class scrolls horizontally 2) the layout algorithm is simpler. |
| */ |
| private static class SymbolCandidateView extends CandidateWordView { |
| private static final String DESCRIPTION_DELIMITER = "\n"; |
| |
| private Optional<View> scrollGuideView = Optional.absent(); |
| |
| public SymbolCandidateView(Context context) { |
| super(context, Orientation.VERTICAL); |
| } |
| |
| public SymbolCandidateView(Context context, AttributeSet attributeSet) { |
| super(context, attributeSet, Orientation.VERTICAL); |
| } |
| |
| public SymbolCandidateView(Context context, AttributeSet attributeSet, int defaultStyle) { |
| super(context, attributeSet, defaultStyle, Orientation.VERTICAL); |
| } |
| |
| // Shared instance initializer. |
| { |
| setSpanBackgroundDrawableType(DrawableType.SYMBOL_CANDIDATE_BACKGROUND); |
| Resources resources = getResources(); |
| scroller.setDecayRate( |
| resources.getInteger(R.integer.symbol_input_scroller_velocity_decay_rate) / 1000000f); |
| scroller.setMinimumVelocity( |
| resources.getInteger(R.integer.symbol_input_scroller_minimum_velocity)); |
| layouter = new SymbolCandidateLayouter(); |
| } |
| |
| void setCandidateTextDimension(float textSize, float descriptionTextSize) { |
| Preconditions.checkArgument(textSize >= 0); |
| Preconditions.checkArgument(descriptionTextSize >= 0); |
| |
| Resources resources = getResources(); |
| |
| float valueHorizontalPadding = |
| resources.getDimension(R.dimen.symbol_candidate_horizontal_padding_size); |
| float descriptionHorizontalPadding = |
| resources.getDimension(R.dimen.symbol_description_right_padding); |
| float descriptionVerticalPadding = |
| resources.getDimension(R.dimen.symbol_description_bottom_padding); |
| float separatorWidth = resources.getDimensionPixelSize(R.dimen.candidate_separator_width); |
| |
| carrierEmojiRenderHelper.setCandidateTextSize(textSize); |
| candidateLayoutRenderer.setValueTextSize(textSize); |
| candidateLayoutRenderer.setValueHorizontalPadding(valueHorizontalPadding); |
| candidateLayoutRenderer.setValueScalingPolicy(ValueScalingPolicy.UNIFORM); |
| candidateLayoutRenderer.setDescriptionTextSize(descriptionTextSize); |
| candidateLayoutRenderer.setDescriptionHorizontalPadding(descriptionHorizontalPadding); |
| candidateLayoutRenderer.setDescriptionVerticalPadding(descriptionVerticalPadding); |
| candidateLayoutRenderer.setSeparatorWidth(separatorWidth); |
| |
| SpanFactory spanFactory = new SpanFactory(); |
| spanFactory.setValueTextSize(textSize); |
| spanFactory.setDescriptionTextSize(descriptionTextSize); |
| spanFactory.setDescriptionDelimiter(DESCRIPTION_DELIMITER); |
| |
| SymbolCandidateLayouter layouter = SymbolCandidateLayouter.class.cast(this.layouter); |
| layouter.setSpanFactory(spanFactory); |
| layouter.setRowHeight(resources.getDimensionPixelSize(R.dimen.symbol_view_candidate_height)); |
| } |
| |
| @Override |
| SymbolCandidateLayouter getCandidateLayouter() { |
| return SymbolCandidateLayouter.class.cast(super.getCandidateLayouter()); |
| } |
| |
| void setMinColumnWidth(float minColumnWidth) { |
| getCandidateLayouter().setMinColumnWidth(minColumnWidth); |
| updateLayouter(); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| if (scrollGuideView.isPresent()) { |
| scrollGuideView.get().invalidate(); |
| } |
| } |
| |
| void setScrollIndicator(View scrollGuideView) { |
| this.scrollGuideView = Optional.of(scrollGuideView); |
| } |
| |
| @Override |
| protected Drawable getViewBackgroundDrawable(Skin skin) { |
| return Preconditions.checkNotNull(skin).symbolCandidateViewBackgroundDrawable; |
| } |
| |
| @Override |
| public void setSkin(Skin skin) { |
| super.setSkin(skin); |
| candidateLayoutRenderer.setSeparatorColor(skin.symbolCandidateBackgroundSeparatorColor); |
| } |
| |
| public void setDescriptionLayoutPolicy(DescriptionLayoutPolicy policy) { |
| candidateLayoutRenderer.setDescriptionLayoutPolicy(Preconditions.checkNotNull(policy)); |
| } |
| } |
| |
| private class OutAnimationAdapter extends AnimationAdapter { |
| @Override |
| public void onAnimationEnd(Animation animation) { |
| // Releases candidate resources. Also, on some devices, this cancels repeating invalidation |
| // to support emoji related stuff. |
| TabHost tabHost = getTabHost(); |
| tabHost.setOnTabChangedListener(null); |
| ViewPager candidateViewPager = getCandidateViewPager(); |
| candidateViewPager.setAdapter(null); |
| candidateViewPager.setOnPageChangeListener(null); |
| } |
| } |
| |
| /** |
| * Name to represent this view for logging. |
| */ |
| static final KeyboardSpecificationName SPEC_NAME = |
| new KeyboardSpecificationName("SYMBOL_INPUT_VIEW", 0, 1, 0); |
| // Source ID of the delete/enter button for logging usage stats. |
| private static final int DELETE_BUTTON_SOURCE_ID = 1; |
| private static final int ENTER_BUTTON_SOURCE_ID = 2; |
| |
| private static final int NUM_TABS = 6; |
| |
| private Optional<Integer> viewHeight = Optional.absent(); |
| private Optional<Integer> numberKeyboardHeight = Optional.absent(); |
| private Optional<Float> keyboardHeightScale = Optional.absent(); |
| |
| private Optional<SymbolCandidateStorage> symbolCandidateStorage = Optional.absent(); |
| |
| @VisibleForTesting SymbolMajorCategory currentMajorCategory = SymbolMajorCategory.NUMBER; |
| @VisibleForTesting boolean emojiEnabled; |
| private boolean isPasswordField; |
| @VisibleForTesting EmojiProviderType emojiProviderType = EmojiProviderType.NONE; |
| |
| @VisibleForTesting SharedPreferences sharedPreferences; |
| @VisibleForTesting Optional<AlertDialog> emojiProviderDialog = Optional.absent(); |
| |
| private Optional<ViewEventListener> viewEventListener = Optional.absent(); |
| private final KeyEventButtonTouchListener deleteKeyEventButtonTouchListener = |
| createDeleteKeyEventButtonTouchListener(getResources()); |
| private final KeyEventButtonTouchListener enterKeyEventButtonTouchListener = |
| createEnterKeyEventButtonTouchListener(getResources()); |
| private Optional<OnClickListener> closeButtonClickListener = Optional.absent(); |
| private Optional<OnClickListener> microphoneButtonClickListener = Optional.absent(); |
| private final SymbolCandidateSelectListener symbolCandidateSelectListener = |
| new SymbolCandidateSelectListener(); |
| |
| private Skin skin = Skin.getFallbackInstance(); |
| private final SymbolMajorCategoryButtonDrawableFactory majorCategoryButtonDrawableFactory = |
| new SymbolMajorCategoryButtonDrawableFactory(getResources()); |
| // Candidate text size in dip. |
| private float candidateTextSize; |
| // Description text size in dip. |
| private float desciptionTextSize; |
| private Optional<KeyEventHandler> keyEventHandler = Optional.absent(); |
| private boolean isMicrophoneButtonEnabled; |
| private boolean popupEnabled; |
| |
| public SymbolInputView(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| } |
| |
| public SymbolInputView(Context context) { |
| super(context); |
| } |
| |
| public SymbolInputView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| { |
| setOutAnimationListener(new OutAnimationAdapter()); |
| sharedPreferences = |
| Preconditions.checkNotNull(PreferenceManager.getDefaultSharedPreferences(getContext())); |
| } |
| |
| private static KeyEventButtonTouchListener createDeleteKeyEventButtonTouchListener( |
| Resources resources) { |
| return new KeyEventButtonTouchListener( |
| DELETE_BUTTON_SOURCE_ID, resources.getInteger(R.integer.uchar_backspace)); |
| } |
| |
| private static KeyEventButtonTouchListener createEnterKeyEventButtonTouchListener( |
| Resources resources) { |
| return new KeyEventButtonTouchListener( |
| ENTER_BUTTON_SOURCE_ID, resources.getInteger(R.integer.uchar_linefeed)); |
| } |
| |
| boolean isInflated() { |
| return getChildCount() > 0; |
| } |
| |
| void inflateSelf() { |
| Preconditions.checkState(!isInflated(), "The symbol input view is already inflated."); |
| |
| // Hack: Because we wrap the real context to inject "retrying" for Drawable loading, |
| // LayoutInflater.from(getContext()).getContext() may be different from getContext(). |
| // So, we clone the inflater here, too, with actual context. |
| Context context = getContext(); |
| LayoutInflater inflater = LayoutInflater.from(context); |
| inflater = inflater.cloneInContext(context); |
| MozcUtil.inflateWithOutOfMemoryRetrial( |
| SymbolInputView.class, inflater, R.layout.symbol_view, Optional.<ViewGroup>of(this), true); |
| // Note: onFinishInflate won't be invoked on android ver 3.0 or later, while it is invoked |
| // on android 2.3 or earlier. So, we define another (but similar) method and invoke it here |
| // manually. |
| onFinishInflateSelf(); |
| } |
| |
| /** |
| * Initializes the instance. Called only once. |
| * {@code onFinishInflate()} is *not* invoked for the inflation of <merge> element we use. |
| * So, instead, we define another onFinishInflate method and invoke this manually. |
| */ |
| protected void onFinishInflateSelf() { |
| if (viewHeight.isPresent() && keyboardHeightScale.isPresent()) { |
| setVerticalDimension(viewHeight.get(), keyboardHeightScale.get()); |
| } |
| |
| initializeMinorCategoryTab(); |
| initializeCloseButton(); |
| initializeDeleteButton(); |
| initializeEnterButton(); |
| initializeMicrophoneButton(); |
| |
| // Set TouchListener that does nothing. Without this hack, state_pressed event |
| // will be propagated to close / enter key and the drawable will be changed to |
| // state_pressed one unexpectedly. Note that those keys are NOT children of this view. |
| // Setting ClickListener to the key seems to suppress this unexpected highlight, too, |
| // but we want to keep the current TouchListener for the enter key. |
| OnTouchListener doNothingOnTouchListener = new OnTouchListener() { |
| @Override |
| public boolean onTouch(View button, android.view.MotionEvent event) { |
| return true; |
| } |
| }; |
| for (int id : new int[] {R.id.button_frame_in_symbol_view, |
| R.id.symbol_view_backspace_separator, |
| R.id.symbol_major_category, |
| R.id.symbol_separator_1, |
| R.id.symbol_separator_2, |
| R.id.symbol_separator_3, |
| R.id.symbol_view_close_button_separator, |
| R.id.symbol_view_enter_button_separator}) { |
| findViewById(id).setOnTouchListener(doNothingOnTouchListener); |
| } |
| |
| KeyboardView keyboardView = KeyboardView.class.cast(findViewById(R.id.number_keyboard)); |
| keyboardView.setPopupEnabled(popupEnabled); |
| keyboardView.setKeyEventHandler(new KeyEventHandler( |
| Looper.getMainLooper(), |
| new KeyboardActionListener() { |
| @Override |
| public void onRelease(int keycode) { |
| } |
| |
| @Override |
| public void onPress(int keycode) { |
| if (viewEventListener.isPresent()) { |
| viewEventListener.get().onFireFeedbackEvent(FeedbackEvent.KEY_DOWN); |
| } |
| } |
| |
| @Override |
| public void onKey(int primaryCode, List<TouchEvent> touchEventList) { |
| if (keyEventHandler.isPresent()) { |
| keyEventHandler.get().sendKey(primaryCode, touchEventList); |
| } |
| } |
| |
| @Override |
| public void onCancel() { |
| } |
| }, |
| getResources().getInteger(R.integer.config_repeat_key_delay), |
| getResources().getInteger(R.integer.config_repeat_key_interval), |
| getResources().getInteger(R.integer.config_long_press_key_delay))); |
| |
| enableEmoji(emojiEnabled); |
| |
| // Disable h/w acceleration to use a PictureDrawable. |
| for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) { |
| getMajorCategoryButton(majorCategory).setLayerType(View.LAYER_TYPE_SOFTWARE, null); |
| } |
| |
| updateSkinAwareDrawable(); |
| reset(); |
| } |
| |
| private static void setLayoutHeight(View view, int height) { |
| ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); |
| layoutParams.height = height; |
| view.setLayoutParams(layoutParams); |
| } |
| |
| public void setVerticalDimension(int symbolInputViewHeight, float keyboardHeightScale) { |
| this.viewHeight = Optional.of(symbolInputViewHeight); |
| this.keyboardHeightScale = Optional.of(keyboardHeightScale); |
| |
| Resources resources = getResources(); |
| float originalMajorCategoryHeight = |
| resources.getDimension(R.dimen.symbol_view_major_category_height); |
| int majorCategoryHeight = Math.round(originalMajorCategoryHeight * keyboardHeightScale); |
| this.numberKeyboardHeight = Optional.of( |
| symbolInputViewHeight - majorCategoryHeight |
| - resources.getDimensionPixelSize(R.dimen.button_frame_height)); |
| |
| if (!isInflated()) { |
| return; |
| } |
| |
| setLayoutHeight(this, symbolInputViewHeight); |
| setLayoutHeight(getMajorCategoryFrame(), majorCategoryHeight); |
| setLayoutHeight(findViewById(R.id.number_keyboard), numberKeyboardHeight.get()); |
| setLayoutHeight(findViewById(R.id.number_keyboard_frame), LayoutParams.WRAP_CONTENT); |
| } |
| |
| public int getNumberKeyboardHeight() { |
| return numberKeyboardHeight.get(); |
| } |
| |
| private void resetCandidateViewPager() { |
| if (!isInflated()) { |
| return; |
| } |
| |
| ViewPager candidateViewPager = getCandidateViewPager(); |
| TabHost tabHost = getTabHost(); |
| Preconditions.checkState(symbolCandidateStorage.isPresent()); |
| |
| SymbolTabWidgetViewPagerAdapter adapter = new SymbolTabWidgetViewPagerAdapter( |
| getContext(), |
| symbolCandidateStorage.get(), viewEventListener, symbolCandidateSelectListener, |
| currentMajorCategory, skin, emojiProviderType, tabHost, candidateViewPager, |
| candidateTextSize, desciptionTextSize); |
| candidateViewPager.setAdapter(adapter); |
| candidateViewPager.setOnPageChangeListener(adapter); |
| tabHost.setOnTabChangedListener(adapter); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void updateMajorCategoryBackgroundSkin() { |
| View view = getMajorCategoryFrame(); |
| if (view != null) { |
| view.setBackgroundDrawable( |
| skin.symbolMajorCategoryBackgroundDrawable.getConstantState().newDrawable()); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void updateMinorCategoryBackgroundSkin() { |
| View view = getMinorCategoryFrame(); |
| if (view != null) { |
| view.setBackgroundDrawable( |
| skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable()); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void updateNumberKeyboardSkin() { |
| getNumberKeyboardView().setSkin(skin); |
| findViewById(R.id.number_frame).setBackgroundDrawable( |
| skin.windowBackgroundDrawable.getConstantState().newDrawable()); |
| findViewById(R.id.button_frame_in_symbol_view).setBackgroundDrawable( |
| skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable()); |
| } |
| |
| /** |
| * Sets click event handlers to each major category button. |
| * It is necessary that the inflation has been done before this method invocation. |
| */ |
| @SuppressWarnings("deprecation") |
| private void updateMajorCategoryButtonsSkin() { |
| Resources resources = getResources(); |
| for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) { |
| MozcImageButton view = getMajorCategoryButton(majorCategory); |
| Preconditions.checkState( |
| view != null, "The view corresponding to " + majorCategory.name() + " is not found."); |
| view.setOnClickListener(new MajorCategoryButtonClickListener(majorCategory)); |
| switch (majorCategory) { |
| case NUMBER: |
| view.setBackgroundDrawable( |
| majorCategoryButtonDrawableFactory.createLeftButtonDrawable()); |
| break; |
| case EMOJI: |
| view.setBackgroundDrawable( |
| majorCategoryButtonDrawableFactory.createRightButtonDrawable(emojiEnabled)); |
| break; |
| default: |
| view.setBackgroundDrawable( |
| majorCategoryButtonDrawableFactory.createCenterButtonDrawable()); |
| break; |
| } |
| view.setImageDrawable(skin.getDrawable(resources, majorCategory.buttonImageResourceId)); |
| // Update the padding since setBackgroundDrawable() overwrites it. |
| view.setMaxImageHeight( |
| resources.getDimensionPixelSize(majorCategory.maxImageHeightResourceId)); |
| } |
| } |
| |
| private void initializeMinorCategoryTab() { |
| TabHost tabhost = getTabHost(); |
| tabhost.setup(); |
| // Create NUM_TABS (= 6) tabs. |
| // Note that we may want to change the number of tabs, however due to the limitation of |
| // the current TabHost implementation, it is difficult. Fortunately, all major categories |
| // have the same number of minor categories, so we use it as hard-coded value. |
| for (int i = 0; i < NUM_TABS; ++i) { |
| // The tab's id is the index of the tab. |
| TabSpec tab = tabhost.newTabSpec(String.valueOf(i)); |
| MozcImageView view = new MozcImageView(getContext()); |
| view.setSoundEffectsEnabled(false); |
| tab.setIndicator(view); |
| // Set dummy view for the content. The actual content will be managed by ViewPager. |
| tab.setContent(R.id.symbol_input_dummy); |
| tabhost.addTab(tab); |
| } |
| |
| // Hack: Set the current tab to the non-default (neither 0 nor 1) position, |
| // so that the reset process will set the view's visibility appropriately. |
| tabhost.setCurrentTab(2); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void updateTabBackgroundSkin() { |
| if (!isInflated()) { |
| return; |
| } |
| getTabHost().setBackgroundDrawable( |
| skin.windowBackgroundDrawable.getConstantState().newDrawable()); |
| TabWidget tabWidget = getTabWidget(); |
| for (int i = 0; i < tabWidget.getTabCount(); ++i) { |
| View view = tabWidget.getChildTabViewAt(i); |
| view.setBackgroundDrawable(createTabBackgroundDrawable(skin)); |
| } |
| } |
| |
| private static Drawable createTabBackgroundDrawable(Skin skin) { |
| Preconditions.checkNotNull(skin); |
| return new LayerDrawable(new Drawable[] { |
| BackgroundDrawableFactory.createSelectableDrawable( |
| new TabSelectedBackgroundDrawable( |
| Math.round(skin.symbolMinorIndicatorHeightDimension), |
| skin.symbolMinorCategoryTabSelectedColor), |
| Optional.<Drawable>absent()), |
| createMinorButtonBackgroundDrawable(skin) |
| }); |
| } |
| |
| private void resetTabImageForMinorCategory() { |
| if (!isInflated()) { |
| return; |
| } |
| TabWidget tabWidget = getTabWidget(); |
| List<SymbolMinorCategory> minorCategoryList = currentMajorCategory.minorCategories; |
| int definedTabSize = Math.min(minorCategoryList.size(), tabWidget.getChildCount()); |
| for (int i = 0; i < definedTabSize; ++i) { |
| MozcImageView view = MozcImageView.class.cast(tabWidget.getChildTabViewAt(i)); |
| SymbolMinorCategory symbolMinorCategory = minorCategoryList.get(i); |
| view.setRawId(symbolMinorCategory.drawableResourceId); |
| if (symbolMinorCategory.maxImageHeightResourceId != SymbolMinorCategory.INVALID_RESOURCE_ID) { |
| view.setMaxImageHeight( |
| getResources().getDimensionPixelSize(symbolMinorCategory.maxImageHeightResourceId)); |
| } |
| if (symbolMinorCategory.contentDescriptionResourceId |
| != SymbolMinorCategory.INVALID_RESOURCE_ID) { |
| view.setContentDescription( |
| getResources().getString(symbolMinorCategory.contentDescriptionResourceId)); |
| } |
| } |
| } |
| |
| private static Drawable createMajorButtonBackgroundDrawable(Skin skin) { |
| int padding = Math.round(skin.symbolMajorButtonPaddingDimension); |
| int round = Math.round(skin.symbolMajorButtonRoundDimension); |
| return BackgroundDrawableFactory.createPressableDrawable( |
| new RoundRectKeyDrawable( |
| padding, padding, padding, padding, round, |
| skin.symbolPressedFunctionKeyTopColor, |
| skin.symbolPressedFunctionKeyBottomColor, |
| skin.symbolPressedFunctionKeyHighlightColor, |
| skin.symbolPressedFunctionKeyShadowColor), |
| Optional.<Drawable>of(new RoundRectKeyDrawable( |
| padding, padding, padding, padding, round, |
| skin.symbolReleasedFunctionKeyTopColor, |
| skin.symbolReleasedFunctionKeyBottomColor, |
| skin.symbolReleasedFunctionKeyHighlightColor, |
| skin.symbolReleasedFunctionKeyShadowColor))); |
| } |
| |
| private static Drawable createMinorButtonBackgroundDrawable(Skin skin) { |
| return BackgroundDrawableFactory.createPressableDrawable( |
| new ColorDrawable(skin.symbolMinorCategoryTabPressedColor), |
| Optional.<Drawable>absent()); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void initializeCloseButton() { |
| ImageView closeButton = ImageView.class.cast(findViewById(R.id.symbol_view_close_button)); |
| if (closeButtonClickListener.isPresent()) { |
| closeButton.setOnClickListener(closeButtonClickListener.get()); |
| } |
| } |
| |
| /** |
| * Sets a click event handler to the delete button. |
| * It is necessary that the inflation has been done before this method invocation. |
| */ |
| @SuppressWarnings("deprecation") |
| private void initializeDeleteButton() { |
| MozcImageView deleteButton = |
| MozcImageView.class.cast(findViewById(R.id.symbol_view_delete_button)); |
| deleteButton.setOnTouchListener(deleteKeyEventButtonTouchListener); |
| } |
| |
| /** c.f., {@code initializeDeleteButton}. */ |
| @SuppressWarnings("deprecation") |
| private void initializeEnterButton() { |
| ImageView enterButton = ImageView.class.cast(findViewById(R.id.symbol_view_enter_button)); |
| enterButton.setOnTouchListener(enterKeyEventButtonTouchListener); |
| } |
| |
| private void initializeMicrophoneButton() { |
| MozcImageView microphoneButton = getMicrophoneButton(); |
| if (microphoneButtonClickListener.isPresent()) { |
| microphoneButton.setOnClickListener(microphoneButtonClickListener.get()); |
| } |
| microphoneButton.setVisibility(isMicrophoneButtonEnabled ? VISIBLE : GONE); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void updateSeparatorsSkin() { |
| Resources resources = getResources(); |
| int minorPaddingSize = (int) resources.getFraction( |
| R.fraction.symbol_separator_padding_fraction, |
| resources.getDimensionPixelSize(R.dimen.button_frame_height), 0); |
| findViewById(R.id.symbol_view_backspace_separator).setBackgroundDrawable( |
| new InsetDrawable(new ColorDrawable(skin.symbolSeparatorColor), |
| 0, minorPaddingSize, 0, minorPaddingSize)); |
| int majorPaddingSize = (int) resources.getFraction( |
| R.fraction.symbol_separator_padding_fraction, |
| resources.getDimensionPixelSize(R.dimen.symbol_view_major_category_height), 0); |
| InsetDrawable separator = new InsetDrawable( |
| new ColorDrawable(skin.symbolSeparatorColor), 0, majorPaddingSize, 0, majorPaddingSize); |
| for (int id : new int[] {R.id.symbol_view_close_button_separator, |
| R.id.symbol_view_enter_button_separator}) { |
| findViewById(id).setBackgroundDrawable(separator.getConstantState().newDrawable()); |
| } |
| |
| for (int id : new int[] {R.id.symbol_separator_1, |
| R.id.symbol_separator_3}) { |
| findViewById(id).setBackgroundDrawable( |
| skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable()); |
| } |
| findViewById(R.id.symbol_separator_2).setBackgroundDrawable( |
| skin.symbolSeparatorAboveMajorCategoryBackgroundDrawable |
| .getConstantState().newDrawable()); |
| } |
| |
| @VisibleForTesting TabHost getTabHost() { |
| return TabHost.class.cast(findViewById(android.R.id.tabhost)); |
| } |
| |
| private ViewPager getCandidateViewPager() { |
| return ViewPager.class.cast(findViewById(R.id.symbol_input_candidate_view_pager)); |
| } |
| |
| @VisibleForTesting MozcImageButton getMajorCategoryButton(SymbolMajorCategory majorCategory) { |
| Preconditions.checkNotNull(majorCategory); |
| return MozcImageButton.class.cast(findViewById(majorCategory.buttonResourceId)); |
| } |
| |
| @VisibleForTesting View getEmojiDisabledMessageView() { |
| return findViewById(R.id.symbol_emoji_disabled_message_view); |
| } |
| |
| public void setEmojiEnabled(boolean unicodeEmojiEnabled, boolean carrierEmojiEnabled) { |
| this.emojiEnabled = unicodeEmojiEnabled || carrierEmojiEnabled; |
| enableEmoji(this.emojiEnabled); |
| Preconditions.checkState(symbolCandidateStorage.isPresent()); |
| symbolCandidateStorage.get().setEmojiEnabled(unicodeEmojiEnabled, carrierEmojiEnabled); |
| } |
| |
| public void setPasswordField(boolean isPasswordField) { |
| this.isPasswordField = isPasswordField; |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void enableEmoji(boolean enableEmoji) { |
| if (!isInflated()) { |
| return; |
| } |
| |
| MozcImageButton imageButton = getMajorCategoryButton(SymbolMajorCategory.EMOJI); |
| imageButton.setBackgroundDrawable( |
| majorCategoryButtonDrawableFactory.createRightButtonDrawable(enableEmoji)); |
| // Update the padding since setBackgroundDrawable() overwrites it. |
| imageButton.setMaxImageHeight(getResources().getDimensionPixelSize( |
| SymbolMajorCategory.EMOJI.maxImageHeightResourceId)); |
| } |
| |
| void resetToMajorCategory(Optional<SymbolMajorCategory> category) { |
| Preconditions.checkNotNull(category); |
| setMajorCategory(category.or(currentMajorCategory)); |
| deleteKeyEventButtonTouchListener.reset(); |
| enterKeyEventButtonTouchListener.reset(); |
| } |
| |
| @VisibleForTesting void reset() { |
| // the current minor category is also updated in setMajorCategory. |
| resetToMajorCategory(Optional.of(SymbolMajorCategory.NUMBER)); |
| } |
| |
| @Override |
| public void setVisibility(int visibility) { |
| int previousVisibility = getVisibility(); |
| super.setVisibility(visibility); |
| if (viewEventListener.isPresent()) { |
| if (previousVisibility == View.VISIBLE && visibility != View.VISIBLE) { |
| viewEventListener.get().onCloseSymbolInputView(); |
| } else if (previousVisibility != View.VISIBLE && visibility == View.VISIBLE) { |
| viewEventListener.get().onShowSymbolInputView(Collections.<TouchEvent>emptyList()); |
| } |
| } |
| } |
| |
| void setSymbolCandidateStorage(SymbolCandidateStorage symbolCandidateStorage) { |
| this.symbolCandidateStorage = Optional.of(symbolCandidateStorage); |
| } |
| |
| void setKeyEventHandler(KeyEventHandler keyEventHandler) { |
| this.keyEventHandler = Optional.of(keyEventHandler); |
| deleteKeyEventButtonTouchListener.setKeyEventHandler(keyEventHandler); |
| enterKeyEventButtonTouchListener.setKeyEventHandler(keyEventHandler); |
| } |
| |
| void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) { |
| Preconditions.checkArgument(candidateTextSize > 0); |
| Preconditions.checkArgument(descriptionTextSize > 0); |
| |
| this.candidateTextSize = candidateTextSize; |
| this.desciptionTextSize = descriptionTextSize; |
| } |
| |
| void setPopupEnabled(boolean popupEnabled) { |
| this.popupEnabled = popupEnabled; |
| if (!isInflated()) { |
| return; |
| } |
| getNumberKeyboardView().setPopupEnabled(popupEnabled); |
| } |
| |
| /** |
| * Initializes EmojiProvider selection dialog, if necessary. |
| */ |
| @VisibleForTesting void maybeInitializeEmojiProviderDialog(Context context) { |
| if (emojiProviderDialog.isPresent()) { |
| return; |
| } |
| |
| EmojiProviderDialogListener listener = new EmojiProviderDialogListener(context); |
| try { |
| AlertDialog dialog = new AlertDialog.Builder(context) |
| .setTitle(R.string.pref_emoji_provider_type_title) |
| .setItems(R.array.pref_emoji_provider_type_entries, listener) |
| .create(); |
| this.emojiProviderDialog = Optional.of(dialog); |
| } catch (InflateException e) { |
| // Ignore the exception. |
| } |
| } |
| |
| /** |
| * Sets the major category to show. |
| * |
| * The view is updated. |
| * The active minor category is also updated. |
| * |
| * This method submit a preedit text except for a {@link SymbolMajorCategory#NUMBER} major |
| * category since this class commit a candidate directly. |
| * |
| * @param newCategory the major category to show. |
| */ |
| @VisibleForTesting void setMajorCategory(SymbolMajorCategory newCategory) { |
| Preconditions.checkNotNull(newCategory); |
| |
| { |
| SymbolCandidateView symbolCandidateView = |
| SymbolCandidateView.class.cast(findViewById(R.id.symbol_input_candidate_view)); |
| if (symbolCandidateView != null) { |
| symbolCandidateView.reset(); |
| } |
| } |
| |
| if (newCategory != SymbolMajorCategory.NUMBER && viewEventListener.isPresent()) { |
| viewEventListener.get().onSubmitPreedit(); |
| } |
| |
| if (newCategory == SymbolMajorCategory.NUMBER) { |
| CandidateView candidateView = |
| CandidateView.class.cast(findViewById(R.id.candidate_view_in_symbol_view)); |
| candidateView.clearAnimation(); |
| candidateView.setVisibility(View.GONE); |
| candidateView.reset(); |
| } |
| |
| currentMajorCategory = newCategory; |
| |
| if (currentMajorCategory == SymbolMajorCategory.NUMBER) { |
| findViewById(android.R.id.tabhost).setVisibility(View.GONE); |
| findViewById(R.id.number_frame).setVisibility(View.VISIBLE); |
| setNumberKeyboard(); |
| } else { |
| findViewById(android.R.id.tabhost).setVisibility(View.VISIBLE); |
| findViewById(R.id.number_frame).setVisibility(View.GONE); |
| updateMinorCategory(); |
| } |
| |
| // Hide overlapping separator |
| if (currentMajorCategory == SymbolMajorCategory.NUMBER) { |
| findViewById(R.id.symbol_view_close_button_separator).setVisibility(View.INVISIBLE); |
| } else { |
| findViewById(R.id.symbol_view_close_button_separator).setVisibility(View.VISIBLE); |
| } |
| |
| if (currentMajorCategory == SymbolMajorCategory.EMOJI) { |
| findViewById(R.id.symbol_view_enter_button_separator).setVisibility(View.INVISIBLE); |
| } else { |
| findViewById(R.id.symbol_view_enter_button_separator).setVisibility(View.VISIBLE); |
| } |
| |
| // Update visibility relating attributes. |
| for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) { |
| // Update major category selector button's look and feel. |
| MozcImageButton button = getMajorCategoryButton(majorCategory); |
| if (button != null) { |
| button.setSelected(majorCategory == currentMajorCategory); |
| button.setEnabled(majorCategory != currentMajorCategory); |
| } |
| } |
| |
| View emojiDisabledMessageView = getEmojiDisabledMessageView(); |
| if (emojiDisabledMessageView != null) { |
| // Show messages about emoji-disabling, if necessary. |
| emojiDisabledMessageView.setVisibility( |
| currentMajorCategory == SymbolMajorCategory.EMOJI |
| && !emojiEnabled ? View.VISIBLE : View.GONE); |
| } |
| } |
| |
| private void setNumberKeyboard() { |
| final KeyboardSpecification spec = KeyboardSpecification.SYMBOL_NUMBER; |
| final KeyboardFactory factory = new KeyboardFactory(); |
| |
| getNumberKeyboardView().addOnLayoutChangeListener(new OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange( |
| View view, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| if (right - left == 0 || bottom - top == 0) { |
| return; |
| } |
| KeyboardView keyboardView = KeyboardView.class.cast(view); |
| Keyboard keyboard = factory.get(getResources(), spec, right - left, bottom - top); |
| keyboardView.setKeyboard(keyboard); |
| keyboardView.invalidate(); |
| } |
| }); |
| } |
| |
| private void updateMinorCategory() { |
| // Reset the minor category to the default value. |
| resetTabImageForMinorCategory(); |
| resetCandidateViewPager(); |
| SymbolMinorCategory minorCategory = currentMajorCategory.getDefaultMinorCategory(); |
| Preconditions.checkState(symbolCandidateStorage.isPresent()); |
| if (symbolCandidateStorage.get().getCandidateList(minorCategory).getCandidatesCount() == 0) { |
| minorCategory = currentMajorCategory.getMinorCategoryByRelativeIndex(minorCategory, 1); |
| } |
| int index = currentMajorCategory.minorCategories.indexOf(minorCategory); |
| getCandidateViewPager().setCurrentItem(index); |
| |
| // Disable feedback before setting the current tab programatically. |
| // Background: TabHost.setCurrentTab calls back onTabChanged, in which feedback event is fired. |
| // However, we don't have ways to distinguish if onTabChanged is called through user click |
| // event or by the call of setCurrentTab. If we don't disable feedback here, the click sound |
| // effect is fired twice; one is from the onClick event on major category tab and the other is |
| // by the call of setCurrentTab here. See b/17119766. |
| SymbolTabWidgetViewPagerAdapter adapter = |
| SymbolTabWidgetViewPagerAdapter.class.cast(getCandidateViewPager().getAdapter()); |
| adapter.setFeedbackEnabled(false); |
| getTabHost().setCurrentTab(index); |
| adapter.setFeedbackEnabled(true); |
| } |
| |
| void setEmojiProviderType(EmojiProviderType emojiProviderType) { |
| Preconditions.checkNotNull(emojiProviderType); |
| |
| Preconditions.checkState(symbolCandidateStorage.isPresent()); |
| this.emojiProviderType = emojiProviderType; |
| this.symbolCandidateStorage.get().setEmojiProviderType(emojiProviderType); |
| if (!isInflated()) { |
| return; |
| } |
| |
| resetCandidateViewPager(); |
| } |
| |
| void setEventListener( |
| ViewEventListener viewEventListener, OnClickListener closeButtonClickListener, |
| OnClickListener microphoneButtonClickListener) { |
| this.viewEventListener = Optional.of(viewEventListener); |
| this.closeButtonClickListener = Optional.of(closeButtonClickListener); |
| this.microphoneButtonClickListener = Optional.of(microphoneButtonClickListener); |
| } |
| |
| void setMicrophoneButtonEnabled(boolean enabled) { |
| isMicrophoneButtonEnabled = enabled; |
| if (isInflated()) { |
| getMicrophoneButton().setVisibility(enabled ? VISIBLE : GONE); |
| } |
| } |
| |
| void setSkin(Skin skin) { |
| Preconditions.checkNotNull(skin); |
| if (this.skin.equals(skin)) { |
| return; |
| } |
| this.skin = skin; |
| majorCategoryButtonDrawableFactory.setSkin(skin); |
| if (!isInflated()) { |
| return; |
| } |
| updateSkinAwareDrawable(); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void updateSkinAwareDrawable() { |
| updateTabBackgroundSkin(); |
| resetTabImageForMinorCategory(); |
| |
| SymbolTabWidgetViewPagerAdapter adapter = |
| SymbolTabWidgetViewPagerAdapter.class.cast(getCandidateViewPager().getAdapter()); |
| if (adapter != null) { |
| adapter.setSkin(skin); |
| } |
| updateMajorCategoryBackgroundSkin(); |
| updateMajorCategoryButtonsSkin(); |
| updateMinorCategoryBackgroundSkin(); |
| updateNumberKeyboardSkin(); |
| updateSeparatorsSkin(); |
| getMicrophoneButton().setSkin(skin); |
| |
| TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs)); |
| for (int i = 0; i < tabWidget.getChildCount(); ++i) { |
| MozcImageView.class.cast(tabWidget.getChildTabViewAt(i)).setSkin(skin); |
| } |
| |
| // Note delete button shouldn't be applied createMajorButtonBackgroundDrawable as background |
| // as it should show different background (same as minor categories). |
| for (int id : new int[] {R.id.symbol_view_close_button, |
| R.id.symbol_view_enter_button}) { |
| MozcImageView view = MozcImageView.class.cast(findViewById(id)); |
| view.setSkin(skin); |
| view.setBackgroundDrawable(createMajorButtonBackgroundDrawable(skin)); |
| } |
| MozcImageView deleteKeyView = |
| MozcImageView.class.cast(findViewById(R.id.symbol_view_delete_button)); |
| deleteKeyView.setSkin(skin); |
| deleteKeyView.setBackgroundDrawable(createMinorButtonBackgroundDrawable(skin)); |
| } |
| |
| private KeyboardView getNumberKeyboardView() { |
| return KeyboardView.class.cast(findViewById(R.id.number_keyboard)); |
| } |
| |
| private LinearLayout getMajorCategoryFrame() { |
| return LinearLayout.class.cast(findViewById(R.id.symbol_major_category)); |
| } |
| |
| private LinearLayout getMinorCategoryFrame() { |
| return LinearLayout.class.cast(findViewById(R.id.symbol_minor_category)); |
| } |
| |
| private TabWidget getTabWidget() { |
| return TabWidget.class.cast(findViewById(android.R.id.tabs)); |
| } |
| |
| private MozcImageView getMicrophoneButton() { |
| return MozcImageView.class.cast(findViewById(R.id.microphone_button)); |
| } |
| |
| @Override |
| protected void onSizeChanged(int w, int h, int oldw, int oldh) { |
| super.onSizeChanged(w, h, oldw, oldh); |
| // The boundary of Drawable instance which has been set as background |
| // is not updated automatically. |
| // Update the boundary below. |
| if (isInflated()) { |
| updateSkinAwareDrawable(); |
| } |
| } |
| |
| @Override |
| public void trimMemory() { |
| ViewGroup viewGroup = getCandidateViewPager(); |
| if (viewGroup == null) { |
| return; |
| } |
| for (int i = 0; i < viewGroup.getChildCount(); ++i) { |
| View view = viewGroup.getChildAt(i); |
| if (view instanceof MemoryManageable) { |
| MemoryManageable.class.cast(view).trimMemory(); |
| } |
| } |
| } |
| } |