[go: nahoru, domu]

Skip to content

Commit

Permalink
feat: support adaptive icons for 'generate' (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwmkerr committed Apr 25, 2019
1 parent 7359e32 commit 3423491
Show file tree
Hide file tree
Showing 101 changed files with 553 additions and 109 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ There is an excellent guide on developing Adaptive Icons [here](https://medium.c

To test how adaptive icons will look when animated, swiped, etc, the [Adaptive Icons](https://adapticon.tooo.io/) website by [Marius Claret](https://twitter.com/mariusclaret) is very useful!

Note that Adaptive Icons of *all* supported sizes are generated. However, we also generate the `res/mipmap-anydpi-v26/` adaptive icon. This is a large size icon which Android from v26 onwards will automatically rescale as needed to all other sizes. This technically makes the density specific icons redundant. The reason we generate both is to ensure that after `generate` is run, *all* icons in the project will be consistent.

## Developer Guide

The only dependencies are Node 6 (or above) and Yarn.
Expand Down
58 changes: 47 additions & 11 deletions bin/app-icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ const init = require('../src/init/init');
const labelImage = require('../src/label/label-image');
const fileExists = require('../src/utils/file-exists');

// Helper to throw an error if a file doesn't exist.
const errorIfMissing = (filePath, errorMessage) => {
return fileExists(filePath).then((exists) => {
if (!exists) {
console.error(`${chalk.red('error')}: ${errorMessage}`);
return process.exit(1);
}
return true;
});
};

// Create the program.
program
.version(pack.version);
Expand All @@ -25,25 +36,50 @@ program
.option('-i, --icon [icon]', "The icon to use. Defaults to 'icon.png'", 'icon.png')
.option('-s, --search [optional]', "The folder to search from. Defaults to './'", './')
.option('-p, --platforms [optional]', "The platforms to generate icons for. Defaults to 'android,ios'", 'android,ios')
.action(({ icon, search, platforms }) => {
.option('--background-icon [optional]', "The background icon path. Defaults to 'icon.background.png'")
.option('--foreground-icon [optional]', "The foregroud icon path. Defaults to 'icon.foregroud.png'")
.option('--adaptive-icons [optional]', "Additionally, generate Android Adaptive Icon templates. Defaults to 'false'")
.action((parameters) => {
const {
icon,
backgroundIcon,
foregroundIcon,
search,
platforms,
adaptiveIcons,
} = parameters;
imagemagickCli.getVersion()
.then((version) => {
if (!version) {
console.error(' Error: ImageMagick must be installed. Try:');
console.error(' brew install imagemagick');
return process.exit(1);
process.exit(1);
}

// Check that we have a source icon.
return fileExists(icon);
})
.then((exists) => {
if (!exists) {
console.error(`Source file '${icon}' does not exist. Add the file or specify source icon with the '--icon' parameter.`);
return process.exit(1);
.then(() => {
// Check we have the files we need.
const operations = [];
operations.push(errorIfMissing(icon, `Source file '${icon}' does not exist. Add the file or specify source icon with the '--icon' parameter.`));
if (adaptiveIcons) {
const checkPath = backgroundIcon || 'icon.background.png';
operations.push(errorIfMissing(checkPath, `Background icon file '${checkPath}' does not exist. Add the file or specify background icon with the '--background-icon' parameter.`));
}
if (adaptiveIcons) {
const checkPath = foregroundIcon || 'icon.foreground.png';
operations.push(errorIfMissing(checkPath, `Foreground icon file '${checkPath}' does not exist. Add the file or specify foreground icon with the '--foreground-icon' parameter.`));
}
return Promise.all(operations);
})
.then(() => {
// Generate some icons.
return generate({ sourceIcon: icon, searchRoot: search, platforms });
return generate({
sourceIcon: icon,
backgroundIcon,
foregroundIcon,
searchRoot: search,
platforms,
adaptiveIcons,
});
})
.catch((generateErr) => {
console.error(chalk.red(`An error occurred generating the icons: ${generateErr.message}`));
Expand Down Expand Up @@ -98,7 +134,7 @@ program
.command('init')
.description('Initialises app icons by creating simple icon templates')
.option('-c, --caption [caption]', "An optional caption for the icon, e.g 'App'.")
.option('--adaptive-icons', 'Additionally, generate Android Adaptive Icon templates')
.option('--adaptive-icons [optional]', "Additionally, generate Android Adaptive Icon templates. Defaults to 'false'")
.action((params) => {
const { caption, adaptiveIcons } = params;
imagemagickCli.getVersion()
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"start": "./bin/app-icon.js",
"test": "mocha -t 10000 ./src/{,**/}*.specs.js",
"test:debug": "mocha -d --inspect-brk -w --inspect ./src/{,**/}*.specs.js",
"test:debug": "mocha -d -w ./src/{,**/}*.specs.js",
"cov": "nyc mocha --timeout 10000 ./src/{,**/}*.specs.js",
"lint": "eslint .",
"release": "standard-version"
Expand Down Expand Up @@ -44,7 +44,8 @@
"chalk": "^2.3.0",
"commander": "^2.19.0",
"imagemagick-cli": "^0.5.0",
"mkdirp": "^0.5.1"
"mkdirp": "^0.5.1",
"rimraf": "^2.6.3"
},
"nyc": {
"all": true,
Expand Down
44 changes: 44 additions & 0 deletions src/android/AndroidManifest.adaptive-icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"adaptiveIcons": [
{
"folder": "res/mipmap-ldpi-v26",
"backgroundIcon": "ic_launcher_background.png",
"foregroundIcon": "ic_launcher_foreground.png",
"size": "36x36"
},
{
"folder": "res/mipmap-mdpi-v26",
"backgroundIcon": "ic_launcher_background.png",
"foregroundIcon": "ic_launcher_foreground.png",
"size": "48x48"
},
{
"folder": "res/mipmap-hdpi-v26",
"backgroundIcon": "ic_launcher_background.png",
"foregroundIcon": "ic_launcher_foreground.png",
"size": "72x72"
},
{
"folder": "res/mipmap-xhdpi-v26",
"backgroundIcon": "ic_launcher_background.png",
"foregroundIcon": "ic_launcher_foreground.png",
"size": "96x96"
},
{
"folder": "res/mipmap-xxhdpi-v26",
"backgroundIcon": "ic_launcher_background.png",
"foregroundIcon": "ic_launcher_foreground.png",
"size": "144x144"
},
{
"folder": "res/mipmap-xxxhdpi-v26",
"backgroundIcon": "ic_launcher_background.png",
"foregroundIcon": "ic_launcher_foreground.png",
"size": "192x192"
},
{
"comment": "Note that the anydpi folder will *not* contain icons - it contains a reference to the mipmaps for specific sizes",
"folder": "res/mipmap-anydpi-v26"
}
]
}
64 changes: 64 additions & 0 deletions src/android/generate-manifest-adaptive-icons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const path = require('path');
const fs = require('fs');
const { EOL } = require('os');
const mkdirp = require('mkdirp');

const androidManifestAdaptiveIcons = require('./AndroidManifest.adaptive-icons.json');
const resizeImage = require('../resize/resize-image');

// The XML for the ic launcher manifest.
// eslint-disable-next-line
const icLauncherManifestXml =
`<?xml version="1.0" encoding="utf-8"?>${EOL}`
+ `<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">${EOL}`
+ ` <background android:drawable="@mipmap/ic_launcher_background" />${EOL}`
+ ` <foreground android:drawable="@mipmap/ic_launcher_foreground" />${EOL}`
+ `</adaptive-icon>${EOL}`;

// Generate Android Manifest icons given a manifest file.
module.exports = function generateManifestIcons(backgroundIcon, foregroundIcon, manifest) {
// Create the object we will return.
const results = {
icons: [],
};

// We've got the manifest file, get the parent folder.
const manifestFolder = path.dirname(manifest);

// Generate each image in the full icon set, updating the contents.
return Promise.all(androidManifestAdaptiveIcons.adaptiveIcons.map((icon) => {
// Each icon lives in its own folder, so we'd better make sure that folder
// exists.
return new Promise((resolve, reject) => {
const resourceFolder = path.join(manifestFolder, icon.folder);
mkdirp(resourceFolder, (err) => {
if (err) return reject(err);

// Create the manifests, for the normal icons and round icons.
fs.writeFileSync(path.join(resourceFolder, 'ic_launcher.xml'), icLauncherManifestXml, 'utf8');
fs.writeFileSync(path.join(resourceFolder, 'ic_launcher_round.xml'), icLauncherManifestXml, 'utf8');

const operations = [];
// If the manifest requires us to generate icons for the folder, do so.
// Not *every* folder has icons - for example the 'anydpi' folder will
// not contain icons.
if (icon.backgroundIcon) {
const backgroundOutput = path.join(resourceFolder, icon.backgroundIcon);
operations.push(resizeImage(backgroundIcon, backgroundOutput, icon.size));
results.icons.push(backgroundOutput);
}
if (icon.foregroundIcon) {
const foregroundOutput = path.join(resourceFolder, icon.foregroundIcon);
operations.push(resizeImage(foregroundIcon, foregroundOutput, icon.size));
results.icons.push(foregroundOutput);
}
return resolve(Promise.all(operations));
});
});
})).then(() => {
// Before writing the contents file, sort the contents (otherwise
// they could be in a different order each time).
results.icons.sort();
return results;
});
};
73 changes: 73 additions & 0 deletions src/android/generate-manifest-adaptive-icons.specs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { expect } = require('chai');
const path = require('path');
const deleteFolderIfExists = require('../utils/delete-folder-if-exists');
const generateManifestAdaptiveIcons = require('./generate-manifest-adaptive-icons');
const fileExists = require('../utils/file-exists');

const backgroundIcon = './test/icon.background.png';
const foregroundIcon = './test/icon.foreground.png';

// The folders we expect to generate, relative to the manifest location.
const expectedFolders = [
'./res/mipmap-ldpi-v26',
'./res/mipmap-hdpi-v26',
'./res/mipmap-mdpi-v26',
'./res/mipmap-xhdpi-v26',
'./res/mipmap-xxhdpi-v26',
'./res/mipmap-xxxhdpi-v26',
'./res/mipmap-anydpi-v26',
];

// The files we expect in each of the folders above.
const expectedFiles = [
'./ic_launcher.xml',
'./ic_launcher_round.xml',
'./ic_launcher_background.png',
'./ic_launcher_foreground.png',
];

// Create a test for each manifest.
const testManifests = [{
projectName: 'React Native Manifest',
manifestPath: './test/ReactNativeIconTest/android/app/src/main/AndroidManifest.xml',
}, {
projectName: 'Cordova Manifest',
manifestPath: './test/CordovaApp/platforms/android/src/main/AndroidManifest.xml',
}, {
projectName: 'Native Manifest',
manifestPath: './test/NativeApp/android/native_app/src/main/AndroidManifest.xml',
}];

describe('generate-manifest-adaptive-icons', () => {
// Run each test.
testManifests.forEach(({ projectName, manifestPath }) => {
it(`should be able to generate adaptive icons for the ${projectName} manifest`, () => {
// Get the manifest folder, create an array of every icon we expect to see.
const manifestFolder = path.dirname(manifestPath);
const resourceFolders = expectedFolders.map(f => path.join(manifestFolder, f));
const resourceFoldersFiles = resourceFolders.reduce((allFiles, folder) => {
expectedFiles.forEach(ef => allFiles.push(path.join(folder, ef)));
return allFiles;
}, []);

// A bit of a hack here - the 'anydpi' folder should not contain any images,
// it just references the other mipmaps. So remove the anydpi folder images
// from the expected set of files.
const expectedPaths = resourceFoldersFiles.filter(f => !(/anydpi.*png$/.test(f)));
console.log(`Len: ${resourceFoldersFiles.length}`);
expectedPaths.forEach(f => console.log(`Expecting: ${f}`));

// Delete all of the folders we're expecting to create, then generate the icons.
return Promise.all(resourceFolders.map(deleteFolderIfExists))
.then(() => (
generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifestPath)
))
.then(() => Promise.all(expectedPaths.map(fileExists)))
.then((filesDoExist) => {
filesDoExist.forEach((exists, index) => {
expect(exists, `${resourceFoldersFiles[index]} should be generated`).to.equal(true);
});
});
});
});
});
Loading

0 comments on commit 3423491

Please sign in to comment.