Merge pull request 'Accessibility Changes' (#329) from dev_aamir into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/329master
commit
49b8a51480
@ -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