| /* |
| * 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 android.graphics; |
| |
| /** |
| * @hide |
| */ |
| public class Atlas { |
| /** |
| * This flag indicates whether the packing algorithm will attempt |
| * to rotate entries to make them fit better in the atlas. |
| */ |
| public static final int FLAG_ALLOW_ROTATIONS = 0x1; |
| /** |
| * This flag indicates whether the packing algorithm should leave |
| * an empty 1 pixel wide border around each bitmap. This border can |
| * be useful if the content of the atlas will be used in OpenGL using |
| * bilinear filtering. |
| */ |
| public static final int FLAG_ADD_PADDING = 0x2; |
| /** |
| * Default flags: allow rotations and add padding. |
| */ |
| public static final int FLAG_DEFAULTS = FLAG_ADD_PADDING; |
| |
| /** |
| * Each type defines a different packing algorithm that can |
| * be used by an {@link Atlas}. The best algorithm to use |
| * will depend on the source dataset and the dimensions of |
| * the atlas. |
| */ |
| public enum Type { |
| SliceMinArea, |
| SliceMaxArea, |
| SliceShortAxis, |
| SliceLongAxis |
| } |
| |
| /** |
| * Represents a bitmap packed in the atlas. Each entry has a location in |
| * pixels in the atlas and a rotation flag. If the entry was rotated, the |
| * bitmap must be rotated by 90 degrees (in either direction as long as |
| * the origin remains the same) before being rendered into the atlas. |
| */ |
| public static class Entry { |
| /** |
| * Location, in pixels, of the bitmap on the X axis in the atlas. |
| */ |
| public int x; |
| /** |
| * Location, in pixels, of the bitmap on the Y axis in the atlas. |
| */ |
| public int y; |
| |
| /** |
| * If true, the bitmap must be rotated 90 degrees in the atlas. |
| */ |
| public boolean rotated; |
| } |
| |
| private final Policy mPolicy; |
| |
| /** |
| * Creates a new atlas with the specified algorithm and dimensions |
| * in pixels. Calling this constructor is equivalent to calling |
| * {@link #Atlas(Atlas.Type, int, int, int)} with {@link #FLAG_DEFAULTS}. |
| * |
| * @param type The algorithm to use to pack rectangles in the atlas |
| * @param width The width of the atlas in pixels |
| * @param height The height of the atlas in pixels |
| * |
| * @see #Atlas(Atlas.Type, int, int, int) |
| */ |
| public Atlas(Type type, int width, int height) { |
| this(type, width, height, FLAG_DEFAULTS); |
| } |
| |
| /** |
| * Creates a new atlas with the specified algorithm and dimensions |
| * in pixels. A set of flags can also be specified to control the |
| * behavior of the atlas. |
| * |
| * @param type The algorithm to use to pack rectangles in the atlas |
| * @param width The width of the atlas in pixels |
| * @param height The height of the atlas in pixels |
| * @param flags Optional flags to control the behavior of the atlas: |
| * {@link #FLAG_ADD_PADDING}, {@link #FLAG_ALLOW_ROTATIONS} |
| * |
| * @see #Atlas(Atlas.Type, int, int) |
| */ |
| public Atlas(Type type, int width, int height, int flags) { |
| mPolicy = findPolicy(type, width, height, flags); |
| } |
| |
| /** |
| * Packs a rectangle of the specified dimensions in this atlas. |
| * |
| * @param width The width of the rectangle to pack in the atlas |
| * @param height The height of the rectangle to pack in the atlas |
| * |
| * @return An {@link Entry} instance if the rectangle was packed in |
| * the atlas, or null if the rectangle could not fit |
| * |
| * @see #pack(int, int, Atlas.Entry) |
| */ |
| public Entry pack(int width, int height) { |
| return pack(width, height, null); |
| } |
| |
| /** |
| * Packs a rectangle of the specified dimensions in this atlas. |
| * |
| * @param width The width of the rectangle to pack in the atlas |
| * @param height The height of the rectangle to pack in the atlas |
| * @param entry Out parameter that will be filled in with the location |
| * and attributes of the packed rectangle, can be null |
| * |
| * @return An {@link Entry} instance if the rectangle was packed in |
| * the atlas, or null if the rectangle could not fit |
| * |
| * @see #pack(int, int) |
| */ |
| public Entry pack(int width, int height, Entry entry) { |
| if (entry == null) entry = new Entry(); |
| return mPolicy.pack(width, height, entry); |
| } |
| |
| private static Policy findPolicy(Type type, int width, int height, int flags) { |
| switch (type) { |
| case SliceMinArea: |
| return new SlicePolicy(width, height, flags, |
| new SlicePolicy.MinAreaSplitDecision()); |
| case SliceMaxArea: |
| return new SlicePolicy(width, height, flags, |
| new SlicePolicy.MaxAreaSplitDecision()); |
| case SliceShortAxis: |
| return new SlicePolicy(width, height, flags, |
| new SlicePolicy.ShorterFreeAxisSplitDecision()); |
| case SliceLongAxis: |
| return new SlicePolicy(width, height, flags, |
| new SlicePolicy.LongerFreeAxisSplitDecision()); |
| } |
| return null; |
| } |
| |
| /** |
| * A policy defines how the atlas performs the packing operation. |
| */ |
| private static abstract class Policy { |
| abstract Entry pack(int width, int height, Entry entry); |
| } |
| |
| /** |
| * The Slice algorightm divides the remaining empty space either |
| * horizontally or vertically after a bitmap is placed in the atlas. |
| * |
| * NOTE: the algorithm is explained below using a tree but is |
| * implemented using a linked list instead for performance reasons. |
| * |
| * The algorithm starts with a single empty cell covering the entire |
| * atlas: |
| * |
| * ----------------------- |
| * | | |
| * | | |
| * | | |
| * | Empty space | |
| * | (C0) | |
| * | | |
| * | | |
| * | | |
| * ----------------------- |
| * |
| * The tree of cells looks like this: |
| * |
| * N0(free) |
| * |
| * The algorithm then places a bitmap B1, if possible: |
| * |
| * ----------------------- |
| * | | | |
| * | B1 | | |
| * | | | |
| * |-------- | |
| * | | |
| * | | |
| * | | |
| * | | |
| * ----------------------- |
| * |
| * After placing a bitmap in an empty cell, the algorithm splits |
| * the remaining space in two new empty cells. The split can occur |
| * vertically or horizontally (this is controlled by the "split |
| * decision" parameter of the algorithm.) |
| * |
| * Here is for the instance the result of a vertical split: |
| * |
| * ----------------------- |
| * | | | |
| * | B1 | | |
| * | | | |
| * |--------| C2 | |
| * | | | |
| * | | | |
| * | C1 | | |
| * | | | |
| * ----------------------- |
| * |
| * The cells tree now looks like this: |
| * |
| * C0(occupied) |
| * / \ |
| * / \ |
| * / \ |
| * / \ |
| * C1(free) C2(free) |
| * |
| * For each bitmap to place in the atlas, the Slice algorithm |
| * will visit the free cells until it finds one where a bitmap can |
| * fit. It will then split the now occupied cell and proceed onto |
| * the next bitmap. |
| */ |
| private static class SlicePolicy extends Policy { |
| private final Cell mRoot = new Cell(); |
| |
| private final SplitDecision mSplitDecision; |
| |
| private final boolean mAllowRotation; |
| private final int mPadding; |
| |
| /** |
| * A cell represents a sub-rectangle of the atlas. A cell is |
| * a node in a linked list representing the available free |
| * space in the atlas. |
| */ |
| private static class Cell { |
| int x; |
| int y; |
| |
| int width; |
| int height; |
| |
| Cell next; |
| |
| @Override |
| public String toString() { |
| return String.format("cell[x=%d y=%d width=%d height=%d", x, y, width, height); |
| } |
| } |
| |
| SlicePolicy(int width, int height, int flags, SplitDecision splitDecision) { |
| mAllowRotation = (flags & FLAG_ALLOW_ROTATIONS) != 0; |
| mPadding = (flags & FLAG_ADD_PADDING) != 0 ? 1 : 0; |
| |
| // The entire atlas is empty at first, minus padding |
| Cell first = new Cell(); |
| first.x = first.y = mPadding; |
| first.width = width - 2 * mPadding; |
| first.height = height - 2 * mPadding; |
| |
| mRoot.next = first; |
| mSplitDecision = splitDecision; |
| } |
| |
| @Override |
| Entry pack(int width, int height, Entry entry) { |
| Cell cell = mRoot.next; |
| Cell prev = mRoot; |
| |
| while (cell != null) { |
| if (insert(cell, prev, width, height, entry)) { |
| return entry; |
| } |
| |
| prev = cell; |
| cell = cell.next; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Defines how the remaining empty space should be split up: |
| * vertically or horizontally. |
| */ |
| private static interface SplitDecision { |
| /** |
| * Returns true if the remaining space defined by |
| * <code>freeWidth</code> and <code>freeHeight</code> |
| * should be split horizontally. |
| * |
| * @param freeWidth The rectWidth of the free space after packing a rectangle |
| * @param freeHeight The rectHeight of the free space after packing a rectangle |
| * @param rectWidth The rectWidth of the rectangle that was packed in a cell |
| * @param rectHeight The rectHeight of the rectangle that was packed in a cell |
| */ |
| boolean splitHorizontal(int freeWidth, int freeHeight, |
| int rectWidth, int rectHeight); |
| } |
| |
| // Splits the free area horizontally to minimize the horizontal section area |
| private static class MinAreaSplitDecision implements SplitDecision { |
| @Override |
| public boolean splitHorizontal(int freeWidth, int freeHeight, |
| int rectWidth, int rectHeight) { |
| return rectWidth * freeHeight > freeWidth * rectHeight; |
| } |
| } |
| |
| // Splits the free area horizontally to maximize the horizontal section area |
| private static class MaxAreaSplitDecision implements SplitDecision { |
| @Override |
| public boolean splitHorizontal(int freeWidth, int freeHeight, |
| int rectWidth, int rectHeight) { |
| return rectWidth * freeHeight <= freeWidth * rectHeight; |
| } |
| } |
| |
| // Splits the free area horizontally if the horizontal axis is shorter |
| private static class ShorterFreeAxisSplitDecision implements SplitDecision { |
| @Override |
| public boolean splitHorizontal(int freeWidth, int freeHeight, |
| int rectWidth, int rectHeight) { |
| return freeWidth <= freeHeight; |
| } |
| } |
| |
| // Splits the free area horizontally if the vertical axis is shorter |
| private static class LongerFreeAxisSplitDecision implements SplitDecision { |
| @Override |
| public boolean splitHorizontal(int freeWidth, int freeHeight, |
| int rectWidth, int rectHeight) { |
| return freeWidth > freeHeight; |
| } |
| } |
| |
| /** |
| * Attempts to pack a rectangle of specified dimensions in the available |
| * empty space. |
| * |
| * @param cell The cell representing free space in which to pack the rectangle |
| * @param prev The previous cell in the free space linked list |
| * @param width The width of the rectangle to pack |
| * @param height The height of the rectangle to pack |
| * @param entry Stores the location of the packged rectangle, if it fits |
| * |
| * @return True if the rectangle was packed in the atlas, false otherwise |
| */ |
| @SuppressWarnings("SuspiciousNameCombination") |
| private boolean insert(Cell cell, Cell prev, int width, int height, Entry entry) { |
| boolean rotated = false; |
| |
| // If the rectangle doesn't fit we'll try to rotate it |
| // if possible before giving up |
| if (cell.width < width || cell.height < height) { |
| if (mAllowRotation) { |
| if (cell.width < height || cell.height < width) { |
| return false; |
| } |
| |
| // Rotate the rectangle |
| int temp = width; |
| width = height; |
| height = temp; |
| rotated = true; |
| } else { |
| return false; |
| } |
| } |
| |
| // Remaining free space after packing the rectangle |
| int deltaWidth = cell.width - width; |
| int deltaHeight = cell.height - height; |
| |
| // Split the remaining free space into two new cells |
| Cell first = new Cell(); |
| Cell second = new Cell(); |
| |
| first.x = cell.x + width + mPadding; |
| first.y = cell.y; |
| first.width = deltaWidth - mPadding; |
| |
| second.x = cell.x; |
| second.y = cell.y + height + mPadding; |
| second.height = deltaHeight - mPadding; |
| |
| if (mSplitDecision.splitHorizontal(deltaWidth, deltaHeight, |
| width, height)) { |
| first.height = height; |
| second.width = cell.width; |
| } else { |
| first.height = cell.height; |
| second.width = width; |
| |
| // The order of the cells matters for efficient packing |
| // We want to give priority to the cell chosen by the |
| // split decision heuristic |
| Cell temp = first; |
| first = second; |
| second = temp; |
| } |
| |
| // Remove degenerate cases to keep the free list as small as possible |
| if (first.width > 0 && first.height > 0) { |
| prev.next = first; |
| prev = first; |
| } |
| |
| if (second.width > 0 && second.height > 0) { |
| prev.next = second; |
| second.next = cell.next; |
| } else { |
| prev.next = cell.next; |
| } |
| |
| // The cell is now completely removed from the free list |
| cell.next = null; |
| |
| // Return the location and rotation of the packed rectangle |
| entry.x = cell.x; |
| entry.y = cell.y; |
| entry.rotated = rotated; |
| |
| return true; |
| } |
| } |
| } |