Skip to content

Commit 4177844

Browse files
✨ Allow text delegates fallback to another one with semantics (#245)
Co-authored-by: Yaniv Shaked <[email protected]>
1 parent 3a11fe8 commit 4177844

6 files changed

+130
-46
lines changed

lib/src/delegates/asset_picker_builder_delegate.dart

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
209209

210210
AssetPickerTextDelegate get textDelegate => Singleton.textDelegate;
211211

212+
AssetPickerTextDelegate get semanticsTextDelegate =>
213+
Singleton.textDelegate.semanticsTextDelegate;
214+
212215
/// Keep a `initState` method to sync with [State].
213216
/// 保留一个 `initState` 方法与 [State] 同步。
214217
@mustCallSuper
@@ -394,6 +397,7 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
394397
fontSize: 13,
395398
fontWeight: FontWeight.w500,
396399
),
400+
semanticsLabel: semanticsTextDelegate.gifIndicator,
397401
strutStyle: const StrutStyle(forceStrutHeight: true, height: 1),
398402
),
399403
),
@@ -430,6 +434,7 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
430434
return ScaleText(
431435
textDelegate.emptyList,
432436
maxScaleFactor: 1.5,
437+
semanticsLabel: semanticsTextDelegate.emptyList,
433438
);
434439
}
435440
return w!;
@@ -450,6 +455,7 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
450455
textDelegate.loadFailed,
451456
textAlign: TextAlign.center,
452457
style: const TextStyle(fontSize: 18),
458+
semanticsLabel: semanticsTextDelegate.loadFailed,
453459
),
454460
);
455461
}
@@ -496,6 +502,7 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
496502
style: context.themeData.textTheme.caption?.copyWith(
497503
fontSize: 14,
498504
),
505+
semanticsLabel: semanticsTextDelegate.accessAllTip,
499506
),
500507
),
501508
Icon(
@@ -580,12 +587,14 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
580587
textDelegate.unableToAccessAll,
581588
style: const TextStyle(fontSize: 22),
582589
textAlign: TextAlign.center,
590+
semanticsLabel: semanticsTextDelegate.unableToAccessAll,
583591
),
584592
SizedBox(height: size.height / 30),
585593
ScaleText(
586594
textDelegate.accessAllTip,
587595
style: const TextStyle(fontSize: 18),
588596
textAlign: TextAlign.center,
597+
semanticsLabel: semanticsTextDelegate.accessAllTip,
589598
),
590599
],
591600
),
@@ -603,6 +612,7 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
603612
child: ScaleText(
604613
textDelegate.goToSystemSettings,
605614
style: const TextStyle(fontSize: 17),
615+
semanticsLabel: semanticsTextDelegate.goToSystemSettings,
606616
),
607617
onPressed: PhotoManager.openSetting,
608618
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@@ -613,6 +623,7 @@ abstract class AssetPickerBuilderDelegate<Asset, Path> {
613623
child: ScaleText(
614624
textDelegate.accessLimitedAssets,
615625
style: TextStyle(color: interactiveTextColor(context)),
626+
semanticsLabel: semanticsTextDelegate.accessLimitedAssets,
616627
),
617628
);
618629

@@ -887,7 +898,7 @@ class DefaultAssetPickerBuilderDelegate
887898
backgroundColor: theme.appBarTheme.backgroundColor,
888899
centerTitle: isAppleOS,
889900
title: Semantics(
890-
onTapHint: textDelegate.sActionSwitchPathLabel,
901+
onTapHint: semanticsTextDelegate.sActionSwitchPathLabel,
891902
child: pathEntitySelector(context),
892903
),
893904
leading: backButton(context),
@@ -1285,9 +1296,10 @@ class DefaultAssetPickerBuilderDelegate
12851296
String hint = '';
12861297
if (asset.type == AssetType.audio ||
12871298
asset.type == AssetType.video) {
1288-
hint += '${textDelegate.sNameDurationLabel}: ';
1289-
hint +=
1290-
textDelegate.durationIndicatorBuilder(asset.videoDuration);
1299+
hint += '${semanticsTextDelegate.sNameDurationLabel}: ';
1300+
hint += semanticsTextDelegate.durationIndicatorBuilder(
1301+
asset.videoDuration,
1302+
);
12911303
}
12921304
if (asset.title?.isNotEmpty == true) {
12931305
hint += ', ${asset.title}';
@@ -1297,19 +1309,19 @@ class DefaultAssetPickerBuilderDelegate
12971309
enabled: !isBanned,
12981310
excludeSemantics: true,
12991311
focusable: !isSwitchingPath,
1300-
label: '${textDelegate.semanticTypeLabel(asset.type)}'
1312+
label: '${semanticsTextDelegate.semanticTypeLabel(asset.type)}'
13011313
'${semanticIndex(index)}, '
13021314
'${asset.createDateTime.toString().replaceAll('.000', '')}',
13031315
hidden: isSwitchingPath,
13041316
hint: hint,
13051317
image: asset.type == AssetType.image ||
13061318
asset.type == AssetType.video,
13071319
onTap: () => selectAsset(context, asset, isSelected),
1308-
onTapHint: textDelegate.sActionSelectHint,
1320+
onTapHint: semanticsTextDelegate.sActionSelectHint,
13091321
onLongPress: isPreviewEnabled
13101322
? () => _pushAssetToViewer(context, index, asset)
13111323
: null,
1312-
onLongPressHint: textDelegate.sActionPreviewHint,
1324+
onLongPressHint: semanticsTextDelegate.sActionPreviewHint,
13131325
selected: isSelected,
13141326
sortKey: OrdinalSortKey(
13151327
semanticIndex(index).toDouble(),
@@ -1382,9 +1394,6 @@ class DefaultAssetPickerBuilderDelegate
13821394

13831395
@override
13841396
Widget audioIndicator(BuildContext context, AssetEntity asset) {
1385-
final String durationText = textDelegate.durationIndicatorBuilder(
1386-
Duration(seconds: asset.duration),
1387-
);
13881397
return Container(
13891398
width: double.maxFinite,
13901399
alignment: AlignmentDirectional.bottomStart,
@@ -1399,10 +1408,14 @@ class DefaultAssetPickerBuilderDelegate
13991408
child: Padding(
14001409
padding: const EdgeInsetsDirectional.only(start: 4),
14011410
child: ScaleText(
1402-
durationText,
1403-
semanticsLabel: '${textDelegate.sNameDurationLabel}: '
1404-
'$durationText',
1411+
textDelegate.durationIndicatorBuilder(
1412+
Duration(seconds: asset.duration),
1413+
),
14051414
style: const TextStyle(fontSize: 16),
1415+
semanticsLabel: '${semanticsTextDelegate.sNameDurationLabel}: '
1416+
'${semanticsTextDelegate.durationIndicatorBuilder(
1417+
Duration(seconds: asset.duration),
1418+
)}',
14061419
),
14071420
),
14081421
);
@@ -1470,6 +1483,10 @@ class DefaultAssetPickerBuilderDelegate
14701483
fontSize: 17,
14711484
fontWeight: FontWeight.normal,
14721485
),
1486+
semanticsLabel: p.isSelectedNotEmpty && !isSingleAssetMode
1487+
? '${semanticsTextDelegate.confirm}'
1488+
' (${p.selectedAssets.length}/${p.maxAssets})'
1489+
: semanticsTextDelegate.confirm,
14731490
),
14741491
onPressed: p.isSelectedNotEmpty
14751492
? () => Navigator.of(context).maybePop(p.selectedAssets)
@@ -1525,6 +1542,7 @@ class DefaultAssetPickerBuilderDelegate
15251542
return ScaleText(
15261543
textDelegate.emptyList,
15271544
maxScaleFactor: 1.5,
1545+
semanticsLabel: semanticsTextDelegate.emptyList,
15281546
);
15291547
}
15301548
return PlatformProgressIndicator(
@@ -1601,8 +1619,8 @@ class DefaultAssetPickerBuilderDelegate
16011619
ValueListenableBuilder<PermissionState>(
16021620
valueListenable: permission,
16031621
builder: (_, PermissionState ps, Widget? child) => Semantics(
1604-
label: '${textDelegate.viewingLimitedAssetsTip}, '
1605-
'${textDelegate.changeAccessibleLimitedAssets}',
1622+
label: '${semanticsTextDelegate.viewingLimitedAssetsTip}, '
1623+
'${semanticsTextDelegate.changeAccessibleLimitedAssets}',
16061624
button: true,
16071625
onTap: PhotoManager.presentLimited,
16081626
hidden: !isPermissionLimited,
@@ -1700,6 +1718,9 @@ class DefaultAssetPickerBuilderDelegate
17001718
maxLines: 1,
17011719
overflow: TextOverflow.ellipsis,
17021720
maxScaleFactor: 1.2,
1721+
semanticsLabel: isPermissionLimited && p.isAll
1722+
? semanticsTextDelegate.accessiblePathName
1723+
: p.name,
17031724
),
17041725
),
17051726
w!,
@@ -1765,20 +1786,23 @@ class DefaultAssetPickerBuilderDelegate
17651786
return ColoredBox(color: theme.colorScheme.primary.withOpacity(0.12));
17661787
}
17671788

1768-
final String semanticsName = isPermissionLimited && pathEntity.isAll
1789+
final String name = isPermissionLimited && pathEntity.isAll
17691790
? textDelegate.accessiblePathName
17701791
: pathEntity.name;
1792+
final String semanticsName = isPermissionLimited && pathEntity.isAll
1793+
? semanticsTextDelegate.accessiblePathName
1794+
: pathEntity.name;
17711795
final String semanticsCount = '${pathEntity.assetCount}';
17721796
return Selector<DefaultAssetPickerProvider, AssetPathEntity?>(
17731797
selector: (_, DefaultAssetPickerProvider p) => p.currentPath,
17741798
builder: (_, AssetPathEntity? currentPathEntity, __) {
17751799
final bool isSelected = currentPathEntity == pathEntity;
17761800
return Semantics(
17771801
label: '$semanticsName, '
1778-
'${textDelegate.sUnitAssetCountLabel}: '
1802+
'${semanticsTextDelegate.sUnitAssetCountLabel}: '
17791803
'$semanticsCount',
17801804
selected: isSelected,
1781-
onTapHint: textDelegate.sActionSwitchPathLabel,
1805+
onTapHint: semanticsTextDelegate.sActionSwitchPathLabel,
17821806
button: false,
17831807
child: Material(
17841808
type: MaterialType.transparency,
@@ -1814,7 +1838,7 @@ class DefaultAssetPickerBuilderDelegate
18141838
end: 10,
18151839
),
18161840
child: ScaleText(
1817-
semanticsName,
1841+
name,
18181842
style: const TextStyle(fontSize: 17),
18191843
maxLines: 1,
18201844
overflow: TextOverflow.ellipsis,
@@ -1887,7 +1911,7 @@ class DefaultAssetPickerBuilderDelegate
18871911
enabled: p.isSelectedNotEmpty,
18881912
focusable: !isSwitchingPath,
18891913
hidden: isSwitchingPath,
1890-
onTapHint: textDelegate.sActionPreviewHint,
1914+
onTapHint: semanticsTextDelegate.sActionPreviewHint,
18911915
child: child,
18921916
),
18931917
);
@@ -1910,6 +1934,8 @@ class DefaultAssetPickerBuilderDelegate
19101934
fontSize: 17,
19111935
),
19121936
maxScaleFactor: 1.2,
1937+
semanticsLabel: '${semanticsTextDelegate.preview}'
1938+
'${p.isSelectedNotEmpty ? ' (${p.selectedAssets.length})' : ''}',
19131939
),
19141940
),
19151941
),
@@ -2071,6 +2097,10 @@ class DefaultAssetPickerBuilderDelegate
20712097
),
20722098
maxLines: 1,
20732099
maxScaleFactor: 1.2,
2100+
semanticsLabel:
2101+
semanticsTextDelegate.durationIndicatorBuilder(
2102+
Duration(seconds: asset.duration),
2103+
),
20742104
),
20752105
),
20762106
),

lib/src/delegates/asset_picker_text_delegate.dart

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
/// [Author] Alex (https://github.com/Alex525)
33
/// [Date] 2020/4/7 10:25
44
///
5+
import 'dart:io' show Platform;
6+
57
import 'package:flutter/rendering.dart';
68
import 'package:photo_manager/photo_manager.dart' show AssetType;
79

@@ -112,7 +114,6 @@ class AssetPickerTextDelegate {
112114
///
113115
/// Fields below are only for semantics usage. For customizable these fields,
114116
/// head over to [EnglishAssetPickerTextDelegate] for fields understanding.
115-
116117
String get sTypeAudioLabel => '音频';
117118

118119
String get sTypeImageLabel => '图片';
@@ -147,6 +148,17 @@ class AssetPickerTextDelegate {
147148
String get sNameDurationLabel => '时长';
148149

149150
String get sUnitAssetCountLabel => '数量';
151+
152+
/// Fallback delegate for semantics determined by platform.
153+
///
154+
/// The purpose of this field is to provide a fallback delegate references
155+
/// when a language does not supported by Talkback or VoiceOver. Set this to
156+
/// another text delegate makes screen readers read accordingly.
157+
///
158+
/// See also:
159+
/// * Talkback: https://support.google.com/accessibility/android/answer/11101402)
160+
/// * VoiceOver: https://support.apple.com/en-us/HT206175
161+
AssetPickerTextDelegate get semanticsTextDelegate => this;
150162
}
151163

152164
/// [AssetPickerTextDelegate] implements with English.
@@ -302,39 +314,46 @@ class HebrewAssetPickerTextDelegate extends AssetPickerTextDelegate {
302314
@override
303315
String get accessiblePathName => 'קבצים נגישים';
304316

305-
// Using English for semantics usage, since Hebrew is not supported for TalkBack.
306317
@override
307-
String get sTypeAudioLabel => 'Audio';
318+
String get sTypeAudioLabel => 'שמע';
308319

309320
@override
310-
String get sTypeImageLabel => 'Image';
321+
String get sTypeImageLabel => 'תמונה';
311322

312323
@override
313-
String get sTypeVideoLabel => 'Video';
324+
String get sTypeVideoLabel => 'סרטון';
314325

315326
@override
316-
String get sTypeOtherLabel => 'Other asset';
327+
String get sTypeOtherLabel => 'קובץ אחר';
317328

318329
@override
319-
String get sActionPlayHint => 'play';
330+
String get sActionPlayHint => 'נגן';
320331

321332
@override
322-
String get sActionPreviewHint => 'preview';
333+
String get sActionPreviewHint => 'תצוגה מקדימה';
323334

324335
@override
325-
String get sActionSelectHint => 'select';
336+
String get sActionSelectHint => 'בחר';
326337

327338
@override
328-
String get sActionSwitchPathLabel => 'switch path';
339+
String get sActionSwitchPathLabel => 'החלף תיקייה';
329340

330341
@override
331-
String get sActionUseCameraHint => 'use camera';
342+
String get sActionUseCameraHint => 'השתמש במצלמה';
332343

333344
@override
334-
String get sNameDurationLabel => 'duration';
345+
String get sNameDurationLabel => 'משך';
335346

336347
@override
337-
String get sUnitAssetCountLabel => 'count';
348+
String get sUnitAssetCountLabel => 'כמות';
349+
350+
@override
351+
AssetPickerTextDelegate get semanticsTextDelegate {
352+
if (Platform.isAndroid) {
353+
return EnglishAssetPickerTextDelegate();
354+
}
355+
return this;
356+
}
338357
}
339358

340359
/// [AssetPickerTextDelegate] implementiert mit der deutschen Übersetzung.

0 commit comments

Comments
 (0)