| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.tools.idea.rendering.multi; |
| |
| import com.android.tools.idea.configurations.RenderContext; |
| import com.intellij.openapi.util.Pair; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.*; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import static com.android.tools.idea.rendering.multi.RenderPreviewManager.*; |
| |
| /** |
| * Layout algorithm for render previews which layouts out all |
| * the previews in a regular tile. Suitable for XML preview where |
| * the layout view isn't dominant since it is not directly manipulated on. |
| */ |
| public class PreviewTileLayout { |
| private static final boolean FILL_FROM_BOTTOM = false; |
| |
| private final @NotNull List<RenderPreview> myPreviews; |
| private final @NotNull RenderContext myRenderContext; |
| private final boolean myFixedOrder; |
| |
| public PreviewTileLayout(@NotNull List<RenderPreview> previews, @NotNull RenderContext renderContext, boolean fixedOrder) { |
| myPreviews = previews; |
| myRenderContext = renderContext; |
| myFixedOrder = fixedOrder; |
| } |
| |
| private int myLayoutHeight; |
| private Dimension myFixedRenderSize; |
| |
| @Nullable |
| public Dimension getFixedRenderSize() { |
| return myFixedRenderSize; |
| } |
| |
| public void performLayout() { |
| Rectangle clientArea = myRenderContext.getClientArea(); |
| int availableWidth = clientArea.width; |
| int availableHeight = clientArea.height; |
| availableWidth -= HORIZONTAL_GAP; |
| |
| Dimension fullImageSize = myRenderContext.getFullImageSize(); |
| List<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(myPreviews); |
| if (!myFixedOrder) { |
| if (FILL_FROM_BOTTOM) { |
| Collections.sort(aspectOrder, fullImageSize.width <= fullImageSize.height |
| ? RenderPreview.DECREASING_ASPECT_RATIO |
| : RenderPreview.INCREASING_ASPECT_RATIO); |
| } else { |
| Collections.sort(aspectOrder, fullImageSize.width >= fullImageSize.height ? |
| RenderPreview.DECREASING_ASPECT_RATIO : RenderPreview.INCREASING_ASPECT_RATIO); |
| } |
| } |
| |
| // Compute best row/column size |
| int cellCount = aspectOrder.size() + 1; // +1: for main preview |
| |
| Pair<Integer,Integer> shape = computeOptimalShape(availableWidth, availableHeight, cellCount); |
| int rows = shape.getFirst(); |
| int columns = shape.getSecond(); |
| int tileWidth = (availableWidth - HORIZONTAL_GAP * (columns - 1)) / columns; |
| int tileHeight = (availableHeight - VERTICAL_GAP * (rows - 1) - TITLE_HEIGHT * rows) / rows; |
| |
| RenderPreview[][] grid = computeGrid(aspectOrder, fullImageSize, rows, columns); |
| |
| // Assign positions. |
| // Work row by row; compute the minimum required height by the elements in |
| // the row, and use that height rather than the originally allocated cell height. |
| // If we for example have a row where all devices are in landscape mode, the entire |
| // row is likely not as tall as assigned, so we can recapture that space and use |
| // it for later rows near the top. |
| |
| layoutRows(availableWidth, availableHeight, rows, columns, tileWidth, tileHeight, grid); |
| |
| myLayoutHeight = availableHeight; |
| } |
| |
| private void layoutRows(int availableWidth, |
| int availableHeight, |
| int rows, |
| int columns, |
| int tileWidth, |
| int tileHeight, |
| RenderPreview[][] grid) { |
| int y = availableHeight - 2; |
| int left = availableWidth - columns * tileWidth; |
| if (left > HORIZONTAL_GAP) { |
| // Some spacing on the right |
| left -= HORIZONTAL_GAP; |
| } |
| for (int row = rows - 1; row >= 0; row--) { |
| int rowHeight = 0; |
| //int rowWidth = 0; |
| for (int column = 0; column < columns; column++) { |
| RenderPreview preview = grid[row][column]; |
| if (preview != null) { |
| preview.setMaxSize(tileWidth, tileHeight); |
| rowHeight = Math.max(rowHeight, preview.getLayoutHeight()); |
| //rowWidth += preview.getLayoutWidth(); |
| } |
| } |
| // Handle the fact that the grid doesn't actually contain an entry for the |
| // main rendering |
| if (row == 0) { |
| rowHeight = Math.max(rowHeight, tileHeight); |
| //rowWidth += tileWidth; |
| } |
| |
| // Assign positions -- and perform cell center alignment |
| y -= rowHeight; |
| if (row < rows - 1) { |
| y -= VERTICAL_GAP; |
| } |
| y -= TITLE_HEIGHT; |
| if (y < 0 || row == 0 && rows == 2) { |
| y = 0; |
| } |
| layoutRow(availableHeight, rows, columns, tileWidth, tileHeight, grid[row], y, left, row, rowHeight); |
| |
| if (row == rows - 1) { |
| // Look at the last row; we might be able to grow the last element |
| int last = columns - 1; |
| for (; last >= 0; last--) { |
| if (grid[rows - 1][last] != null) { |
| break; |
| } |
| } |
| if (last != -1 && last < columns - 1) { |
| int x = left + last * (tileWidth + HORIZONTAL_GAP) - HORIZONTAL_GAP; |
| int max = availableWidth; |
| RenderPreview preview = grid[row][last]; |
| int maxWidth = preview.getMaxWidth(); |
| int maxHeight = preview.getMaxHeight(); |
| int layoutWidth = preview.getLayoutWidth(); |
| int layoutHeight = preview.getLayoutHeight(); |
| preview.setMaxSize(Math.min(availableWidth - x, Math.max(maxWidth, max - x - HORIZONTAL_GAP / 2)), |
| Math.min(availableHeight - y, Math.max(maxHeight, rowHeight - TITLE_HEIGHT))); |
| if (preview.getLayoutWidth() != layoutWidth || preview.getLayoutHeight() != layoutHeight) { |
| preview.setPosition(x + HORIZONTAL_GAP / 2, y); |
| } |
| } |
| } |
| } |
| |
| if (rows == 1) { |
| // Center in row |
| int row = 0; |
| for (int column = 0; column < columns; column++) { |
| RenderPreview preview = grid[row][column]; |
| if (preview != null) { |
| preview.setPosition(preview.getX(), (availableHeight - preview.getLayoutHeight()) / 2); |
| } |
| } |
| } else if (columns == 1) { |
| // Center in column |
| int column = 0; |
| for (int row = 0; row < rows; row++) { |
| RenderPreview preview = grid[row][column]; |
| if (preview != null) { |
| preview.setPosition((availableWidth - preview.getLayoutWidth()) / 2, preview.getY()); |
| } |
| } |
| } |
| } |
| |
| private void layoutRow(int availableHeight, |
| int rows, |
| int columns, |
| int tileWidth, |
| int tileHeight, |
| RenderPreview[] renderPreviews, |
| int y, |
| int left, |
| int row, |
| int rowHeight) { |
| for (int column = 0; column < columns; column++) { |
| RenderPreview preview = renderPreviews[column]; |
| if (preview != null) { |
| int x = left + column * (tileWidth + HORIZONTAL_GAP); |
| preview.setPosition(x + (tileWidth - preview.getLayoutWidth()) / 2, y + (rowHeight - preview.getLayoutHeight()) / 2); |
| // TODO: Consider distributing extra space to the last item |
| preview.setVisible(true); |
| } else if (row == 0 && column == 0) { |
| // TODO: This might allow room on remaining row for other views to be larger too!!! Consider giving it to them, |
| // This requires me knowing the aspect ratio of the main view in order to determine whether it's actually going to |
| // use up the allocated width! |
| |
| // Allocate one cell plus |
| int maxWidth = left + tileWidth/* + HORIZONTAL_GAP*/; |
| |
| int maxHeight = TITLE_HEIGHT + tileHeight + availableHeight - rows * tileHeight - VERTICAL_GAP * (rows - 1); |
| myFixedRenderSize = new Dimension(maxWidth, maxHeight); |
| myRenderContext.setMaxSize(myFixedRenderSize.width, myFixedRenderSize.height); |
| } |
| } |
| } |
| |
| private RenderPreview[][] computeGrid(List<RenderPreview> previews, Dimension fullImageSize, int rows, int columns) { |
| int row; |
| int column; |
| RenderPreview[][] grid = new RenderPreview[rows][columns]; |
| |
| if (FILL_FROM_BOTTOM) { |
| // Attempt to leave room in the top row and to the left. |
| // In other words, if we have a partial row, we want the |
| // shape on the left instead of the one on the right: |
| // |
| // XX XXXXX |
| // XXXXX XXXXX |
| // XXXXX XXXXX |
| // XXXXX XX |
| |
| row = rows - 1; |
| column = columns - 1; |
| for (RenderPreview preview : previews) { |
| grid[row][column] = preview; |
| column--; |
| if (column < 0) { |
| column = columns - 1; |
| row--; |
| if (row < 0) { |
| break; |
| } |
| } |
| } |
| } else { |
| if (columns > 1) { |
| row = 0; |
| //// Reserve spot 0 for the default preview |
| column = 1; |
| } else { |
| row = 1; |
| column = 0; |
| } |
| for (RenderPreview preview : previews) { |
| grid[row][column] = preview; |
| column++; |
| if (column == columns) { |
| column = 0; |
| row++; |
| if (row == rows) { |
| break; |
| } |
| } |
| } |
| } |
| return grid; |
| } |
| |
| private double pickAspectRatio() { |
| double minAspect = myPreviews.get(0).getAspectRatio(); |
| double maxAspect = minAspect; |
| |
| for (RenderPreview preview : myPreviews) { |
| double aspect = preview.getAspectRatio(); |
| if (aspect > maxAspect) { |
| maxAspect = aspect; |
| } |
| if (aspect < minAspect) { |
| minAspect = aspect; |
| } |
| } |
| |
| // Aspect: figure out the aspect ratio with the title embedded! |
| double aspect; |
| if (minAspect < 1 && maxAspect > 1) { |
| aspect = 1; |
| } else if (minAspect < 1) { |
| aspect = minAspect; |
| } else { |
| aspect = maxAspect; |
| } |
| return aspect; |
| } |
| |
| private Pair<Integer, Integer> computeOptimalShape(int availableWidth, int availableHeight, int count) { |
| double aspect = pickAspectRatio(); |
| int bestColumns = 1; |
| int bestRows = count; |
| int bestSize = 0; |
| for (int columns = 1; columns <= count; columns++) { |
| int rows = count / columns; |
| if (count % columns > 0) { |
| rows++; |
| } |
| int width = (availableWidth - HORIZONTAL_GAP * (columns - 1)) / columns; |
| int height = (int)(width / aspect); |
| int requiredHeight = height * rows + VERTICAL_GAP * (rows - 1) + TITLE_HEIGHT * rows; |
| if (requiredHeight > availableHeight) { |
| height = (availableHeight - VERTICAL_GAP * (rows - 1) - TITLE_HEIGHT * rows) / rows; |
| width = (int)(height * aspect); |
| } |
| |
| if (width > bestSize) { |
| bestSize = width; |
| bestColumns = columns; |
| bestRows = rows; |
| } |
| } |
| return Pair.create(bestRows, bestColumns); |
| } |
| |
| public int getLayoutHeight() { |
| return myLayoutHeight; |
| } |
| } |