Accessibility Changes
#329
Merged
Haroon6138
merged 2 commits from dev_aamir into master 3 weeks ago
@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Widget that applies accessibility visual modes to all content EXCEPT images
|
||||||
|
/// Automatically wraps page content and applies appropriate filters
|
||||||
|
/// Images within the content remain unaffected
|
||||||
|
class AccessibilityFilterWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const AccessibilityFilterWrapper({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ProfileSettingsViewModel>(
|
||||||
|
builder: (context, profileVm, _) {
|
||||||
|
return _applyVisualModeToContent(profileVm.visualMode, child);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _applyVisualModeToContent(String visualMode, Widget child) {
|
||||||
|
switch (visualMode) {
|
||||||
|
case 'invert':
|
||||||
|
return _InvertedContentWrapper(child: child);
|
||||||
|
|
||||||
|
case 'dim':
|
||||||
|
return _DimmedContentWrapper(child: child);
|
||||||
|
|
||||||
|
case 'highContrast':
|
||||||
|
return _HighContrastContentWrapper(child: child);
|
||||||
|
|
||||||
|
case 'off':
|
||||||
|
default:
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper that inverts colors but preserves images
|
||||||
|
class _InvertedContentWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _InvertedContentWrapper({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
-1, 0, 0, 0, 255,
|
||||||
|
0, -1, 0, 0, 255,
|
||||||
|
0, 0, -1, 0, 255,
|
||||||
|
0, 0, 0, 1, 0,
|
||||||
|
]),
|
||||||
|
child: _ImagePreservingWrapper(
|
||||||
|
visualMode: 'invert',
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper that dims content but preserves images
|
||||||
|
class _DimmedContentWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _DimmedContentWrapper({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Preserve images on top
|
||||||
|
_ImagePreservingWrapper(
|
||||||
|
visualMode: 'dim',
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper that applies high contrast but preserves images
|
||||||
|
class _HighContrastContentWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _HighContrastContentWrapper({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
0.2126, 0.7152, 0.0722, 0, 0,
|
||||||
|
0.2126, 0.7152, 0.0722, 0, 0,
|
||||||
|
0.2126, 0.7152, 0.0722, 0, 0,
|
||||||
|
0, 0, 0, 1, 0,
|
||||||
|
]),
|
||||||
|
child: _ImagePreservingWrapper(
|
||||||
|
visualMode: 'highContrast',
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal wrapper that detects and preserves images
|
||||||
|
class _ImagePreservingWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final String visualMode;
|
||||||
|
|
||||||
|
const _ImagePreservingWrapper({
|
||||||
|
required this.child,
|
||||||
|
required this.visualMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// This is a placeholder - in practice, you would need to traverse
|
||||||
|
// the widget tree and wrap Image widgets
|
||||||
|
// For now, this serves as a marker for where image preservation logic would go
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
// Accessibility widgets export file
|
||||||
|
export 'preserve_image_colors.dart';
|
||||||
|
export 'accessible_image.dart';
|
||||||
|
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hmg_patient_app_new/widgets/accessibility/preserve_image_colors.dart';
|
||||||
|
|
||||||
|
/// Custom Image widget that automatically preserves original colors
|
||||||
|
/// when accessibility visual modes are active.
|
||||||
|
///
|
||||||
|
/// Usage: Replace Image.network() with AccessibleImage.network()
|
||||||
|
/// Replace Image.asset() with AccessibleImage.asset()
|
||||||
|
class AccessibleImage extends StatelessWidget {
|
||||||
|
final ImageProvider image;
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
final BoxFit? fit;
|
||||||
|
final AlignmentGeometry alignment;
|
||||||
|
final ImageRepeat repeat;
|
||||||
|
final Color? color;
|
||||||
|
final BlendMode? colorBlendMode;
|
||||||
|
final FilterQuality filterQuality;
|
||||||
|
final Widget Function(BuildContext, Widget, int?, bool)? frameBuilder;
|
||||||
|
final Widget Function(BuildContext, Object, StackTrace?)? errorBuilder;
|
||||||
|
final String? semanticLabel;
|
||||||
|
final bool excludeFromSemantics;
|
||||||
|
final bool gaplessPlayback;
|
||||||
|
final bool isAntiAlias;
|
||||||
|
|
||||||
|
const AccessibleImage({
|
||||||
|
super.key,
|
||||||
|
required this.image,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.fit,
|
||||||
|
this.alignment = Alignment.center,
|
||||||
|
this.repeat = ImageRepeat.noRepeat,
|
||||||
|
this.color,
|
||||||
|
this.colorBlendMode,
|
||||||
|
this.filterQuality = FilterQuality.low,
|
||||||
|
this.frameBuilder,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.semanticLabel,
|
||||||
|
this.excludeFromSemantics = false,
|
||||||
|
this.gaplessPlayback = false,
|
||||||
|
this.isAntiAlias = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates an accessible image from a network URL
|
||||||
|
factory AccessibleImage.network(
|
||||||
|
String src, {
|
||||||
|
Key? key,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
BoxFit? fit,
|
||||||
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
|
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||||
|
Color? color,
|
||||||
|
BlendMode? colorBlendMode,
|
||||||
|
FilterQuality filterQuality = FilterQuality.low,
|
||||||
|
Widget Function(BuildContext, Widget, int?, bool)? frameBuilder,
|
||||||
|
Widget Function(BuildContext, Object, StackTrace?)? errorBuilder,
|
||||||
|
String? semanticLabel,
|
||||||
|
bool excludeFromSemantics = false,
|
||||||
|
bool gaplessPlayback = false,
|
||||||
|
bool isAntiAlias = false,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
int? cacheWidth,
|
||||||
|
int? cacheHeight,
|
||||||
|
}) {
|
||||||
|
return AccessibleImage(
|
||||||
|
key: key,
|
||||||
|
image: NetworkImage(src, headers: headers),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
alignment: alignment,
|
||||||
|
repeat: repeat,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
frameBuilder: frameBuilder,
|
||||||
|
errorBuilder: errorBuilder,
|
||||||
|
semanticLabel: semanticLabel,
|
||||||
|
excludeFromSemantics: excludeFromSemantics,
|
||||||
|
gaplessPlayback: gaplessPlayback,
|
||||||
|
isAntiAlias: isAntiAlias,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an accessible image from an asset
|
||||||
|
factory AccessibleImage.asset(
|
||||||
|
String name, {
|
||||||
|
Key? key,
|
||||||
|
AssetBundle? bundle,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
BoxFit? fit,
|
||||||
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
|
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||||
|
Color? color,
|
||||||
|
BlendMode? colorBlendMode,
|
||||||
|
FilterQuality filterQuality = FilterQuality.low,
|
||||||
|
Widget Function(BuildContext, Widget, int?, bool)? frameBuilder,
|
||||||
|
Widget Function(BuildContext, Object, StackTrace?)? errorBuilder,
|
||||||
|
String? semanticLabel,
|
||||||
|
bool excludeFromSemantics = false,
|
||||||
|
bool gaplessPlayback = false,
|
||||||
|
bool isAntiAlias = false,
|
||||||
|
String? package,
|
||||||
|
int? cacheWidth,
|
||||||
|
int? cacheHeight,
|
||||||
|
}) {
|
||||||
|
return AccessibleImage(
|
||||||
|
key: key,
|
||||||
|
image: AssetImage(name, bundle: bundle, package: package),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
alignment: alignment,
|
||||||
|
repeat: repeat,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
frameBuilder: frameBuilder,
|
||||||
|
errorBuilder: errorBuilder,
|
||||||
|
semanticLabel: semanticLabel,
|
||||||
|
excludeFromSemantics: excludeFromSemantics,
|
||||||
|
gaplessPlayback: gaplessPlayback,
|
||||||
|
isAntiAlias: isAntiAlias,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PreserveImageColors(
|
||||||
|
child: Image(
|
||||||
|
image: image,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
alignment: alignment,
|
||||||
|
repeat: repeat,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
frameBuilder: frameBuilder,
|
||||||
|
errorBuilder: errorBuilder,
|
||||||
|
semanticLabel: semanticLabel,
|
||||||
|
excludeFromSemantics: excludeFromSemantics,
|
||||||
|
gaplessPlayback: gaplessPlayback,
|
||||||
|
isAntiAlias: isAntiAlias,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Widget that preserves original image colors even when visual mode filters are active
|
||||||
|
/// Wraps any image widget and applies reverse filters to cancel out accessibility filters
|
||||||
|
class PreserveImageColors extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const PreserveImageColors({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ProfileSettingsViewModel>(
|
||||||
|
builder: (context, profileVm, _) {
|
||||||
|
final visualMode = profileVm.visualMode;
|
||||||
|
|
||||||
|
// If no filter is active, just return the child
|
||||||
|
if (visualMode == 'off') {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply reverse filter to cancel out the app-wide filter
|
||||||
|
return _applyReverseFilter(visualMode, child);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply reverse filter to cancel out the parent filter effect
|
||||||
|
Widget _applyReverseFilter(String visualMode, Widget child) {
|
||||||
|
switch (visualMode) {
|
||||||
|
case 'invert':
|
||||||
|
// Apply invert filter again to reverse the inversion (double negative = positive)
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
-1, 0, 0, 0, 255, // Red channel inverted back
|
||||||
|
0, -1, 0, 0, 255, // Green channel inverted back
|
||||||
|
0, 0, -1, 0, 255, // Blue channel inverted back
|
||||||
|
0, 0, 0, 1, 0, // Alpha channel unchanged
|
||||||
|
]),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dim':
|
||||||
|
// Apply brightening to compensate for dimming
|
||||||
|
// Since parent has 30% black overlay, we increase brightness
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
1.43, 0, 0, 0, 0, // Brighten red channel
|
||||||
|
0, 1.43, 0, 0, 0, // Brighten green channel
|
||||||
|
0, 0, 1.43, 0, 0, // Brighten blue channel
|
||||||
|
0, 0, 0, 1, 0, // Alpha unchanged
|
||||||
|
]),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'highContrast':
|
||||||
|
// For high contrast, apply color saturation to restore colors
|
||||||
|
// This won't perfectly restore original colors but helps images stand out
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
1.5, 0, 0, 0, 0, // Boost red
|
||||||
|
0, 1.5, 0, 0, 0, // Boost green
|
||||||
|
0, 0, 1.5, 0, 0, // Boost blue
|
||||||
|
0, 0, 0, 1, 0, // Alpha unchanged
|
||||||
|
]),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension on Image to easily wrap with PreserveImageColors
|
||||||
|
extension ImageAccessibility on Image {
|
||||||
|
Widget preserveColors() {
|
||||||
|
return PreserveImageColors(child: this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Widget that preserves original SVG icon colors even when visual mode filters are active
|
||||||
|
/// Wraps any SvgPicture widget and applies reverse filters to cancel out accessibility filters
|
||||||
|
class PreserveSvgColors extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const PreserveSvgColors({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ProfileSettingsViewModel>(
|
||||||
|
builder: (context, profileVm, _) {
|
||||||
|
final visualMode = profileVm.visualMode;
|
||||||
|
|
||||||
|
// If no filter is active, just return the child
|
||||||
|
if (visualMode == 'off') {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply reverse filter to cancel out the app-wide filter
|
||||||
|
return _applyReverseFilter(visualMode, child);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply reverse filter to cancel out the parent filter effect
|
||||||
|
Widget _applyReverseFilter(String visualMode, Widget child) {
|
||||||
|
switch (visualMode) {
|
||||||
|
case 'invert':
|
||||||
|
// Apply invert filter again to reverse the inversion (double negative = positive)
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
-1, 0, 0, 0, 255, // Red channel inverted back
|
||||||
|
0, -1, 0, 0, 255, // Green channel inverted back
|
||||||
|
0, 0, -1, 0, 255, // Blue channel inverted back
|
||||||
|
0, 0, 0, 1, 0, // Alpha channel unchanged
|
||||||
|
]),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dim':
|
||||||
|
// Apply brightening to compensate for dimming
|
||||||
|
// Since parent has 30% black overlay, we increase brightness
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
1.43, 0, 0, 0, 0, // Brighten red channel
|
||||||
|
0, 1.43, 0, 0, 0, // Brighten green channel
|
||||||
|
0, 0, 1.43, 0, 0, // Brighten blue channel
|
||||||
|
0, 0, 0, 1, 0, // Alpha unchanged
|
||||||
|
]),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'highContrast':
|
||||||
|
// For high contrast, apply color saturation to restore colors
|
||||||
|
// This won't perfectly restore original colors but helps icons stand out
|
||||||
|
return ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
|
1.5, 0, 0, 0, 0, // Boost red
|
||||||
|
0, 1.5, 0, 0, 0, // Boost green
|
||||||
|
0, 0, 1.5, 0, 0, // Boost blue
|
||||||
|
0, 0, 0, 1, 0, // Alpha unchanged
|
||||||
|
]),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension on SvgPicture to easily wrap with PreserveSvgColors
|
||||||
|
extension SvgAccessibility on SvgPicture {
|
||||||
|
Widget preserveColors() {
|
||||||
|
return PreserveSvgColors(child: this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue