PageRenderTime 342ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/android/src/com/google/android/inputmethod/japanese/ui/ConversionCandidateLayouter.java

https://gitlab.com/somiyagawa/mozc
Java | 361 lines | 206 code | 49 blank | 106 comment | 18 complexity | 8da5582f723f04d193321d39c042a4c1 MD5 | raw file
  1. // Copyright 2010-2016, Google Inc.
  2. // All rights reserved.
  3. //
  4. // Redistribution and use in source and binary forms, with or without
  5. // modification, are permitted provided that the following conditions are
  6. // met:
  7. //
  8. // * Redistributions of source code must retain the above copyright
  9. // notice, this list of conditions and the following disclaimer.
  10. // * Redistributions in binary form must reproduce the above
  11. // copyright notice, this list of conditions and the following disclaimer
  12. // in the documentation and/or other materials provided with the
  13. // distribution.
  14. // * Neither the name of Google Inc. nor the names of its
  15. // contributors may be used to endorse or promote products derived from
  16. // this software without specific prior written permission.
  17. //
  18. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. package org.mozc.android.inputmethod.japanese.ui;
  30. import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList;
  31. import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord;
  32. import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Row;
  33. import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span;
  34. import com.google.common.annotations.VisibleForTesting;
  35. import com.google.common.base.Optional;
  36. import com.google.common.base.Preconditions;
  37. import java.util.ArrayList;
  38. import java.util.Collections;
  39. import java.util.List;
  40. /**
  41. * Layouts the conversion candidate words.
  42. *
  43. * First, all the rows this layouter creates are split into "chunk"s.
  44. * The width of each chunk is equal to {@code pageWidth / numChunks} evenly.
  45. * Next, the candidates are assigned to chunks. The order of the candidates is kept.
  46. * A candidate may occupy one or more successive chunks which are on the same row.
  47. *
  48. * The height of each row is round up to integer, so that the snap-paging
  49. * should work well.
  50. *
  51. */
  52. public class ConversionCandidateLayouter implements CandidateLayouter {
  53. /**
  54. * The metrics between chunk and span.
  55. *
  56. * The main purpose of this class is to inject the chunk compression
  57. * heuristics for testing.
  58. */
  59. static class ChunkMetrics {
  60. private final float chunkWidth;
  61. private final float compressionRatio;
  62. private final float horizontalPadding;
  63. private final float minWidth;
  64. ChunkMetrics(float chunkWidth,
  65. float compressionRatio,
  66. float horizontalPadding,
  67. float minWidth) {
  68. this.chunkWidth = chunkWidth;
  69. this.compressionRatio = compressionRatio;
  70. this.horizontalPadding = horizontalPadding;
  71. this.minWidth = minWidth;
  72. }
  73. /** Returns the number of chunks which the span would consume. */
  74. int getNumChunks(Span span) {
  75. Preconditions.checkNotNull(span);
  76. float compressedValueWidth =
  77. compressValueWidth(span.getValueWidth(), compressionRatio, horizontalPadding, minWidth);
  78. return (int) Math.ceil((compressedValueWidth + span.getDescriptionWidth()) / chunkWidth);
  79. }
  80. static float compressValueWidth(
  81. float valueWidth, float compressionRatio, float horizontalPadding, float minWidth) {
  82. // Sum of geometric progression.
  83. // a == 1.0 (default pixel size)
  84. // r == candidateWidthCompressionRate (pixel width decay rate)
  85. // n == defaultWidth
  86. if (compressionRatio != 1) {
  87. valueWidth =
  88. (1f - (float) Math.pow(compressionRatio, valueWidth)) / (1f - compressionRatio);
  89. }
  90. return Math.max(valueWidth + horizontalPadding * 2, minWidth);
  91. }
  92. }
  93. private Optional<SpanFactory> spanFactory = Optional.absent();
  94. /** Horizontal common ratio of the value size. */
  95. private float valueWidthCompressionRate;
  96. /** Minimum width of the value. */
  97. private float minValueWidth;
  98. /** The Minimum width of the chunk. */
  99. private float minChunkWidth;
  100. /** Height of the value. */
  101. private float valueHeight;
  102. private float valueHorizontalPadding;
  103. private float valueVerticalPadding;
  104. /** The current view's width. */
  105. private int viewWidth;
  106. private boolean reserveEmptySpan = false;
  107. /**
  108. * @param spanFactory the spanFactory to set
  109. */
  110. public void setSpanFactory(SpanFactory spanFactory) {
  111. this.spanFactory = Optional.of(Preconditions.checkNotNull(spanFactory));
  112. }
  113. /**
  114. * @param valueWidthCompressionRate the valueWidthCompressionRate to set
  115. */
  116. public void setValueWidthCompressionRate(float valueWidthCompressionRate) {
  117. this.valueWidthCompressionRate = valueWidthCompressionRate;
  118. }
  119. /**
  120. * @param minValueWidth the minValueWidth to set
  121. */
  122. public void setMinValueWidth(float minValueWidth) {
  123. this.minValueWidth = minValueWidth;
  124. }
  125. /**
  126. * @param minChunkWidth the minChunkWidth to set
  127. */
  128. public void setMinChunkWidth(float minChunkWidth) {
  129. this.minChunkWidth = minChunkWidth;
  130. }
  131. /**
  132. * @param valueHeight the valueHeight to set
  133. */
  134. public void setValueHeight(float valueHeight) {
  135. this.valueHeight = valueHeight;
  136. }
  137. /**
  138. * @param valueHorizontalPadding the valueHorizontalPadding to set
  139. */
  140. public void setValueHorizontalPadding(float valueHorizontalPadding) {
  141. this.valueHorizontalPadding = valueHorizontalPadding;
  142. }
  143. /**
  144. * @param valueVerticalPadding the valueVerticalPadding to set
  145. */
  146. public void setValueVerticalPadding(float valueVerticalPadding) {
  147. this.valueVerticalPadding = valueVerticalPadding;
  148. }
  149. @Override
  150. public boolean setViewSize(int width, int height) {
  151. if (viewWidth == width) {
  152. // Doesn't need to invalidate the layout if the width isn't changed.
  153. return false;
  154. }
  155. viewWidth = width;
  156. return true;
  157. }
  158. private int getNumChunks() {
  159. return (int) (viewWidth / minChunkWidth);
  160. }
  161. public float getChunkWidth() {
  162. return viewWidth / (float) getNumChunks();
  163. }
  164. @Override
  165. public int getPageWidth() {
  166. return Math.max(viewWidth, 0);
  167. }
  168. public int getRowHeight() {
  169. return (int) Math.ceil(valueHeight + valueVerticalPadding * 2);
  170. }
  171. @Override
  172. public int getPageHeight() {
  173. return getRowHeight();
  174. }
  175. @Override
  176. public Optional<CandidateLayout> layout(CandidateList candidateList) {
  177. Preconditions.checkNotNull(candidateList);
  178. if (minChunkWidth <= 0 || viewWidth <= 0 || candidateList.getCandidatesCount() == 0 ||
  179. !spanFactory.isPresent()) {
  180. return Optional.<CandidateLayout>absent();
  181. }
  182. int numChunks = getNumChunks();
  183. float chunkWidth = getChunkWidth();
  184. ChunkMetrics chunkMetrics = new ChunkMetrics(
  185. chunkWidth, valueWidthCompressionRate, valueHorizontalPadding, minValueWidth);
  186. List<Row> rowList = buildRowList(candidateList, spanFactory.get(), numChunks, chunkMetrics,
  187. reserveEmptySpan);
  188. int[] numAllocatedChunks = new int[numChunks];
  189. boolean isFirst = reserveEmptySpan;
  190. for (Row row : rowList) {
  191. layoutSpanList(
  192. row.getSpanList(),
  193. (isFirst ? (viewWidth - (int) chunkWidth) : viewWidth),
  194. (isFirst ? numChunks - 1 : numChunks),
  195. chunkMetrics, numAllocatedChunks);
  196. isFirst = false;
  197. }
  198. // Push empty span at the end of the first row.
  199. if (reserveEmptySpan) {
  200. Span emptySpan = new Span(Optional.<CandidateWord>absent(), 0, 0,
  201. Collections.<String>emptyList());
  202. List<Span> spanList = rowList.get(0).getSpanList();
  203. emptySpan.setLeft(spanList.get(spanList.size() - 1).getRight());
  204. emptySpan.setRight(viewWidth);
  205. rowList.get(0).addSpan(emptySpan);
  206. }
  207. // In order to snap the scrolling on any row boundary, rounding up the rowHeight
  208. // to align pixels.
  209. int rowHeight = getRowHeight();
  210. layoutRowList(rowList, viewWidth, rowHeight);
  211. return Optional.of(new CandidateLayout(rowList, viewWidth, rowHeight * rowList.size()));
  212. }
  213. /**
  214. * Builds the row list based on the number of estimated chunks for each span.
  215. *
  216. * The order of the candidates will be kept.
  217. */
  218. @VisibleForTesting
  219. static List<Row> buildRowList(
  220. CandidateList candidateList, SpanFactory spanFactory,
  221. int numChunks, ChunkMetrics chunkMetrics, boolean enableSpan) {
  222. Preconditions.checkNotNull(candidateList);
  223. Preconditions.checkNotNull(spanFactory);
  224. Preconditions.checkNotNull(chunkMetrics);
  225. List<Row> rowList = new ArrayList<Row>();
  226. int numRemainingChunks = 0;
  227. for (CandidateWord candidateWord : candidateList.getCandidatesList()) {
  228. Span span = spanFactory.newInstance(candidateWord);
  229. int numSpanChunks = chunkMetrics.getNumChunks(span);
  230. if (numRemainingChunks < numSpanChunks) {
  231. // There is no space on the current row to put the current span.
  232. // Create a new row.
  233. numRemainingChunks = numChunks;
  234. // For the first line, we reserve a chunk at right-top in order to place an icon
  235. // button for folding/expanding keyboard.
  236. if (enableSpan && rowList.isEmpty()) {
  237. numRemainingChunks--;
  238. }
  239. rowList.add(new Row());
  240. }
  241. // Add the span to the last row.
  242. rowList.get(rowList.size() - 1).addSpan(span);
  243. numRemainingChunks -= numSpanChunks;
  244. }
  245. return rowList;
  246. }
  247. /**
  248. * Sets left and right of each span. The left and right should be aligned to the chunks.
  249. * Also, the right of the last span should be equal to {@code pageWidth}.
  250. *
  251. * In order to avoid integer array memory allocation (as this method will be invoked
  252. * many times to layout a {@link CandidateList}), it is necessary to pass an integer
  253. * array for the calculation buffer, {@code numAllocatedChunks}.
  254. * The size of the buffer must be equal to or greater than {@code spanList.size()}.
  255. * Its elements needn't be initialized.
  256. */
  257. @VisibleForTesting
  258. static void layoutSpanList(
  259. List<Span> spanList, int pageWidth,
  260. int numChunks, ChunkMetrics chunkMetrics, int[] numAllocatedChunks) {
  261. Preconditions.checkNotNull(spanList);
  262. Preconditions.checkNotNull(chunkMetrics);
  263. Preconditions.checkNotNull(numAllocatedChunks);
  264. Preconditions.checkArgument(spanList.size() <= numAllocatedChunks.length);
  265. int numRemainingChunks = numChunks;
  266. // First, allocate the chunks based on the metrics.
  267. {
  268. int index = 0;
  269. for (Span span : spanList) {
  270. int numSpanChunks = Math.min(numRemainingChunks, chunkMetrics.getNumChunks(span));
  271. numAllocatedChunks[index] = numSpanChunks;
  272. numRemainingChunks -= numSpanChunks;
  273. ++index;
  274. }
  275. }
  276. // Then assign remaining chunks to each span as even as possible by round-robin.
  277. for (int index = 0; numRemainingChunks > 0;
  278. --numRemainingChunks, index = (index + 1) % spanList.size()) {
  279. ++numAllocatedChunks[index];
  280. }
  281. // Set the actual left and right to each span.
  282. {
  283. int index = 0;
  284. float left = 0;
  285. float spanWidth = pageWidth / (float) numChunks;
  286. int cumulativeNumAllocatedChunks = 0;
  287. for (Span span : spanList) {
  288. cumulativeNumAllocatedChunks += numAllocatedChunks[index++];
  289. float right = Math.min(spanWidth * cumulativeNumAllocatedChunks, pageWidth);
  290. span.setLeft(left);
  291. span.setRight(right);
  292. left = right;
  293. }
  294. }
  295. // Set the right of the last element to the pageWidth to align the page.
  296. spanList.get(spanList.size() - 1).setRight(pageWidth);
  297. }
  298. /** Sets top, width and height to the each row. */
  299. @VisibleForTesting
  300. static void layoutRowList(List<Row> rowList, int pageWidth, int rowHeight) {
  301. int top = 0;
  302. for (Row row : Preconditions.checkNotNull(rowList)) {
  303. row.setTop(top);
  304. row.setWidth(pageWidth);
  305. row.setHeight(rowHeight);
  306. top += rowHeight;
  307. }
  308. }
  309. public void reserveEmptySpanForInputFoldButton(boolean reserveEmptySpan) {
  310. this.reserveEmptySpan = reserveEmptySpan;
  311. }
  312. }