Skip to content

Commit

Permalink
Add adaptive icons (#1888)
Browse files Browse the repository at this point in the history
  • Loading branch information
kalsheikh authored and SusanRatiLane committed Nov 21, 2019
1 parent 5326235 commit 653d8d2
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
import org.codehaus.jettison.json.JSONObject;
import org.codehaus.jettison.json.JSONTokener;

import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
Expand Down Expand Up @@ -55,6 +59,7 @@
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.lang.Math;

import javax.imageio.ImageIO;

Expand Down Expand Up @@ -778,6 +783,47 @@ private boolean createProviderXml(File providerDir) {
return true;
}

// Writes ic_launcher.xml to initialize adaptive icon
private boolean writeICLauncher(File adaptiveIconFile, boolean isRound) {
String mainClass = project.getMainClass();
String packageName = Signatures.getPackageName(mainClass);
try {
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(adaptiveIconFile), "UTF-8"));
out.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
out.write("<adaptive-icon " + "xmlns:android=\"http://schemas.android.com/apk/res/android\" " + ">\n");
out.write("<background android:drawable=\"@color/ic_launcher_background\" />\n");
if (isRound) {
out.write("<foreground android:drawable=\"@mipmap/ic_launcher_round\" />\n");
}else{
out.write("<foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n");
}
out.write("</adaptive-icon>\n");
out.close();
} catch (IOException e) {
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "ic launcher"));
return false;
}
return true;
}

// Writes ic_launcher_background.xml to indicate background color of adaptive icon
private boolean writeICLauncherBackground(File icBackgroundFile) {
try {
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(icBackgroundFile), "UTF-8"));
out.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
out.write("<resources>\n");
out.write("<color name=\"ic_launcher_background\">#ffffff</color>\n");
out.write("</resources>\n");
out.close();
} catch (IOException e) {
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "ic launcher background"));
return false;
}
return true;
}

/*
* Creates an AndroidManifest.xml file needed for the Android application.
*/
Expand Down Expand Up @@ -891,7 +937,8 @@ private boolean writeAndroidManifest(File manifestFile) {
out.write("android:label=\"" + aName + "\" ");
}
out.write("android:networkSecurityConfig=\"@xml/network_security_config\" ");
out.write("android:icon=\"@drawable/ya\" ");
out.write("android:icon=\"@mipmap/ic_launcher\" ");
out.write("android:roundIcon=\"@mipmap/ic_launcher\" ");
if (isForCompanion) { // This is to hook into ACRA
out.write("android:name=\"com.google.appinventor.components.runtime.ReplApplication\" ");
} else {
Expand Down Expand Up @@ -1103,7 +1150,21 @@ public static boolean compile(Project project, Set<String> compTypes, Map<String
out.println("________Preparing application icon");
File resDir = createDir(buildDir, "res");
File drawableDir = createDir(resDir, "drawable");
if (!compiler.prepareApplicationIcon(new File(drawableDir, "ya.png"))) {

// Create mipmap directories
File mipmapV26 = createDir(resDir, "mipmap-anydpi-v26");
File mipmapHdpi = createDir(resDir,"mipmap-hdpi");
File mipmapMdpi = createDir(resDir,"mipmap-mdpi");
File mipmapXhdpi = createDir(resDir,"mipmap-xhdpi");
File mipmapXxhdpi = createDir(resDir,"mipmap-xxhdpi");
File mipmapXxxhdpi = createDir(resDir,"mipmap-xxxhdpi");

// Create list of mipmaps for all icon types with respective sizes
List<File> mipmapDirectoriesForIcons = Arrays.asList(mipmapMdpi, mipmapHdpi, mipmapXhdpi, mipmapXxhdpi, mipmapXxxhdpi);
List<Integer> standardICSizesForMipmaps = Arrays.asList(48,72,96,144,192);
List<Integer> foregroundICSizesForMipmaps = Arrays.asList(108,162,216,324,432);

if (!compiler.prepareApplicationIcon(new File(drawableDir, "ya.png"), mipmapDirectoriesForIcons, standardICSizesForMipmaps, foregroundICSizesForMipmaps)) {
return false;
}
if (reporter != null) {
Expand Down Expand Up @@ -1143,6 +1204,27 @@ public static boolean compile(Project project, Set<String> compTypes, Map<String
return false;
}

// Generate ic_launcher.xml
out.println("________Generating adaptive icon file");
File icLauncher = new File(mipmapV26, "ic_launcher.xml");
if (!compiler.writeICLauncher(icLauncher, false)) {
return false;
}

// Generate ic_launcher_round.xml
out.println("________Generating round adaptive icon file");
File icLauncherRound = new File(mipmapV26, "ic_launcher_round.xml");
if (!compiler.writeICLauncher(icLauncherRound, true)) {
return false;
}

// Generate ic_launcher_background.xml
out.println("________Generating adaptive icon background file");
File icBackgroundColor = new File(styleDir, "ic_launcher_background.xml");
if (!compiler.writeICLauncherBackground(icBackgroundColor)) {
return false;
}

// Generate AndroidManifest.xml
out.println("________Generating manifest file");
File manifestFile = new File(buildDir, "AndroidManifest.xml");
Expand Down Expand Up @@ -1627,10 +1709,76 @@ private boolean runApkSigner(String apkAbsolutePath, String keystoreAbsolutePath
return true;
}

/*
* Returns a resized image given a new width and height
*/
private BufferedImage resizeImage(BufferedImage icon, int height, int width) {
Image tmp = icon.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage finalResized = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = finalResized.createGraphics();
g2.drawImage(tmp, 0, 0, null);
return finalResized;
}

/*
* Creates the circle image of an icon
*/
private BufferedImage produceRoundIcon(BufferedImage icon) {
int imageWidth = icon.getWidth();
// Ratio of icon size to png image size for round icon is 0.80
double iconWidth = imageWidth * 0.80;
// Round iconWidth value to even int for a centered png
int intIconWidth = ((int)Math.round(iconWidth / 2) * 2);
Image tmp = icon.getScaledInstance(intIconWidth, intIconWidth, Image.SCALE_SMOOTH);
int marginWidth = ((imageWidth - intIconWidth) / 2);
BufferedImage roundIcon = new BufferedImage(imageWidth, imageWidth, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = roundIcon.createGraphics();
g2.setClip(new Ellipse2D.Float(marginWidth, marginWidth, intIconWidth, intIconWidth));
g2.drawImage(tmp, marginWidth, marginWidth, null);
return roundIcon;
}

/*
* Creates the image of an icon with rounded corners
*/
private BufferedImage produceRoundedCornerIcon(BufferedImage icon) {
int imageWidth = icon.getWidth();
// Ratio of icon size to png image size for roundRect icon is 0.93
double iconWidth = imageWidth * 0.93;
// Round iconWidth value to even int for a centered png
int intIconWidth = ((int)Math.round(iconWidth / 2) * 2);
Image tmp = icon.getScaledInstance(intIconWidth, intIconWidth, Image.SCALE_SMOOTH);
int marginWidth = ((imageWidth - intIconWidth) / 2);
// Corner radius of roundedCornerIcon needs to be 1/12 of width according to Android material guidelines
float cornerRadius = intIconWidth / 12;
BufferedImage roundedCornerIcon = new BufferedImage(imageWidth, imageWidth, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = roundedCornerIcon.createGraphics();
g2.setClip(new RoundRectangle2D.Float(marginWidth, marginWidth, intIconWidth, intIconWidth, cornerRadius, cornerRadius));
g2.drawImage(tmp, marginWidth, marginWidth, null);
return roundedCornerIcon;
}

/*
* Creates the foreground image of an icon
*/
private BufferedImage produceForegroundImageIcon(BufferedImage icon) {
int imageWidth = icon.getWidth();
// Ratio of icon size to png image size for foreground/round icon is 0.80
double iconWidth = imageWidth * 0.80;
// Round iconWidth value to even int for a centered png
int intIconWidth = ((int)Math.round(iconWidth / 2) * 2);
Image tmp = icon.getScaledInstance(intIconWidth, intIconWidth, Image.SCALE_SMOOTH);
int marginWidth = ((imageWidth - intIconWidth) / 2);
BufferedImage foregroundImageIcon = new BufferedImage(imageWidth, imageWidth, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = foregroundImageIcon.createGraphics();
g2.drawImage(tmp, marginWidth, marginWidth, null);
return foregroundImageIcon;
}

/*
* Loads the icon for the application, either a user provided one or the default one.
*/
private boolean prepareApplicationIcon(File outputPngFile) {
private boolean prepareApplicationIcon(File outputPngFile, List<File> mipmapDirectories, List<Integer> standardICSizes, List<Integer> foregroundICSizes) {
String userSpecifiedIcon = Strings.nullToEmpty(project.getIcon());
try {
BufferedImage icon;
Expand All @@ -1640,14 +1788,38 @@ private boolean prepareApplicationIcon(File outputPngFile) {
if (icon == null) {
// This can happen if the iconFile isn't an image file.
// For example, icon is null if the file is a .wav file.
// TODO(lizlooney) - This happens if the user specifies a .ico file. We should fix that.
// TODO(lizlooney) - This happens if the user specifies a .ico file. We should
// fix that.
userErrors.print(String.format(ICON_ERROR, userSpecifiedIcon));
return false;
}
} else {
// Load the default image.
icon = ImageIO.read(Compiler.class.getResource(DEFAULT_ICON));
}

BufferedImage roundIcon = produceRoundIcon(icon);
BufferedImage roundRectIcon = produceRoundedCornerIcon(icon);
BufferedImage foregroundIcon = produceForegroundImageIcon(icon);

// For each mipmap directory, create all types of ic_launcher photos with respective mipmap sizes
for(int i=0; i < mipmapDirectories.size(); i++){
File mipmapDirectory = mipmapDirectories.get(i);
Integer standardSize = standardICSizes.get(i);
Integer foregroundSize = foregroundICSizes.get(i);

BufferedImage round = resizeImage(roundIcon,standardSize,standardSize);
BufferedImage roundRect = resizeImage(roundRectIcon,standardSize,standardSize);
BufferedImage foreground = resizeImage(foregroundIcon,foregroundSize,foregroundSize);

File roundIconPng = new File(mipmapDirectory,"ic_launcher_round.png");
File roundRectIconPng = new File(mipmapDirectory,"ic_launcher.png");
File foregroundPng = new File(mipmapDirectory,"ic_launcher_foreground.png");

ImageIO.write(round, "png", roundIconPng);
ImageIO.write(roundRect, "png", roundRectIconPng);
ImageIO.write(foreground, "png", foregroundPng);
}
ImageIO.write(icon, "png", outputPngFile);
} catch (Exception e) {
e.printStackTrace();
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 653d8d2

Please sign in to comment.