目录
Microsoft Visual Studio的Android库项目中的Android图标资源
介绍
本文适用于在使用Xamarin开发Android应用和iOS应用方面已有一定经验的开发人员。虽然周围有很多图标库,无论是免费的还是商业的,但有时,您可能希望从现有的图像文件(理想情况下是SVG文件)生成图标集。
本文将分享我在开发原生移动应用程序时从SVG文件生成各种大小的图标的经验。虽然我使用的主要开发工具是Xamarin,但即使您一直在使用XCode或Android Studio等其他工具,这里提到的原则和工具也应该适用。
XCode 14+支持单一大小的应用程序图标。Android Studio还包含 Image Asset Studio,可根据材质图标和自定义图片生成应用图标。但是,如果出于持续集成的目的不想使用这种交互式方式,或者你正在使用Xamarin,请继续阅读。
背景
几年前,我开发了Visual Acuity Charts,用于测试远视力,以检查近视的早期迹象。有一些免费的SVG图标,我使用了一些在线转换工具来生成所需的图标集。我一直在使用Nika Nikabadze为Visual Sutido创建的“材质图标生成器”,App Icon Generator。
Microsoft Visual Studio的Android库项目中的Android图标资源
iOS 图标资产
但是,使用这些VS扩展或在线工具的过程相当麻烦:
- 使用VS扩展时,太多的用户交互困扰着我。
- 使用在线工具:上传SVG文件 -> 配置 -> 生成 -> 等待 -> 下载zip -> 解压缩到项目文件夹。总的来说,这样的过程对持续集成并不友好,因此我编写了一系列PowerShell脚本来生成图标集。
总的来说,这两个工具相当不错,但是,如果您感到我所感受到的麻烦,您可以尝试本文中介绍的方法。
先决条件
SVG图标库
还有更多,免费或商业的。
言论
- 确保免费图标的使用符合Google Material图标库使用的 SIL开放字体许可证(OFL) 等许可证。
- Google Material Icons网站以ZIP格式提供了适用于Android和iOS的图像集下载。
"Inkscape 是一个免费的开源矢量图形编辑器,适用于GNU/Linux、Windows和macOS。
作为一名软件开发人员,我偶尔会做一些随意而简单的图形设计,我不打算购买专业平面设计师通常使用的多合一、功能强大且复杂的图形设计工具。Inkscape满足了我的需求。
撰写简单的图标
例如,我没有从头开始绘制所有内容,而是使用现有的SVG文件来编写应用程序启动图标。
应用启动器图标
Google Material Icons中的可见性图标
尽管如此,Inscape还是为您提供了丰富的功能,让您从头开始绘制复杂的图标或图像。
调整现有图标以符合Google和Apple的设计指南
有时,您可能会发现一些候选者不太符合特定的设计准则,并希望调整大小和边距。
引用
使用代码
以下PowerShell脚本利用Inkscape的命令行功能,脚本的起草参考了上面的图标设计指南。
Android
以下脚本为按钮和应用图标生成图标。
svgForAndroid.ps1
param([string]$svgFile, [int]$width, [int]$height, [boolean]$forAppIcon,
[string]$appIconName)
# Create icons for drawable and mipmap
# Examples:
# ./svgForAndroid.ps1 -svgFile "my.svg" -width 36
# ./svgForAndroid.ps1 "my.svg" 36
# For App Launcher Icons,
# ref: https://developer.android.com/training/multiscreen/screendensities
# ./svgForAndroid.ps1 -svgFile "my.svg" -width 48 -forAppIcon $true
# and this is excluding 36x36 ldpi
# The script file and the svg file should be in the same folder,
# and the generated files will be in a sub-folder.
# For convenience of developing using Xamarin, follow such convension:
# Rename the svg file to something like send_36.svg if you want 36pt.
# Remarks: Android requires all resource file names are in lower case.
cd $PSScriptRoot
$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
function export([string]$subDir, [decimal]$ratio){
$dir= [System.IO.Path]::Combine($baseFileName,$subDir)
$exportedFileName=If ($forAppIcon) {If ($appIconName) {$appIconName+".png"}
Else {"5367533/ic_launcher.png"}} Else {$baseFileName+"_"+ $width + ".png"}
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$dwidth=[int]($width * $ratio)
$dheight=0
if ($height -gt 0){$dheight = $height * $ratio} Else {$dheight = $dwidth}
$arguments="$svgFile --export-filename=$exported -w $dwidth -h $dheight -d 96"
New-Item -ItemType Directory -Force -Path $dir
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
}
If ($forAppIcon){
export "mipmap-mdpi" 1
export "mipmap-hdpi" 1.5
export "mipmap-xhdpi" 2
export "mipmap-xxhdpi" 3
export "mipmap-xxxhdpi" 4
} Else {
export "drawable-mdpi" 1
export "drawable-hdpi" 1.5
export "drawable-xhdpi" 2
export "drawable-xxhdpi" 3
export "drawable-xxxhdpi" 4
}
iOS系统
图像集
以下脚本生成三个png文件并Contents.json。
svgForIOSImageset.ps1
param([string]$svgFile, [Int32]$width, [Int32]$height, [boolean]$original)
# Create icons for imageset.
# Examples:
# .\svgForAndroid.ps1 -svgFile "my.svg" -width 36 -original $true
# .\svgForIOSImageset.ps1 -svgFile "my.svg" -width 36
# If $original is false, template-rendering-intent in Contents.json
# will become template for visual effects such as replacing colors.
# The script file and the svg file should be in the same folder,
# and the generated files will be in a sub-folder.
cd $PSScriptRoot
$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
$dir= $baseFileName + "_" + $width + ".imageset"
New-Item -ItemType Directory -Force -Path $dir
function export([decimal]$ratio){
$exportedFileName=$baseFileName + "_" + $width +"pt_" + $ratio + "x.png"
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$dwidth=[int]($width * $ratio)
$dheight=0
if ($height -gt 0){$dheight = $height * $ratio} Else {$dheight = $dwidth}
$arguments="$svgFile --export-filename=$exported -w $dwidth -h $dheight -d 96"
Write-Host $arguments
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
return $exportedFileName
}
$f1=export 1
$f2=export 2
$f3=export 3
$intent=If ($original) {"original"} Else {"template"}
$contentsTemplate=
@"
{
"images": [
{
"filename": "$f1",
"idiom": "universal",
"scale": "1x"
},
{
"filename": "$f2",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "$f3",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "whocare",
"template-rendering-intent": "$intent",
"version": 1
}
}
"@
$jsonFile=[System.IO.Path]::Combine($dir, "Contents.json")
Set-Content $jsonFile $contentsTemplate
运行脚本后,您将在 myicon.imageset 等文件夹中获取这些文件:
应用图标
运行脚本后,你将获得26个文件,包括 myAppIcon.appiconset 等文件夹中的Contents.json:
svgForIOSAppIconSet.ps1
param([string]$svgFile)
# Create AppIcons.appiconset of Assets.xcassets
# Examples:
# ./svgForIOSAppIconSet.ps1 -svgFile "my.svg"
# The script file and the svg file should be in the same folder,
# and the generated files will be in a sub-folder like "my.appiconset".
cd $PSScriptRoot
$baseFileName=[System.IO.Path]::GetFileNameWithoutExtension($svgFile)
$dir= $baseFileName + ".appiconset"
New-Item -ItemType Directory -Force -Path $dir
function export([decimal]$size){
$exportedFileName=$size.ToString() + ".png"
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$arguments="$svgFile --export-filename $exported -w $size -h $size"
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
return $exportedFileName
}
function exportForWatch(){
$exportedFileName="watch.png"
$exported= [System.IO.Path]::Combine($dir, $exportedFileName)
$arguments="$svgFile --export-filename $exported -w 55 -h 55"
$procArgs = @{
FilePath = "C:\Program Files\Inkscape\bin\inkscape.exe"
ArgumentList = $arguments
PassThru = $true
}
$processTsc = Start-Process @procArgs
return $exportedFileName
}
export 20
export 29
export 32
export 40
export 50
export 57
export 58
export 60
export 64
export 72
export 76
export 80
export 87
export 100
export 114
export 120
export 128
export 144
export 152
export 167
export 180
export 256
export 512
export 1024
exportForWatch
$contentsTemplate=
@"
{
"images": [
{
"size": "60x60",
"expected-size": "180",
"filename": "180.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "40x40",
"expected-size": "80",
"filename": "80.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "40x40",
"expected-size": "120",
"filename": "120.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "60x60",
"expected-size": "120",
"filename": "120.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "57x57",
"expected-size": "57",
"filename": "57.png",
"idiom": "iphone",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "29x29",
"expected-size": "29",
"filename": "29.png",
"idiom": "iphone",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "87",
"filename": "87.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "57x57",
"expected-size": "114",
"filename": "114.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "40",
"filename": "40.png",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "60",
"filename": "60.png",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "1024x1024",
"filename": "1024.png",
"expected-size": "1024",
"idiom": "ios-marketing",
"scale": "1x"
},
{
"size": "40x40",
"expected-size": "80",
"filename": "80.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "72x72",
"expected-size": "72",
"filename": "72.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "76x76",
"expected-size": "152",
"filename": "152.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "50x50",
"expected-size": "100",
"filename": "100.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "76x76",
"expected-size": "76",
"filename": "76.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "29",
"filename": "29.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "50x50",
"expected-size": "50",
"filename": "50.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "72x72",
"expected-size": "144",
"filename": "144.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "40x40",
"expected-size": "40",
"filename": "40.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "83.5x83.5",
"expected-size": "167",
"filename": "167.png",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "20",
"filename": "20.png",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "20x20",
"expected-size": "40",
"filename": "40.png",
"idiom": "ipad",
"scale": "2x"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "38mm",
"scale": "2x",
"size": "86x86",
"expected-size": "172",
"role": "quickLook"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "38mm",
"scale": "2x",
"size": "40x40",
"expected-size": "80",
"role": "appLauncher"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "42mm",
"scale": "2x",
"size": "98x98",
"expected-size": "196",
"role": "quickLook"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "38mm",
"scale": "2x",
"size": "24x24",
"expected-size": "48",
"role": "notificationCenter"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "42mm",
"scale": "2x",
"size": "27.5x27.5",
"expected-size": "55",
"role": "notificationCenter"
},
{
"size": "29x29",
"expected-size": "87",
"filename": "87.png",
"idiom": "watch",
"role": "companionSettings",
"scale": "3x"
},
{
"idiom": "watch",
"filename": "watch.png",
"subtype": "42mm",
"scale": "2x",
"size": "44x44",
"expected-size": "88",
"role": "longLook"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"idiom": "watch",
"role": "companionSettings",
"scale": "2x"
},
{
"size": "1024x1024",
"expected-size": "1024",
"filename": "1024.png",
"idiom": "watch-marketing",
"scale": "1x"
},
{
"size": "128x128",
"expected-size": "128",
"filename": "128.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "256x256",
"expected-size": "256",
"filename": "256.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "128x128",
"expected-size": "256",
"filename": "256.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "256x256",
"expected-size": "512",
"filename": "512.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "32x32",
"expected-size": "32",
"filename": "32.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "512x512",
"expected-size": "512",
"filename": "512.png",
"idiom": "mac",
"scale": "1x"
},
{
"size": "16x16",
"expected-size": "32",
"filename": "32.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "32x32",
"expected-size": "64",
"filename": "64.png",
"idiom": "mac",
"scale": "2x"
},
{
"size": "512x512",
"expected-size": "1024",
"filename": "1024.png",
"idiom": "mac",
"scale": "2x"
}
]
}
"@
$jsonFile=[System.IO.Path]::Combine($dir, "Contents.json")
Set-Content $jsonFile $contentsTemplate
兴趣点
您可以根据SDLC/CI流程调整本文中提供的PowerShell脚本。
我在开发原生移动应用程序方面的经验仅限于智能手机和平板电脑。因此,如果您正在为智能手表或智能电视开发本机应用程序,您可能需要根据各自的图标设计规范添加更多行。
MAUI
若要开发适用于Android和iOS的本机应用,需要创建至少两个特定于平台的应用项目,一个用于Android,另一个用于iOS,而共享库代码包含在Xamarin.Forms项目和.NET Standard项目中。
使用 MAUI,您只需要一个适用于Android、iOS、Windows和Mac等的应用程序项目。图标源可以是矢量图像(SVG文件)。显然,MAUI将读取SVG文件并根据各自平台的要求生成PNG文件,就像本文中的脚本文件一样。这听起来很自然,很有前途,而且很有成效。
几年前,我为Xamarin重写了“Tour of Heroes”(Angular的官方教程应用),以演示如何在实际项目中使用 WebApiClientGen。2023年10月,我已将应用程序迁移到MAUI。Xamarin构建的Android可部署版本约为22MB,但MAUI的可部署版本约为33MB,而两者都是发布版本。
这种规模上的通胀显然是不可取的。我将有兴趣检查Microsoft是否可以在.NET Conf 2023之后以及2024年4月计划停用Xamarin之前解决这个问题。
https://www.codeproject.com/Articles/5367533/Generate-Icon-Sets-for-Android-and-iOS-in-Native-M