Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added support for generating android adaptive icons #5667

Merged
merged 8 commits into from
Mar 25, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Usage | Synopsis
------|-------
`$ tns resources generate icons <Path to image>` | Generate all icons for Android and iOS based on the specified image.

### Options

* `--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"`.

### Arguments

* `<Path to image>` is a valid path to an image that will be used to generate all icons.
Expand Down
1 change: 1 addition & 0 deletions lib/commands/generate-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class GenerateIconsCommand
): Promise<void> {
await this.$assetsGenerationService.generateIcons({
imagePath,
background,
projectDir: this.$projectData.projectDir,
});
}
Expand Down
9 changes: 2 additions & 7 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,12 +1144,7 @@ interface IResourceGenerationData extends IProjectDir {
* @param {string} platform Specify for which platform to generate assets. If not defined will generate for all platforms
*/
platform?: string;
}

/**
* Describes the data needed for splash screens generation
*/
interface ISplashesGenerationData extends IResourceGenerationData {
/**
* @param {string} background Background color that will be used for background. Defaults to #FFFFFF
*/
Expand All @@ -1169,11 +1164,11 @@ interface IAssetsGenerationService {

/**
* Generate splash screens for iOS and Android
* @param {ISplashesGenerationData} splashesGenerationData Provides the data needed for splash screens generation
* @param {IResourceGenerationData} splashesGenerationData Provides the data needed for splash screens generation
* @returns {Promise<void>}
*/
generateSplashScreens(
splashesGenerationData: ISplashesGenerationData
splashesGenerationData: IResourceGenerationData
): Promise<void>;
}

Expand Down
5 changes: 5 additions & 0 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ interface IAssetItem {
resizeOperation?: string;
overlayImageScale?: number;
rgba?: boolean;

// additional operations for special cases
operation?: "delete" | "writeXMLColor";
data?: any;
}

interface IAssetSubGroup {
Expand Down Expand Up @@ -438,6 +442,7 @@ interface IImageDefinitionGroup {
interface IImageDefinitionsStructure {
ios: IImageDefinitionGroup;
android: IImageDefinitionGroup;
android_legacy: IImageDefinitionGroup;
}

interface ITemplateData {
Expand Down
76 changes: 64 additions & 12 deletions lib/services/assets-generation/assets-generation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import { AssetConstants } from "../../constants";
import {
IAssetsGenerationService,
IResourceGenerationData,
ISplashesGenerationData,
} from "../../declarations";
import {
IProjectDataService,
IAssetGroup,
IAssetSubGroup,
} from "../../definitions/project";
import { IDictionary } from "../../common/declarations";
import { IDictionary, IFileSystem } from "../../common/declarations";
import * as _ from "lodash";
import { injector } from "../../common/yok";
import { EOL } from "os";

export const enum Operations {
OverlayWith = "overlayWith",
Blank = "blank",
Resize = "resize",
OuterScale = "outerScale",
}

export class AssetsGenerationService implements IAssetsGenerationService {
Expand All @@ -32,7 +33,8 @@ export class AssetsGenerationService implements IAssetsGenerationService {

constructor(
private $logger: ILogger,
private $projectDataService: IProjectDataService
private $projectDataService: IProjectDataService,
private $fs: IFileSystem
) {}

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

@exported("assetsGenerationService")
public async generateSplashScreens(
splashesGenerationData: ISplashesGenerationData
splashesGenerationData: IResourceGenerationData
): Promise<void> {
this.$logger.info("Generating splash screens ...");
await this.generateImagesForDefinitions(
Expand All @@ -60,10 +62,10 @@ export class AssetsGenerationService implements IAssetsGenerationService {
}

private async generateImagesForDefinitions(
generationData: ISplashesGenerationData,
generationData: IResourceGenerationData,
propertiesToEnumerate: string[]
): Promise<void> {
generationData.background = generationData.background || "white";
const background = generationData.background || "white";
const assetsStructure = await this.$projectDataService.getAssetsStructure(
generationData
);
Expand All @@ -88,6 +90,45 @@ export class AssetsGenerationService implements IAssetsGenerationService {
.value();

for (const assetItem of assetItems) {
if (assetItem.operation === "delete") {
if (this.$fs.exists(assetItem.path)) {
this.$fs.deleteFile(assetItem.path);
}
continue;
}

if (assetItem.operation === "writeXMLColor") {
const colorName = assetItem.data?.colorName;
if (!colorName) {
continue;
}
try {
const color =
(generationData as any)[assetItem.data?.fromKey] ??
assetItem.data?.default ??
"white";

const colorHEX: number = Jimp.cssColorToHex(color);
const hex = colorHEX?.toString(16).substring(0, 6) ?? "FFFFFF";

this.$fs.writeFile(
assetItem.path,
[
`<?xml version="1.0" encoding="utf-8"?>`,
`<resources>`,
` <color name="${colorName}">#${hex.toUpperCase()}</color>`,
`</resources>`,
].join(EOL)
);
} catch (err) {
this.$logger.info(
`Failed to write provided color to ${assetItem.path} -> ${colorName}. See --log trace for more info.`
);
this.$logger.trace(err);
}
continue;
}

const operation = assetItem.resizeOperation || Operations.Resize;
let tempScale: number = null;
if (assetItem.scale) {
Expand Down Expand Up @@ -133,24 +174,35 @@ export class AssetsGenerationService implements IAssetsGenerationService {
imageResize
);
image = this.generateImage(
generationData.background,
background,
width,
height,
outputPath,
image
);
break;
case Operations.Blank:
image = this.generateImage(background, width, height, outputPath);
break;
case Operations.Resize:
image = await this.resize(generationData.imagePath, width, height);
break;
case Operations.OuterScale:
// Resize image without applying scale
image = await this.resize(
generationData.imagePath,
assetItem.width,
assetItem.height
);
// The scale will apply to the underlying layer of the generated image
image = this.generateImage(
generationData.background,
"#00000000",
width,
height,
outputPath
outputPath,
image
);
break;
case Operations.Resize:
image = await this.resize(generationData.imagePath, width, height);
break;
default:
throw new Error(`Invalid image generation operation: ${operation}`);
}
Expand Down
43 changes: 27 additions & 16 deletions lib/services/project-data-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,18 +236,29 @@ export class ProjectDataService implements IProjectDataService {
? path.join(pathToAndroidDir, SRC_DIR, MAIN_DIR, RESOURCES_DIR)
: pathToAndroidDir;

const currentStructure = this.$fs.enumerateFilesInDirectorySync(basePath);
const content = this.getImageDefinitions().android;
let useLegacy = false;
try {
const manifest = this.$fs.readText(
path.resolve(basePath, "../AndroidManifest.xml")
);
useLegacy = !manifest.includes(`android:icon="@mipmap/ic_launcher"`);
} catch (err) {
// ignore
}

const content = this.getImageDefinitions()[
useLegacy ? "android_legacy" : "android"
];

return {
icons: this.getAndroidAssetSubGroup(content.icons, currentStructure),
icons: this.getAndroidAssetSubGroup(content.icons, basePath),
splashBackgrounds: this.getAndroidAssetSubGroup(
content.splashBackgrounds,
currentStructure
basePath
),
splashCenterImages: this.getAndroidAssetSubGroup(
content.splashCenterImages,
currentStructure
basePath
),
splashImages: null,
};
Expand Down Expand Up @@ -448,23 +459,23 @@ export class ProjectDataService implements IProjectDataService {

private getAndroidAssetSubGroup(
assetItems: IAssetItem[],
realPaths: string[]
basePath: string
): IAssetSubGroup {
const assetSubGroup: IAssetSubGroup = {
images: <any>[],
};

const normalizedPaths = _.map(realPaths, (p) => path.normalize(p));
_.each(assetItems, (assetItem) => {
_.each(normalizedPaths, (currentNormalizedPath) => {
const imagePath = path.join(assetItem.directory, assetItem.filename);
if (currentNormalizedPath.indexOf(path.normalize(imagePath)) !== -1) {
assetItem.path = currentNormalizedPath;
assetItem.size = `${assetItem.width}${AssetConstants.sizeDelimiter}${assetItem.height}`;
assetSubGroup.images.push(assetItem);
return false;
}
});
const imagePath = path.join(
basePath,
assetItem.directory,
assetItem.filename
);
assetItem.path = imagePath;
if (assetItem.width && assetItem.height) {
assetItem.size = `${assetItem.width}${AssetConstants.sizeDelimiter}${assetItem.height}`;
}
assetSubGroup.images.push(assetItem);
});

return assetSubGroup;
Expand Down