blob: 544477dbc34968be2d6a92a819476439a61bee10 [file] [log] [blame]
/*
* 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;
}
}