Optimize Android TV launcher icon

This commit is contained in:
chen08209
2026-08-05 14:54:07 +08:00
parent 4551e57ff4
commit 9a99897864
8 changed files with 128 additions and 0 deletions
@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="240"
android:viewportHeight="240">
<group
android:scaleX="0.9"
android:scaleY="0.9"
android:translateX="15.23"
android:translateY="12.22">
<path
android:fillColor="#6666FB"
android:pathData="M60.65,89.6L154.18,35.6A18,18 107.59,0 1,178.77 42.19L178.77,42.19A18,18 107.59,0 1,172.18 66.78L78.65,120.78A18,18 106.67,0 1,54.06 114.19L54.06,114.19A18,18 106.67,0 1,60.65 89.6z" />
<path
android:fillColor="#336AB6"
android:pathData="M84.65,131.17L131.42,104.17A18,18 107.83,0 1,156 110.76L156,110.76A18,18 107.83,0 1,149.42 135.35L102.65,162.35A18,18 106.67,0 1,78.06 155.76L78.06,155.76A18,18 106.67,0 1,84.65 131.17z" />
<path
android:fillColor="#5CA8E9"
android:pathData="M108.65,172.74L108.65,172.74A18,18 116.03,0 1,133.24 179.33L133.24,179.33A18,18 116.03,0 1,126.65 203.92L126.65,203.92A18,18 116.03,0 1,102.06 197.33L102.06,197.33A18,18 116.03,0 1,108.65 172.74z" />
</group>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground_tv" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+102
View File
@@ -0,0 +1,102 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart';
String _androidAttribute(String source, String element, String attribute) {
final elementTag = RegExp('<$element\\b[^>]*>').firstMatch(source)!.group(0)!;
return RegExp(
'android:$attribute="([^"]+)"',
).firstMatch(elementTag)!.group(1)!;
}
double _androidDoubleAttribute(
String source,
String element,
String attribute,
) {
return double.parse(_androidAttribute(source, element, attribute));
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('TV launcher icons meet density-specific minimum sizes', () async {
const expectedSizes = {
'mdpi': 80,
'hdpi': 120,
'xhdpi': 160,
'xxhdpi': 240,
'xxxhdpi': 320,
};
for (final MapEntry(key: density, value: size) in expectedSizes.entries) {
final file = File(
'android/app/src/main/res/'
'mipmap-television-$density/ic_launcher.webp',
);
expect(file.existsSync(), isTrue, reason: 'missing ${file.path}');
final codec = await ui.instantiateImageCodec(await file.readAsBytes());
final frame = await codec.getNextFrame();
expect(
(frame.image.width, frame.image.height),
(size, size),
reason: file.path,
);
frame.image.dispose();
codec.dispose();
}
});
test('TV adaptive launcher icon stays centered in the safe zone', () {
final adaptiveIcon = File(
'android/app/src/main/res/'
'mipmap-television-anydpi-v26/ic_launcher.xml',
).readAsStringSync();
expect(
_androidAttribute(adaptiveIcon, 'foreground', 'drawable'),
'@drawable/ic_launcher_foreground_tv',
);
expect(
_androidAttribute(adaptiveIcon, 'background', 'drawable'),
'@color/ic_launcher_background',
);
final vector = File(
'android/app/src/main/res/drawable/'
'ic_launcher_foreground_tv.xml',
).readAsStringSync();
final scaleX = _androidDoubleAttribute(vector, 'group', 'scaleX');
final scaleY = _androidDoubleAttribute(vector, 'group', 'scaleY');
final translateX = _androidDoubleAttribute(vector, 'group', 'translateX');
final translateY = _androidDoubleAttribute(vector, 'group', 'translateY');
final viewportWidth = _androidDoubleAttribute(
vector,
'vector',
'viewportWidth',
);
final viewportHeight = _androidDoubleAttribute(
vector,
'vector',
'viewportHeight',
);
// Conservative bounds of the current logo, including the curved caps.
const logoBounds = ui.Rect.fromLTRB(54, 33, 179, 206.5);
final transformedBounds = ui.Rect.fromLTRB(
(logoBounds.left * scaleX + translateX) / viewportWidth * 108,
(logoBounds.top * scaleY + translateY) / viewportHeight * 108,
(logoBounds.right * scaleX + translateX) / viewportWidth * 108,
(logoBounds.bottom * scaleY + translateY) / viewportHeight * 108,
);
const safeZone = ui.Rect.fromLTWH(18, 18, 72, 72);
expect(transformedBounds.left, greaterThanOrEqualTo(safeZone.left));
expect(transformedBounds.top, greaterThanOrEqualTo(safeZone.top));
expect(transformedBounds.right, lessThanOrEqualTo(safeZone.right));
expect(transformedBounds.bottom, lessThanOrEqualTo(safeZone.bottom));
expect(transformedBounds.center.dx, closeTo(safeZone.center.dx, 0.05));
expect(transformedBounds.center.dy, closeTo(safeZone.center.dy, 0.05));
});
}