Skip to content

Commit 73694bd

Browse files
authored
feat: support generating android adaptive icons (#5667)
1 parent 0569873 commit 73694bd

File tree

7 files changed

+230
-43
lines changed

7 files changed

+230
-43
lines changed

docs/man_pages/project/configuration/resources/resources-generate-icons.md

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Usage | Synopsis
1515
------|-------
1616
`$ tns resources generate icons <Path to image>` | Generate all icons for Android and iOS based on the specified image.
1717

18+
### Options
19+
20+
* `--background` Sets the background color of the icon. When no color is specified, a default value of `transparent` is used. `<Color>` is a valid color and can be represented with string, like `white`, `black`, `blue`, or with HEX representation, for example `#FFFFFF`, `#000000`, `#0000FF`. NOTE: As the `#` is special symbol in some terminals, make sure to place the value in quotes, for example `$ tns resources generate icons ../myImage.png --background "#FF00FF"`.
21+
1822
### Arguments
1923

2024
* `<Path to image>` is a valid path to an image that will be used to generate all icons.

lib/commands/generate-assets.ts

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class GenerateIconsCommand
6161
): Promise<void> {
6262
await this.$assetsGenerationService.generateIcons({
6363
imagePath,
64+
background,
6465
projectDir: this.$projectData.projectDir,
6566
});
6667
}

lib/declarations.d.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -1144,12 +1144,7 @@ interface IResourceGenerationData extends IProjectDir {
11441144
* @param {string} platform Specify for which platform to generate assets. If not defined will generate for all platforms
11451145
*/
11461146
platform?: string;
1147-
}
11481147

1149-
/**
1150-
* Describes the data needed for splash screens generation
1151-
*/
1152-
interface ISplashesGenerationData extends IResourceGenerationData {
11531148
/**
11541149
* @param {string} background Background color that will be used for background. Defaults to #FFFFFF
11551150
*/
@@ -1169,11 +1164,11 @@ interface IAssetsGenerationService {
11691164

11701165
/**
11711166
* Generate splash screens for iOS and Android
1172-
* @param {ISplashesGenerationData} splashesGenerationData Provides the data needed for splash screens generation
1167+
* @param {IResourceGenerationData} splashesGenerationData Provides the data needed for splash screens generation
11731168
* @returns {Promise<void>}
11741169
*/
11751170
generateSplashScreens(
1176-
splashesGenerationData: ISplashesGenerationData
1171+
splashesGenerationData: IResourceGenerationData
11771172
): Promise<void>;
11781173
}
11791174

lib/definitions/project.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ interface IAssetItem {
407407
resizeOperation?: string;
408408
overlayImageScale?: number;
409409
rgba?: boolean;
410+
411+
// additional operations for special cases
412+
operation?: "delete" | "writeXMLColor";
413+
data?: any;
410414
}
411415

412416
interface IAssetSubGroup {
@@ -438,6 +442,7 @@ interface IImageDefinitionGroup {
438442
interface IImageDefinitionsStructure {
439443
ios: IImageDefinitionGroup;
440444
android: IImageDefinitionGroup;
445+
android_legacy: IImageDefinitionGroup;
441446
}
442447

443448
interface ITemplateData {

lib/services/assets-generation/assets-generation-service.ts

+64-12
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ import { AssetConstants } from "../../constants";
55
import {
66
IAssetsGenerationService,
77
IResourceGenerationData,
8-
ISplashesGenerationData,
98
} from "../../declarations";
109
import {
1110
IProjectDataService,
1211
IAssetGroup,
1312
IAssetSubGroup,
1413
} from "../../definitions/project";
15-
import { IDictionary } from "../../common/declarations";
14+
import { IDictionary, IFileSystem } from "../../common/declarations";
1615
import * as _ from "lodash";
1716
import { injector } from "../../common/yok";
17+
import { EOL } from "os";
1818

1919
export const enum Operations {
2020
OverlayWith = "overlayWith",
2121
Blank = "blank",
2222
Resize = "resize",
23+
OuterScale = "outerScale",
2324
}
2425

2526
export class AssetsGenerationService implements IAssetsGenerationService {
@@ -32,7 +33,8 @@ export class AssetsGenerationService implements IAssetsGenerationService {
3233

3334
constructor(
3435
private $logger: ILogger,
35-
private $projectDataService: IProjectDataService
36+
private $projectDataService: IProjectDataService,
37+
private $fs: IFileSystem
3638
) {}
3739

3840
@exported("assetsGenerationService")
@@ -49,7 +51,7 @@ export class AssetsGenerationService implements IAssetsGenerationService {
4951

5052
@exported("assetsGenerationService")
5153
public async generateSplashScreens(
52-
splashesGenerationData: ISplashesGenerationData
54+
splashesGenerationData: IResourceGenerationData
5355
): Promise<void> {
5456
this.$logger.info("Generating splash screens ...");
5557
await this.generateImagesForDefinitions(
@@ -60,10 +62,10 @@ export class AssetsGenerationService implements IAssetsGenerationService {
6062
}
6163

6264
private async generateImagesForDefinitions(
63-
generationData: ISplashesGenerationData,
65+
generationData: IResourceGenerationData,
6466
propertiesToEnumerate: string[]
6567
): Promise<void> {
66-
generationData.background = generationData.background || "white";
68+
const background = generationData.background || "white";
6769
const assetsStructure = await this.$projectDataService.getAssetsStructure(
6870
generationData
6971
);
@@ -88,6 +90,45 @@ export class AssetsGenerationService implements IAssetsGenerationService {
8890
.value();
8991

9092
for (const assetItem of assetItems) {
93+
if (assetItem.operation === "delete") {
94+
if (this.$fs.exists(assetItem.path)) {
95+
this.$fs.deleteFile(assetItem.path);
96+
}
97+
continue;
98+
}
99+
100+
if (assetItem.operation === "writeXMLColor") {
101+
const colorName = assetItem.data?.colorName;
102+
if (!colorName) {
103+
continue;
104+
}
105+
try {
106+
const color =
107+
(generationData as any)[assetItem.data?.fromKey] ??
108+
assetItem.data?.default ??
109+
"white";
110+
111+
const colorHEX: number = Jimp.cssColorToHex(color);
112+
const hex = colorHEX?.toString(16).substring(0, 6) ?? "FFFFFF";
113+
114+
this.$fs.writeFile(
115+
assetItem.path,
116+
[
117+
`<?xml version="1.0" encoding="utf-8"?>`,
118+
`<resources>`,
119+
` <color name="${colorName}">#${hex.toUpperCase()}</color>`,
120+
`</resources>`,
121+
].join(EOL)
122+
);
123+
} catch (err) {
124+
this.$logger.info(
125+
`Failed to write provided color to ${assetItem.path} -> ${colorName}. See --log trace for more info.`
126+
);
127+
this.$logger.trace(err);
128+
}
129+
continue;
130+
}
131+
91132
const operation = assetItem.resizeOperation || Operations.Resize;
92133
let tempScale: number = null;
93134
if (assetItem.scale) {
@@ -133,24 +174,35 @@ export class AssetsGenerationService implements IAssetsGenerationService {
133174
imageResize
134175
);
135176
image = this.generateImage(
136-
generationData.background,
177+
background,
137178
width,
138179
height,
139180
outputPath,
140181
image
141182
);
142183
break;
143184
case Operations.Blank:
185+
image = this.generateImage(background, width, height, outputPath);
186+
break;
187+
case Operations.Resize:
188+
image = await this.resize(generationData.imagePath, width, height);
189+
break;
190+
case Operations.OuterScale:
191+
// Resize image without applying scale
192+
image = await this.resize(
193+
generationData.imagePath,
194+
assetItem.width,
195+
assetItem.height
196+
);
197+
// The scale will apply to the underlying layer of the generated image
144198
image = this.generateImage(
145-
generationData.background,
199+
"#00000000",
146200
width,
147201
height,
148-
outputPath
202+
outputPath,
203+
image
149204
);
150205
break;
151-
case Operations.Resize:
152-
image = await this.resize(generationData.imagePath, width, height);
153-
break;
154206
default:
155207
throw new Error(`Invalid image generation operation: ${operation}`);
156208
}

lib/services/project-data-service.ts

+27-16
Original file line numberDiff line numberDiff line change
@@ -236,18 +236,29 @@ export class ProjectDataService implements IProjectDataService {
236236
? path.join(pathToAndroidDir, SRC_DIR, MAIN_DIR, RESOURCES_DIR)
237237
: pathToAndroidDir;
238238

239-
const currentStructure = this.$fs.enumerateFilesInDirectorySync(basePath);
240-
const content = this.getImageDefinitions().android;
239+
let useLegacy = false;
240+
try {
241+
const manifest = this.$fs.readText(
242+
path.resolve(basePath, "../AndroidManifest.xml")
243+
);
244+
useLegacy = !manifest.includes(`android:icon="@mipmap/ic_launcher"`);
245+
} catch (err) {
246+
// ignore
247+
}
248+
249+
const content = this.getImageDefinitions()[
250+
useLegacy ? "android_legacy" : "android"
251+
];
241252

242253
return {
243-
icons: this.getAndroidAssetSubGroup(content.icons, currentStructure),
254+
icons: this.getAndroidAssetSubGroup(content.icons, basePath),
244255
splashBackgrounds: this.getAndroidAssetSubGroup(
245256
content.splashBackgrounds,
246-
currentStructure
257+
basePath
247258
),
248259
splashCenterImages: this.getAndroidAssetSubGroup(
249260
content.splashCenterImages,
250-
currentStructure
261+
basePath
251262
),
252263
splashImages: null,
253264
};
@@ -448,23 +459,23 @@ export class ProjectDataService implements IProjectDataService {
448459

449460
private getAndroidAssetSubGroup(
450461
assetItems: IAssetItem[],
451-
realPaths: string[]
462+
basePath: string
452463
): IAssetSubGroup {
453464
const assetSubGroup: IAssetSubGroup = {
454465
images: <any>[],
455466
};
456467

457-
const normalizedPaths = _.map(realPaths, (p) => path.normalize(p));
458468
_.each(assetItems, (assetItem) => {
459-
_.each(normalizedPaths, (currentNormalizedPath) => {
460-
const imagePath = path.join(assetItem.directory, assetItem.filename);
461-
if (currentNormalizedPath.indexOf(path.normalize(imagePath)) !== -1) {
462-
assetItem.path = currentNormalizedPath;
463-
assetItem.size = `${assetItem.width}${AssetConstants.sizeDelimiter}${assetItem.height}`;
464-
assetSubGroup.images.push(assetItem);
465-
return false;
466-
}
467-
});
469+
const imagePath = path.join(
470+
basePath,
471+
assetItem.directory,
472+
assetItem.filename
473+
);
474+
assetItem.path = imagePath;
475+
if (assetItem.width && assetItem.height) {
476+
assetItem.size = `${assetItem.width}${AssetConstants.sizeDelimiter}${assetItem.height}`;
477+
}
478+
assetSubGroup.images.push(assetItem);
468479
});
469480

470481
return assetSubGroup;

0 commit comments

Comments
 (0)