| // 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.accessibility.AccessibilityUtil; |
| import org.mozc.android.inputmethod.japanese.accessibility.CandidateWindowAccessibilityDelegate; |
| 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.protobuf.ProtoCandidates.CandidateList; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayout; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Row; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayouter; |
| import org.mozc.android.inputmethod.japanese.ui.SnapScroller; |
| import org.mozc.android.inputmethod.japanese.view.SkinType; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Paint.Align; |
| import android.graphics.RectF; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v4.widget.EdgeEffectCompat; |
| import android.util.AttributeSet; |
| import android.view.GestureDetector; |
| import android.view.GestureDetector.SimpleOnGestureListener; |
| import android.view.MotionEvent; |
| import android.view.View; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * A view for candidate words. |
| * |
| */ |
| // TODO(matsuzakit): Optional is introduced partially. Complete introduction. |
| abstract class CandidateWordView extends View implements MemoryManageable { |
| |
| /** |
| * Handles gestures to scroll candidate list and choose a candidate. |
| */ |
| class CandidateWordGestureDetector { |
| class CandidateWordViewGestureListener extends SimpleOnGestureListener { |
| @Override |
| public boolean onFling( |
| MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { |
| float velocity = orientationTrait.projectVector(velocityX, velocityY); |
| // As fling is started, current action is not tapping. |
| // Reset pressing state so that candidate selection is not triggered at touch up event. |
| releaseCandidate(); |
| // Fling makes scrolling. |
| scroller.fling(-(int) velocity); |
| invalidate(); |
| return true; |
| } |
| |
| @Override |
| public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { |
| float distance = orientationTrait.projectVector(distanceX, distanceY); |
| int oldScrollPosition = scroller.getScrollPosition(); |
| int oldMaxScrollPosition = scroller.getMaxScrollPosition(); |
| scroller.scrollBy((int) distance); |
| orientationTrait.scrollTo(CandidateWordView.this, scroller.getScrollPosition()); |
| // As scroll is started, current action is not tapping. |
| // Reset pressing state so that candidate selection is not triggered at touch up event. |
| releaseCandidate(); |
| |
| // Edge effect. Now, in production, we only support vertical scroll. |
| if (oldScrollPosition + distance < 0) { |
| topEdgeEffect.onPull(distance / getHeight()); |
| if (!bottomEdgeEffect.isFinished()) { |
| bottomEdgeEffect.onRelease(); |
| } |
| } else if (oldScrollPosition + distance > oldMaxScrollPosition) { |
| bottomEdgeEffect.onPull(distance / getHeight()); |
| if (!topEdgeEffect.isFinished()) { |
| topEdgeEffect.onRelease(); |
| } |
| } |
| |
| invalidate(); |
| return true; |
| } |
| } |
| |
| // GestureDetector cannot handle all complex gestures which we need. |
| // But we use GestureDetector for some gesture recognition |
| // because implementing whole gesture detection logic by ourselves is a bit tedious. |
| final GestureDetector gestureDetector; |
| |
| /** |
| * Points to an instance of currently pressed candidate word. Or {@code null} if any |
| * candidates aren't pressed. |
| */ |
| @Nullable |
| private CandidateWord pressedCandidate; |
| private final RectF candidateRect = new RectF(); |
| private Optional<Integer> pressedRowIndex = Optional.absent(); |
| |
| public CandidateWordGestureDetector(Context context) { |
| gestureDetector = new GestureDetector(context, new CandidateWordViewGestureListener()); |
| } |
| |
| private void pressCandidate(int rowIndex, Span span) { |
| Row row = calculatedLayout.getRowList().get(rowIndex); |
| pressedRowIndex = Optional.of(rowIndex); |
| pressedCandidate = span.getCandidateWord().orNull(); |
| // TODO(yamaguchi):maybe better to make this rect larger by several pixels to avoid that |
| // users fail to select a candidate by unconscious small movement of tap point. |
| // (i.e. give hysterisis for noise reduction) |
| // Needs UX study. |
| candidateRect.set(span.getLeft(), row.getTop(), |
| span.getRight(), row.getTop() + row.getHeight()); |
| } |
| |
| private void releaseCandidate() { |
| pressedCandidate = null; |
| pressedRowIndex = Optional.absent(); |
| } |
| |
| CandidateWord getPressedCandidate() { |
| return pressedCandidate; |
| } |
| |
| /** |
| * Checks if a down event is fired inside a candidate rectangle. |
| * If so, begin pressing it. |
| * |
| * It is assumed that rows are stored in up-to-down order, |
| * and spans are in left-to-right order. |
| * |
| * @param scrolledX X coordinate of down event point including scroll offset |
| * @param scrolledY Y coordinate of down event point including scroll offset |
| * @return true if the down event is fired inside a candidate rectangle. |
| */ |
| private boolean findCandidateAndPress(float scrolledX, float scrolledY) { |
| if (calculatedLayout == null) { |
| return false; |
| } |
| for (int rowIndex = 0; rowIndex < calculatedLayout.getRowList().size(); ++rowIndex) { |
| Row row = calculatedLayout.getRowList().get(rowIndex); |
| if (scrolledY < row.getTop()) { |
| break; |
| } |
| if (scrolledY >= row.getTop() + row.getHeight()) { |
| continue; |
| } |
| for (Span span : row.getSpanList()) { |
| if (scrolledX < span.getLeft()) { |
| break; |
| } |
| if (scrolledX >= span.getRight()) { |
| continue; |
| } |
| pressCandidate(rowIndex, span); |
| invalidate(); |
| return true; |
| } |
| return false; |
| } |
| return false; |
| } |
| |
| boolean onTouchEvent(MotionEvent event) { |
| if (gestureDetector.onTouchEvent(event)) { |
| return true; |
| } |
| |
| float scrolledX = event.getX() + getScrollX(); |
| float scrolledY = event.getY() + getScrollY(); |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| findCandidateAndPress(scrolledX, scrolledY); |
| scroller.stopScrolling(); |
| if (!topEdgeEffect.isFinished()) { |
| topEdgeEffect.onRelease(); |
| } |
| if (!bottomEdgeEffect.isFinished()) { |
| bottomEdgeEffect.onRelease(); |
| } |
| return true; |
| case MotionEvent.ACTION_MOVE: |
| if (pressedCandidate != null) { |
| // Turn off highlighting if contact point gets out of the candidate. |
| if (!candidateRect.contains(scrolledX, scrolledY)) { |
| releaseCandidate(); |
| invalidate(); |
| } |
| } |
| return true; |
| case MotionEvent.ACTION_CANCEL: |
| if (pressedCandidate != null) { |
| releaseCandidate(); |
| invalidate(); |
| } |
| return true; |
| case MotionEvent.ACTION_UP: |
| if (pressedCandidate != null) { |
| if (candidateRect.contains(scrolledX, scrolledY) && candidateSelectListener != null) { |
| candidateSelectListener.onCandidateSelected(pressedCandidate, pressedRowIndex); |
| } |
| releaseCandidate(); |
| invalidate(); |
| } |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Polymorphic behavior based on scroll orientation. |
| */ |
| // TODO(hidehiko): rename OrientationTrait to OrientationTraits. |
| interface OrientationTrait { |
| /** @return scroll position of which direction corresponds to the orientation. */ |
| int getScrollPosition(View view); |
| |
| /** @return the projected value. */ |
| float projectVector(float x, float y); |
| |
| /** Scrolls to {@code position}. {@code position} is applied to corresponding axis. */ |
| void scrollTo(View view, int position); |
| |
| /** @return left or top position based on the orientation. */ |
| float getCandidatePosition(Row row, Span span); |
| |
| /** @return width or height based on the orientation. */ |
| float getCandidateLength(Row row, Span span); |
| |
| /** @return view's width or height based on the orientation. */ |
| int getViewLength(View view); |
| |
| /** @return the page size of the layout for the scroll orientation. */ |
| int getPageSize(CandidateLayouter layouter); |
| |
| /** @return the content size for the scroll orientation of the layout. 0 for absent. */ |
| float getContentSize(Optional<CandidateLayout> layout); |
| } |
| |
| enum Orientation implements OrientationTrait { |
| HORIZONTAL { |
| @Override |
| public int getScrollPosition(View view) { |
| return view.getScrollX(); |
| } |
| @Override |
| public void scrollTo(View view, int position) { |
| view.scrollTo(position, 0); |
| } |
| @Override |
| public float getCandidatePosition(Row row, Span span) { |
| return span.getLeft(); |
| } |
| @Override |
| public float getCandidateLength(Row row, Span span) { |
| return span.getWidth(); |
| } |
| @Override |
| public int getViewLength(View view) { |
| return view.getWidth(); |
| } |
| @Override |
| public float projectVector(float x, float y) { |
| return x; |
| } |
| @Override |
| public int getPageSize(CandidateLayouter layouter) { |
| return Preconditions.checkNotNull(layouter).getPageWidth(); |
| } |
| @Override |
| public float getContentSize(Optional<CandidateLayout> layout) { |
| return layout.isPresent() ? layout.get().getContentWidth() : 0; |
| } |
| }, |
| VERTICAL { |
| @Override |
| public int getScrollPosition(View view) { |
| return view.getScrollY(); |
| } |
| @Override |
| public void scrollTo(View view, int position) { |
| view.scrollTo(0, position); |
| } |
| @Override |
| public float getCandidatePosition(Row row, Span span) { |
| return row.getTop(); |
| } |
| @Override |
| public float getCandidateLength(Row row, Span span) { |
| return row.getHeight(); |
| } |
| @Override |
| public int getViewLength(View view) { |
| return view.getHeight(); |
| } |
| @Override |
| public float projectVector(float x, float y) { |
| return y; |
| } |
| @Override |
| public int getPageSize(CandidateLayouter layouter) { |
| return Preconditions.checkNotNull(layouter).getPageHeight(); |
| } |
| @Override |
| public float getContentSize(Optional<CandidateLayout> layout) { |
| return layout.isPresent() ? layout.get().getContentHeight() : 0; |
| } |
| }; |
| } |
| |
| private CandidateSelectListener candidateSelectListener; |
| |
| // Finally, we only need vertical scrolling. |
| // TODO(hidehiko): Remove horizontal scrolling related codes. |
| private final EdgeEffectCompat topEdgeEffect = new EdgeEffectCompat(getContext()); |
| private final EdgeEffectCompat bottomEdgeEffect = new EdgeEffectCompat(getContext()); |
| |
| // The Scroller which manages the status of scrolling the view. |
| // Default behavior of ScrollView does not suffice our UX design |
| // so we introduced this Scroller. |
| // TODO(matsuzakit): The parameter is TBD (needs UX study?). |
| protected final SnapScroller scroller = new SnapScroller(); |
| // The CandidateLayouter which calculates the layout of candidate words. |
| // This fields is not final but must be set in initialization in the subclasses. |
| @VisibleForTesting CandidateLayouter layouter; |
| // The calculated layout, created by this.layouter. |
| protected CandidateLayout calculatedLayout; |
| // The CandidateList which is currently shown on the view. |
| protected CandidateList currentCandidateList; |
| // The Y position where the last touch event occurs. |
| float lastEventPosition; |
| |
| // No padding by default. |
| private int horizontalPadding = 0; |
| |
| protected final CandidateLayoutRenderer candidateLayoutRenderer = |
| new CandidateLayoutRenderer(this); |
| |
| CandidateWordGestureDetector candidateWordGestureDetector = |
| new CandidateWordGestureDetector(getContext()); |
| |
| // Scroll orientation. |
| private final OrientationTrait orientationTrait; |
| |
| protected final BackgroundDrawableFactory backgroundDrawableFactory = |
| new BackgroundDrawableFactory(getResources().getDisplayMetrics().density); |
| private DrawableType backgroundDrawableType = null; |
| |
| private final CandidateWindowAccessibilityDelegate accessibilityDelegate; |
| |
| CandidateWordView(Context context, OrientationTrait orientationFeature) { |
| super(context); |
| this.orientationTrait = orientationFeature; |
| } |
| |
| CandidateWordView(Context context, AttributeSet attributeSet, |
| OrientationTrait orientationTrait) { |
| super(context, attributeSet); |
| this.orientationTrait = orientationTrait; |
| } |
| |
| CandidateWordView(Context context, AttributeSet attributeSet, int defaultStyle, |
| OrientationTrait orientationTrait) { |
| super(context, attributeSet, defaultStyle); |
| this.orientationTrait = orientationTrait; |
| } |
| |
| { |
| accessibilityDelegate = new CandidateWindowAccessibilityDelegate(this); |
| ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate); |
| } |
| |
| void setCandidateSelectListener(CandidateSelectListener candidateSelectListener) { |
| this.candidateSelectListener = candidateSelectListener; |
| } |
| |
| CandidateLayouter getCandidateLayouter() { |
| return layouter; |
| } |
| |
| protected void setHorizontalPadding(int horizontalPadding) { |
| this.horizontalPadding = horizontalPadding; |
| updateLayouter(); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| super.onLayout(changed, left, top, right, bottom); |
| int width = Math.max(right - left - horizontalPadding * 2, 0); |
| int height = bottom - top; |
| if (layouter != null && layouter.setViewSize(width, height)) { |
| updateCalculatedLayout(); |
| } |
| updateScroller(); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| return candidateWordGestureDetector.onTouchEvent(event); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| candidateLayoutRenderer.onAttachedToWindow(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| candidateLayoutRenderer.onDetachedFromWindow(); |
| super.onDetachedFromWindow(); |
| } |
| |
| public void setEmojiProviderType(EmojiProviderType providerType) { |
| Preconditions.checkNotNull(providerType); |
| |
| candidateLayoutRenderer.setEmojiProviderType(providerType); |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| super.draw(canvas); |
| |
| // Render edge effect. |
| boolean postInvalidateIsNeeded = false; |
| if (!topEdgeEffect.isFinished()) { |
| int saveCount = canvas.save(); |
| try { |
| canvas.translate(0, Math.min(0, getScrollY())); |
| topEdgeEffect.setSize(getWidth(), getHeight()); |
| if (topEdgeEffect.draw(canvas)) { |
| postInvalidateIsNeeded = true; |
| } |
| } finally { |
| canvas.restoreToCount(saveCount); |
| } |
| } |
| |
| if (!bottomEdgeEffect.isFinished()) { |
| int saveCount = canvas.save(); |
| try { |
| int width = getWidth(); |
| int height = getHeight(); |
| canvas.translate(-width, getScrollY() + height); |
| canvas.rotate(180, width, 0); |
| bottomEdgeEffect.setSize(width, height); |
| if (bottomEdgeEffect.draw(canvas)) { |
| postInvalidateIsNeeded = true; |
| } |
| } finally { |
| canvas.restoreToCount(saveCount); |
| } |
| } |
| |
| if (postInvalidateIsNeeded) { |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| if (calculatedLayout == null || currentCandidateList == null) { |
| // No layout is available. |
| return; |
| } |
| |
| // Paint the candidates. |
| int saveCount = canvas.save(); |
| try { |
| canvas.translate(horizontalPadding, 0); |
| CandidateWord pressedCandidate = candidateWordGestureDetector.getPressedCandidate(); |
| int pressedCandidateIndex = (pressedCandidate != null && pressedCandidate.hasIndex()) |
| ? pressedCandidate.getIndex() : -1; |
| candidateLayoutRenderer.drawCandidateLayout(canvas, calculatedLayout, pressedCandidateIndex); |
| } finally { |
| canvas.restoreToCount(saveCount); |
| } |
| } |
| |
| @Override |
| public final void computeScroll() { |
| if (scroller.isScrolling()) { |
| // If still scrolling, update the scroll position and invalidate the window. |
| Optional<Float> optionalVelocity = scroller.computeScrollOffset(); |
| orientationTrait.scrollTo(this, scroller.getScrollPosition()); |
| if (optionalVelocity.isPresent()) { |
| Float velocity = optionalVelocity.get(); |
| // The end of scrolling. Check edge effect. |
| if (velocity < 0) { |
| topEdgeEffect.onAbsorb(velocity.intValue()); |
| if (!bottomEdgeEffect.isFinished()) { |
| bottomEdgeEffect.onRelease(); |
| } |
| } else if (velocity > 0) { |
| bottomEdgeEffect.onAbsorb(velocity.intValue()); |
| if (!topEdgeEffect.isFinished()) { |
| topEdgeEffect.onRelease(); |
| } |
| } |
| } |
| |
| // This invalidation makes next scrolling. |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| super.computeScroll(); |
| } |
| |
| @VisibleForTesting int getUpdatedScrollPosition(Row row, Span span) { |
| int scrollPosition = orientationTrait.getScrollPosition(this); |
| float candidatePosition = orientationTrait.getCandidatePosition(row, span); |
| float candidateLength = orientationTrait.getCandidateLength(row, span); |
| int viewLength = orientationTrait.getViewLength(this); |
| if (candidatePosition < scrollPosition || |
| candidatePosition + candidateLength > scrollPosition + viewLength) { |
| return (int) candidatePosition; |
| } else { |
| return scrollPosition; |
| } |
| } |
| |
| /** |
| * If focused candidate is invisible (including partial invisible), |
| * update scroll position to see the candidate. |
| */ |
| protected void updateScrollPositionBasedOnFocusedIndex() { |
| int scrollPosition = 0; |
| if (calculatedLayout != null && currentCandidateList != null) { |
| int focusedIndex = currentCandidateList.getFocusedIndex(); |
| row_loop: for (Row row : calculatedLayout.getRowList()) { |
| for (Span span : row.getSpanList()) { |
| if (!span.getCandidateWord().isPresent()) { |
| continue; |
| } |
| if (span.getCandidateWord().get().getIndex() == focusedIndex) { |
| scrollPosition = getUpdatedScrollPosition(row, span); |
| break row_loop; |
| } |
| } |
| } |
| } |
| |
| setScrollPosition(scrollPosition); |
| } |
| |
| void setScrollPosition(int position) { |
| scroller.scrollTo(position); |
| orientationTrait.scrollTo(this, scroller.getScrollPosition()); |
| invalidate(); |
| } |
| |
| void update(CandidateList candidateList) { |
| CandidateList previousCandidateList = currentCandidateList; |
| currentCandidateList = candidateList; |
| candidateLayoutRenderer.setCandidateList(Optional.fromNullable(candidateList)); |
| if (layouter != null && !equals(candidateList, previousCandidateList)) { |
| updateCalculatedLayout(); |
| } |
| updateScroller(); |
| invalidate(); |
| } |
| |
| private static boolean equals(CandidateList list1, CandidateList list2) { |
| if (list1 == list2) { |
| return true; |
| } |
| if (list1 == null || list2 == null) { |
| return false; |
| } |
| |
| return list1.getCandidatesList().equals(list2.getCandidatesList()); |
| } |
| |
| /** |
| * Updates the layouter, and also updates the calculatedLayout based on the updated layouter. |
| * |
| * TODO(hidehiko): This method is remaining here to reduce a CL size smaller |
| * in order to make refactoring step by step. This will be cleaned when CandidateWordView |
| * is refactored. |
| */ |
| protected final void updateLayouter() { |
| updateCalculatedLayout(); |
| updateScroller(); |
| } |
| |
| /** |
| * Updates the calculatedLayout if possible. |
| */ |
| private void updateCalculatedLayout() { |
| if (currentCandidateList == null || layouter == null) { |
| calculatedLayout = null; |
| } else { |
| calculatedLayout = layouter.layout(currentCandidateList).orNull(); |
| } |
| Optional<CandidateLayout> candidateLayout = Optional.fromNullable(calculatedLayout); |
| accessibilityDelegate.setCandidateLayout( |
| candidateLayout, |
| (int) orientationTrait.getContentSize(candidateLayout), |
| orientationTrait.getViewLength(this)); |
| } |
| |
| private void updateScroller() { |
| if (calculatedLayout == null || layouter == null) { |
| scroller.setPageSize(0); |
| scroller.setContentSize(0); |
| } else { |
| int pageSize = orientationTrait.getPageSize(layouter); |
| int contentSize = |
| (int) orientationTrait.getContentSize(Optional.fromNullable(calculatedLayout)); |
| if (pageSize != 0) { |
| // Ceil to align pages. |
| contentSize = (contentSize + pageSize - 1) / pageSize * pageSize; |
| } |
| scroller.setPageSize(pageSize); |
| scroller.setContentSize(contentSize); |
| } |
| scroller.setViewSize(orientationTrait.getViewLength(this)); |
| } |
| |
| public CandidateList getCandidateList() { |
| return currentCandidateList; |
| } |
| |
| /** |
| * Utility method for creating paint instance. |
| */ |
| protected static Paint createPaint( |
| boolean antiAlias, int color, Align textAlign, float textSize) { |
| Paint paint = new Paint(); |
| paint.setAntiAlias(antiAlias); |
| paint.setColor(color); |
| paint.setTextAlign(textAlign); |
| paint.setTextSize(textSize); |
| return paint; |
| } |
| |
| protected void setBackgroundDrawableType(DrawableType drawableType) { |
| backgroundDrawableType = drawableType; |
| resetBackground(); |
| } |
| |
| private void resetBackground() { |
| candidateLayoutRenderer.setSpanBackgroundDrawable( |
| Optional.fromNullable(backgroundDrawableFactory.getDrawable(backgroundDrawableType))); |
| } |
| |
| void setSkinType(SkinType skinType) { |
| backgroundDrawableFactory.setSkinType(skinType); |
| resetBackground(); |
| } |
| |
| @Override |
| public void trimMemory() { |
| calculatedLayout = null; |
| accessibilityDelegate.setCandidateLayout(Optional.<CandidateLayout>absent(), 0, 0); |
| currentCandidateList = null; |
| } |
| |
| @Override |
| protected boolean dispatchHoverEvent(MotionEvent event) { |
| if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { |
| return accessibilityDelegate.dispatchHoverEvent(event); |
| } |
| return false; |
| } |
| } |