Last active
April 28, 2022 11:41
-
-
Save piotr-cz/1a3f1360d189231792db21756cb4a72d to your computer and use it in GitHub Desktop.
Cordova splash screens hook, with Android 9-Patch generation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version='1.0' encoding='utf-8'?> | |
<widget> | |
<!-- ... --> | |
<hook src="scripts/cordova/media/splashscreens.js" type="before_platform_add" /> | |
</widget> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env node | |
/** | |
* Generate splash screens | |
* <cordova hook> | |
* <cli command> | |
* @see https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/index.html | |
*/ | |
const path = require('path') | |
const sharp = require('sharp') | |
// Not available, but not relevant to this gist | |
const utils = require('../../utils') | |
// CLI command | |
if (require.main === module) { | |
dispatch(utils.getCliOpts(process)) | |
// Cordova hook/ import | |
} else { | |
module.exports = ctx => dispatch(ctx.opts) | |
} | |
/** | |
* Dispatch | |
* @param {Array} platforms | |
* @param {Object} options | |
* @param {string} [options.productFlavor] | |
* @param {string} projectRoot | |
* @return {Promise} | |
*/ | |
function dispatch({platforms = [], options = {}, projectRoot}) { | |
console.log('Generating splash screens...') | |
const productFlavorId = options.productFlavor || utils.getDefaultProductFlavorId(projectRoot) | |
const progressPromises = [] | |
// Android | |
if (!platforms.length || utils.platformsIncludes(platforms, 'android')) { | |
progressPromises.push(createAndroidSplashscreens(projectRoot, productFlavorId)) | |
} | |
// iOS | |
if (!platforms.length || utils.platformsIncludes(platforms, 'ios')) { | |
progressPromises.push(createIosSplashscreens(projectRoot, productFlavorId)) | |
} | |
// Summary | |
return Promise.all(progressPromises) | |
.then(() => console.log('...done\n')) | |
.catch(error => console.error(error)) | |
} | |
/** | |
* Create Android splash screens | |
* @param {string} projectRoot | |
* @param {string} productFlavorId | |
* @return {Promise} | |
*/ | |
async function createAndroidSplashscreens(projectRoot, productFlavorId) { | |
const dimensions = [ | |
// Portrait | |
['drawable-port-ldpi-screen', 200, 320], | |
['drawable-port-mdpi-screen', 320, 480], | |
['drawable-port-hdpi-screen', 480, 800], | |
['drawable-port-xhdpi-screen', 720, 1280], | |
['drawable-port-xxhdpi-screen', 960, 1600], | |
['drawable-port-xxxhdpi-screen', 1280, 1920], | |
// Landscape (not used) | |
['drawable-land-ldpi-screen', 320, 200], | |
['drawable-land-mdpi-screen', 480, 320], | |
['drawable-land-hdpi-screen', 800, 480], | |
['drawable-land-xhdpi-screen', 1280, 720], | |
['drawable-land-xxhdpi-screen', 1600, 960], | |
['drawable-land-xxxhdpi-screen', 1920, 1280], | |
] | |
const sourcePortaitPath = path.resolve(projectRoot, 'artwork', productFlavorId, 'splash--portrait.svg') | |
const buildDir = path.resolve(projectRoot, 'res/screen/android') | |
const metadata = await sharp(sourcePortaitPath).metadata() | |
return Promise.all(dimensions.map(([basename, width, height]) => | |
createSplash(sourcePortaitPath, `${buildDir}/${basename}.9.png`, {width, height}, metadata, true) | |
)) | |
} | |
/** | |
* Create iOS Splash screens | |
* @param {string} projectRoot | |
* @param {string} productFlavorId | |
* @return {Promise} | |
* @see https://github.com/apache/cordova-ios/blob/rel/5.0.0/bin/templates/scripts/cordova/lib/prepare.js#L416 | |
* @see https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/ | |
*/ | |
async function createIosSplashscreens(projectRoot, productFlavorId) { | |
const dimensions = [ | |
// Lauch storyboard images | |
// Single-image launch screen | |
['Default@2x~universal~anyany', 2732, 2732], // Any | |
// Multi-image launch sreens (Universal) | |
['Default@2x~universal~comany', 1278, 2732], // Portrait | |
['Default@2x~universal~anycom', 2732, 1278], // Landscape (useless) | |
['Default@2x~universal~comcom', 1334, 750], // Landscape (narrow) | |
['Default@3x~universal~anyany', 2208, 2208], // Any | |
['Default@3x~universal~comany', 1242, 2208], // Portrait | |
['Default@3x~universal~anycom', 2208, 1242], // Landscape | |
['Default@3x~universal~comcom', 1242, 1242], // Landscape (narrow, useless) | |
] | |
const sourcePortaitPath = path.resolve(projectRoot, 'artwork', productFlavorId, 'splash--portrait.svg') | |
const buildDir = path.resolve(projectRoot, 'res/screen/ios') | |
const metadata = await sharp(sourcePortaitPath).metadata() | |
return Promise.all(dimensions.map(([basename, width, height]) => | |
createSplash(sourcePortaitPath, `${buildDir}/${basename}.png`, {width, height}, metadata) | |
)) | |
} | |
/** | |
* Create splash screen | |
* @param {string} sourcePath | |
* @param {string} destPath | |
* @param {Object} destSize | |
* @param {number} destSize.width | |
* @param {number} destSize.height | |
* @param {Object} metadata | |
* @param {number} metadata.width | |
* @param {number} metadata.height | |
* @param {number} metadata.density | |
* @param {boolean} use9patch | |
* @return {Promise} | |
*/ | |
async function createSplash(sourcePath, destPath, destSize, metadata, use9patch = false) { | |
// Note: max density is 2400 | |
const density = Math.max(destSize.width, destSize.height) / Math.max(metadata.width, metadata.height) * metadata.density | |
const composites = [ | |
{ input: await sharp(sourcePath, {density}).resize(destSize.width, destSize.height).toBuffer() }, | |
] | |
const canvasSize = { ...destSize } | |
if (use9patch) { | |
// Add offset on each side | |
canvasSize.width += 2 | |
canvasSize.height += 2 | |
// Add markers | |
composites.push({ | |
input: await sharp(create9PatchMarkers(canvasSize)).toBuffer() | |
}) | |
} | |
return sharp({ | |
create: { | |
width: canvasSize.width, | |
height: canvasSize.height, | |
channels: 4, | |
background: 'transparent', | |
} | |
}) | |
.composite(composites) | |
.toFile(destPath) | |
} | |
/** | |
* Create 9patch markers | |
* At position: top-left, top-right, left-top, left-bottom | |
* @param {Object} canvasSize | |
* @param {number} canvasSize.width | |
* @param {number} canvasSize.height | |
* @return {Buffer} | |
*/ | |
function create9PatchMarkers({width, height}) { | |
const repeatableLength = Math.round(Math.min(width, height) * 0.15) | |
return Buffer.from(` | |
<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"> | |
<line stroke="black" x1="1" x2="${repeatableLength}" y1=".5" y2=".5" /> | |
<line stroke="black" x1="${width - repeatableLength}" x2="${width - 1}" y1=".5" y2=".5" /> | |
<line stroke="black" x1=".5" x2=".5" y1="1" y2="${repeatableLength}" /> | |
<line stroke="black" x1=".5" x2=".5" y1="${height - repeatableLength}" y2="${height - 1}" /> | |
</svg> | |
`) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment