Optimize Android TV launcher icon
This commit is contained in:
@@ -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 |
@@ -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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user