| // 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.ProtoCandidates.CandidateList; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Row; |
| import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * Layouts the conversion candidate words. |
| * |
| * First, all the rows this layouter creates are split into "chunk"s. |
| * The width of each chunk is equal to {@code pageWidth / numChunks} evenly. |
| * Next, the candidates are assigned to chunks. The order of the candidates is kept. |
| * A candidate may occupy one or more successive chunks which are on the same row. |
| * |
| * The height of each row is round up to integer, so that the snap-paging |
| * should work well. |
| * |
| */ |
| public class ConversionCandidateLayouter implements CandidateLayouter { |
| |
| /** |
| * The metrics between chunk and span. |
| * |
| * The main purpose of this class is to inject the chunk compression |
| * heuristics for testing. |
| */ |
| static class ChunkMetrics { |
| |
| private final float chunkWidth; |
| private final float compressionRatio; |
| private final float horizontalPadding; |
| private final float minWidth; |
| |
| ChunkMetrics(float chunkWidth, |
| float compressionRatio, |
| float horizontalPadding, |
| float minWidth) { |
| this.chunkWidth = chunkWidth; |
| this.compressionRatio = compressionRatio; |
| this.horizontalPadding = horizontalPadding; |
| this.minWidth = minWidth; |
| } |
| |
| /** Returns the number of chunks which the span would consume. */ |
| int getNumChunks(Span span) { |
| Preconditions.checkNotNull(span); |
| float compressedValueWidth = |
| compressValueWidth(span.getValueWidth(), compressionRatio, horizontalPadding, minWidth); |
| return (int) Math.ceil((compressedValueWidth + span.getDescriptionWidth()) / chunkWidth); |
| } |
| |
| static float compressValueWidth( |
| float valueWidth, float compressionRatio, float horizontalPadding, float minWidth) { |
| // Sum of geometric progression. |
| // a == 1.0 (default pixel size) |
| // r == candidateWidthCompressionRate (pixel width decay rate) |
| // n == defaultWidth |
| if (compressionRatio != 1) { |
| valueWidth = |
| (1f - (float) Math.pow(compressionRatio, valueWidth)) / (1f - compressionRatio); |
| } |
| return Math.max(valueWidth + horizontalPadding * 2, minWidth); |
| } |
| } |
| |
| private Optional<SpanFactory> spanFactory = Optional.absent(); |
| |
| /** Horizontal common ratio of the value size. */ |
| private float valueWidthCompressionRate; |
| |
| /** Minimum width of the value. */ |
| private float minValueWidth; |
| |
| /** The Minimum width of the chunk. */ |
| private float minChunkWidth; |
| |
| /** Height of the value. */ |
| private float valueHeight; |
| |
| private float valueHorizontalPadding; |
| private float valueVerticalPadding; |
| |
| /** The current view's width. */ |
| private int viewWidth; |
| |
| private boolean reserveEmptySpan = false; |
| |
| /** |
| * @param spanFactory the spanFactory to set |
| */ |
| public void setSpanFactory(SpanFactory spanFactory) { |
| this.spanFactory = Optional.of(Preconditions.checkNotNull(spanFactory)); |
| } |
| |
| /** |
| * @param valueWidthCompressionRate the valueWidthCompressionRate to set |
| */ |
| public void setValueWidthCompressionRate(float valueWidthCompressionRate) { |
| this.valueWidthCompressionRate = valueWidthCompressionRate; |
| } |
| |
| /** |
| * @param minValueWidth the minValueWidth to set |
| */ |
| public void setMinValueWidth(float minValueWidth) { |
| this.minValueWidth = minValueWidth; |
| } |
| |
| /** |
| * @param minChunkWidth the minChunkWidth to set |
| */ |
| public void setMinChunkWidth(float minChunkWidth) { |
| this.minChunkWidth = minChunkWidth; |
| } |
| |
| /** |
| * @param valueHeight the valueHeight to set |
| */ |
| public void setValueHeight(float valueHeight) { |
| this.valueHeight = valueHeight; |
| } |
| |
| /** |
| * @param valueHorizontalPadding the valueHorizontalPadding to set |
| */ |
| public void setValueHorizontalPadding(float valueHorizontalPadding) { |
| this.valueHorizontalPadding = valueHorizontalPadding; |
| } |
| |
| /** |
| * @param valueVerticalPadding the valueVerticalPadding to set |
| */ |
| public void setValueVerticalPadding(float valueVerticalPadding) { |
| this.valueVerticalPadding = valueVerticalPadding; |
| } |
| |
| @Override |
| public boolean setViewSize(int width, int height) { |
| if (viewWidth == width) { |
| // Doesn't need to invalidate the layout if the width isn't changed. |
| return false; |
| } |
| viewWidth = width; |
| return true; |
| } |
| |
| private int getNumChunks() { |
| return (int) (viewWidth / minChunkWidth); |
| } |
| |
| public float getChunkWidth() { |
| return viewWidth / (float) getNumChunks(); |
| } |
| |
| @Override |
| public int getPageWidth() { |
| return Math.max(viewWidth, 0); |
| } |
| |
| public int getRowHeight() { |
| return (int) Math.ceil(valueHeight + valueVerticalPadding * 2); |
| } |
| |
| @Override |
| public int getPageHeight() { |
| return getRowHeight(); |
| } |
| |
| @Override |
| public Optional<CandidateLayout> layout(CandidateList candidateList) { |
| Preconditions.checkNotNull(candidateList); |
| if (minChunkWidth <= 0 || viewWidth <= 0 || candidateList.getCandidatesCount() == 0 || |
| !spanFactory.isPresent()) { |
| return Optional.<CandidateLayout>absent(); |
| } |
| |
| int numChunks = getNumChunks(); |
| float chunkWidth = getChunkWidth(); |
| ChunkMetrics chunkMetrics = new ChunkMetrics( |
| chunkWidth, valueWidthCompressionRate, valueHorizontalPadding, minValueWidth); |
| List<Row> rowList = buildRowList(candidateList, spanFactory.get(), numChunks, chunkMetrics, |
| reserveEmptySpan); |
| int[] numAllocatedChunks = new int[numChunks]; |
| boolean isFirst = reserveEmptySpan; |
| for (Row row : rowList) { |
| layoutSpanList( |
| row.getSpanList(), |
| (isFirst ? (viewWidth - (int) chunkWidth) : viewWidth), |
| (isFirst ? numChunks - 1 : numChunks), |
| chunkMetrics, numAllocatedChunks); |
| isFirst = false; |
| } |
| |
| // Push empty span at the end of the first row. |
| if (reserveEmptySpan) { |
| Span emptySpan = new Span(Optional.<CandidateWord>absent(), 0, 0, |
| Collections.<String>emptyList()); |
| List<Span> spanList = rowList.get(0).getSpanList(); |
| emptySpan.setLeft(spanList.get(spanList.size() - 1).getRight()); |
| emptySpan.setRight(viewWidth); |
| rowList.get(0).addSpan(emptySpan); |
| } |
| |
| // In order to snap the scrolling on any row boundary, rounding up the rowHeight |
| // to align pixels. |
| int rowHeight = getRowHeight(); |
| layoutRowList(rowList, viewWidth, rowHeight); |
| |
| return Optional.of(new CandidateLayout(rowList, viewWidth, rowHeight * rowList.size())); |
| } |
| |
| /** |
| * Builds the row list based on the number of estimated chunks for each span. |
| * |
| * The order of the candidates will be kept. |
| */ |
| @VisibleForTesting |
| static List<Row> buildRowList( |
| CandidateList candidateList, SpanFactory spanFactory, |
| int numChunks, ChunkMetrics chunkMetrics, boolean enableSpan) { |
| Preconditions.checkNotNull(candidateList); |
| Preconditions.checkNotNull(spanFactory); |
| Preconditions.checkNotNull(chunkMetrics); |
| |
| List<Row> rowList = new ArrayList<Row>(); |
| |
| int numRemainingChunks = 0; |
| for (CandidateWord candidateWord : candidateList.getCandidatesList()) { |
| Span span = spanFactory.newInstance(candidateWord); |
| int numSpanChunks = chunkMetrics.getNumChunks(span); |
| if (numRemainingChunks < numSpanChunks) { |
| // There is no space on the current row to put the current span. |
| // Create a new row. |
| numRemainingChunks = numChunks; |
| |
| // For the first line, we reserve a chunk at right-top in order to place an icon |
| // button for folding/expanding keyboard. |
| if (enableSpan && rowList.isEmpty()) { |
| numRemainingChunks--; |
| } |
| |
| rowList.add(new Row()); |
| } |
| |
| // Add the span to the last row. |
| rowList.get(rowList.size() - 1).addSpan(span); |
| numRemainingChunks -= numSpanChunks; |
| } |
| |
| return rowList; |
| } |
| |
| /** |
| * Sets left and right of each span. The left and right should be aligned to the chunks. |
| * Also, the right of the last span should be equal to {@code pageWidth}. |
| * |
| * In order to avoid integer array memory allocation (as this method will be invoked |
| * many times to layout a {@link CandidateList}), it is necessary to pass an integer |
| * array for the calculation buffer, {@code numAllocatedChunks}. |
| * The size of the buffer must be equal to or greater than {@code spanList.size()}. |
| * Its elements needn't be initialized. |
| */ |
| @VisibleForTesting |
| static void layoutSpanList( |
| List<Span> spanList, int pageWidth, |
| int numChunks, ChunkMetrics chunkMetrics, int[] numAllocatedChunks) { |
| Preconditions.checkNotNull(spanList); |
| Preconditions.checkNotNull(chunkMetrics); |
| Preconditions.checkNotNull(numAllocatedChunks); |
| Preconditions.checkArgument(spanList.size() <= numAllocatedChunks.length); |
| |
| int numRemainingChunks = numChunks; |
| // First, allocate the chunks based on the metrics. |
| { |
| int index = 0; |
| for (Span span : spanList) { |
| int numSpanChunks = Math.min(numRemainingChunks, chunkMetrics.getNumChunks(span)); |
| numAllocatedChunks[index] = numSpanChunks; |
| numRemainingChunks -= numSpanChunks; |
| ++index; |
| } |
| } |
| |
| // Then assign remaining chunks to each span as even as possible by round-robin. |
| for (int index = 0; numRemainingChunks > 0; |
| --numRemainingChunks, index = (index + 1) % spanList.size()) { |
| ++numAllocatedChunks[index]; |
| } |
| |
| // Set the actual left and right to each span. |
| { |
| int index = 0; |
| float left = 0; |
| float spanWidth = pageWidth / (float) numChunks; |
| int cumulativeNumAllocatedChunks = 0; |
| for (Span span : spanList) { |
| cumulativeNumAllocatedChunks += numAllocatedChunks[index++]; |
| float right = Math.min(spanWidth * cumulativeNumAllocatedChunks, pageWidth); |
| span.setLeft(left); |
| span.setRight(right); |
| left = right; |
| } |
| } |
| |
| // Set the right of the last element to the pageWidth to align the page. |
| spanList.get(spanList.size() - 1).setRight(pageWidth); |
| } |
| |
| /** Sets top, width and height to the each row. */ |
| @VisibleForTesting |
| static void layoutRowList(List<Row> rowList, int pageWidth, int rowHeight) { |
| int top = 0; |
| for (Row row : Preconditions.checkNotNull(rowList)) { |
| row.setTop(top); |
| row.setWidth(pageWidth); |
| row.setHeight(rowHeight); |
| top += rowHeight; |
| } |
| } |
| |
| public void reserveEmptySpanForInputFoldButton(boolean reserveEmptySpan) { |
| this.reserveEmptySpan = reserveEmptySpan; |
| } |
| } |