.net 10, refactor for better stability

This commit is contained in:
David H. Friedel Jr. 2025-12-09 18:39:20 -05:00
parent f9ab873c67
commit 2b003b7970
33 changed files with 2732 additions and 1629 deletions

807
API_Reference.md Normal file
View File

@ -0,0 +1,807 @@
# MarketAlly.TouchEffect.Maui API Reference
Complete API documentation for MarketAlly.TouchEffect.Maui v2.0.0.
## Table of Contents
- [TouchEffect Class](#toucheffect-class)
- [Attached Properties](#attached-properties)
- [Events](#events)
- [Static Methods](#static-methods)
- [TouchBehavior Class](#touchbehavior-class)
- [TouchEffectBuilder Class](#toucheffectbuilder-class)
- [TouchEffectPresets Class](#toucheffectpresets-class)
- [Enumerations](#enumerations)
- [Interfaces](#interfaces)
- [Constants](#constants)
---
## TouchEffect Class
The core class that provides touch and hover visual feedback for any `VisualElement`.
**Namespace:** `MarketAlly.TouchEffect.Maui`
**Inheritance:** `RoutingEffect`
### Attached Properties
#### State Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `IsAvailable` | `bool` | `true` | Enables or disables the touch effect. When `false`, no touch feedback occurs. |
| `ShouldMakeChildrenInputTransparent` | `bool` | `true` | When `true`, child elements become input-transparent to allow touch to pass through. |
| `Status` | `TouchStatus` | `Completed` | **Read-only.** Current touch status (Started, Completed, Canceled). |
| `State` | `TouchState` | `Normal` | **Read-only.** Current touch state (Normal, Pressed). |
| `InteractionStatus` | `TouchInteractionStatus` | `Completed` | **Read-only.** Current interaction status. |
| `HoverStatus` | `HoverStatus` | `Exited` | **Read-only.** Current hover status (Entered, Exited). |
| `HoverState` | `HoverState` | `Normal` | **Read-only.** Current hover state (Normal, Hovered). |
**Usage:**
```xml
<Frame touch:TouchEffect.IsAvailable="{Binding CanInteract}"
touch:TouchEffect.ShouldMakeChildrenInputTransparent="True">
<Label Text="Interactive Content" />
</Frame>
```
#### Command Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Command` | `ICommand` | `null` | Command executed when touch completes successfully. |
| `CommandParameter` | `object` | `null` | Parameter passed to the `Command`. |
| `LongPressCommand` | `ICommand` | `null` | Command executed after long press duration elapses. |
| `LongPressCommandParameter` | `object` | `null` | Parameter for the `LongPressCommand`. Falls back to `CommandParameter` if null. |
| `LongPressDuration` | `int` | `500` | Duration in milliseconds before `LongPressCommand` executes. |
**Usage:**
```xml
<Frame touch:TouchEffect.Command="{Binding TapCommand}"
touch:TouchEffect.CommandParameter="{Binding Item}"
touch:TouchEffect.LongPressCommand="{Binding ContextMenuCommand}"
touch:TouchEffect.LongPressDuration="800">
<Label Text="Tap or Long Press" />
</Frame>
```
#### Background Color Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NormalBackgroundColor` | `Color` | `Default` | Background color in normal state. |
| `HoveredBackgroundColor` | `Color` | `Default` | Background color when hovered (desktop platforms). |
| `PressedBackgroundColor` | `Color` | `Default` | Background color when pressed. |
**Usage:**
```xml
<Frame touch:TouchEffect.NormalBackgroundColor="White"
touch:TouchEffect.HoveredBackgroundColor="LightGray"
touch:TouchEffect.PressedBackgroundColor="Gray">
<Label Text="Color Feedback" />
</Frame>
```
#### Opacity Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NormalOpacity` | `double` | `1.0` | Opacity in normal state. |
| `HoveredOpacity` | `double` | `1.0` | Opacity when hovered. |
| `PressedOpacity` | `double` | `1.0` | Opacity when pressed. |
**Usage:**
```xml
<Frame touch:TouchEffect.NormalOpacity="1.0"
touch:TouchEffect.HoveredOpacity="0.9"
touch:TouchEffect.PressedOpacity="0.7">
<Label Text="Opacity Feedback" />
</Frame>
```
#### Scale Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NormalScale` | `double` | `1.0` | Scale in normal state. |
| `HoveredScale` | `double` | `1.0` | Scale when hovered. |
| `PressedScale` | `double` | `1.0` | Scale when pressed. |
**Usage:**
```xml
<Frame touch:TouchEffect.NormalScale="1.0"
touch:TouchEffect.HoveredScale="1.02"
touch:TouchEffect.PressedScale="0.95">
<Label Text="Scale Feedback" />
</Frame>
```
#### Translation Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NormalTranslationX` | `double` | `0.0` | X translation in normal state. |
| `HoveredTranslationX` | `double` | `0.0` | X translation when hovered. |
| `PressedTranslationX` | `double` | `0.0` | X translation when pressed. |
| `NormalTranslationY` | `double` | `0.0` | Y translation in normal state. |
| `HoveredTranslationY` | `double` | `0.0` | Y translation when hovered. |
| `PressedTranslationY` | `double` | `0.0` | Y translation when pressed. |
**Usage:**
```xml
<Frame touch:TouchEffect.PressedTranslationY="2">
<Label Text="Moves down when pressed" />
</Frame>
```
#### Rotation Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NormalRotation` | `double` | `0.0` | Z-axis rotation in normal state (degrees). |
| `HoveredRotation` | `double` | `0.0` | Z-axis rotation when hovered. |
| `PressedRotation` | `double` | `0.0` | Z-axis rotation when pressed. |
| `NormalRotationX` | `double` | `0.0` | X-axis rotation in normal state. |
| `HoveredRotationX` | `double` | `0.0` | X-axis rotation when hovered. |
| `PressedRotationX` | `double` | `0.0` | X-axis rotation when pressed. |
| `NormalRotationY` | `double` | `0.0` | Y-axis rotation in normal state. |
| `HoveredRotationY` | `double` | `0.0` | Y-axis rotation when hovered. |
| `PressedRotationY` | `double` | `0.0` | Y-axis rotation when pressed. |
**Usage:**
```xml
<Frame touch:TouchEffect.PressedRotation="5">
<Label Text="Tilts when pressed" />
</Frame>
```
#### Animation Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `AnimationDuration` | `int` | `0` | Default animation duration in milliseconds. |
| `AnimationEasing` | `Easing` | `null` | Default easing function for animations. |
| `PressedAnimationDuration` | `int` | `0` | Animation duration for pressed state transitions. |
| `PressedAnimationEasing` | `Easing` | `null` | Easing for pressed state transitions. |
| `NormalAnimationDuration` | `int` | `0` | Animation duration for returning to normal state. |
| `NormalAnimationEasing` | `Easing` | `null` | Easing for normal state transitions. |
| `HoveredAnimationDuration` | `int` | `0` | Animation duration for hover state transitions. |
| `HoveredAnimationEasing` | `Easing` | `null` | Easing for hover state transitions. |
| `PulseCount` | `int` | `0` | Number of pulse repetitions. Use `-1` for infinite. |
**Usage:**
```xml
<Frame touch:TouchEffect.AnimationDuration="150"
touch:TouchEffect.AnimationEasing="{x:Static Easing.CubicOut}"
touch:TouchEffect.PressedScale="0.95">
<Label Text="Animated Feedback" />
</Frame>
```
#### Toggle Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `IsToggled` | `bool?` | `null` | Toggle state. `null` disables toggle behavior. Supports two-way binding. |
| `DisallowTouchThreshold` | `int` | `0` | Movement threshold in pixels before touch is canceled. |
**Usage:**
```xml
<Frame touch:TouchEffect.IsToggled="{Binding IsSelected, Mode=TwoWay}"
touch:TouchEffect.PressedBackgroundColor="Blue"
touch:TouchEffect.NormalBackgroundColor="Gray">
<Label Text="Toggle Button" />
</Frame>
```
#### Native Animation Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NativeAnimation` | `bool` | `false` | Enables platform-specific animations (Android ripple, iOS highlight). |
| `NativeAnimationColor` | `Color` | `Default` | Color for native animation effects. |
| `NativeAnimationRadius` | `int` | `-1` | Radius for native ripple effect. `-1` uses default. |
| `NativeAnimationShadowRadius` | `int` | `-1` | Shadow radius for native effects. |
| `NativeAnimationBorderless` | `bool` | `false` | When `true`, ripple extends beyond view bounds. |
**Usage:**
```xml
<Frame touch:TouchEffect.NativeAnimation="True"
touch:TouchEffect.NativeAnimationColor="Blue"
touch:TouchEffect.NativeAnimationRadius="100">
<Label Text="Native Ripple Effect" />
</Frame>
```
#### Background Image Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `NormalBackgroundImageSource` | `ImageSource` | `null` | Background image in normal state. |
| `HoveredBackgroundImageSource` | `ImageSource` | `null` | Background image when hovered. |
| `PressedBackgroundImageSource` | `ImageSource` | `null` | Background image when pressed. |
| `BackgroundImageAspect` | `Aspect` | `AspectFill` | Default aspect ratio for background images. |
| `NormalBackgroundImageAspect` | `Aspect` | `AspectFill` | Aspect ratio for normal state image. |
| `HoveredBackgroundImageAspect` | `Aspect` | `AspectFill` | Aspect ratio for hovered state image. |
| `PressedBackgroundImageAspect` | `Aspect` | `AspectFill` | Aspect ratio for pressed state image. |
| `ShouldSetImageOnAnimationEnd` | `bool` | `false` | When `true`, image changes occur after animation completes. |
### Events
| Event | EventArgs | Description |
|-------|-----------|-------------|
| `StatusChanged` | `TouchStatusChangedEventArgs` | Fired when touch status changes. |
| `StateChanged` | `TouchStateChangedEventArgs` | Fired when touch state changes. |
| `InteractionStatusChanged` | `TouchInteractionStatusChangedEventArgs` | Fired when interaction status changes. |
| `HoverStatusChanged` | `HoverStatusChangedEventArgs` | Fired when hover status changes. |
| `HoverStateChanged` | `HoverStateChangedEventArgs` | Fired when hover state changes. |
| `Completed` | `TouchCompletedEventArgs` | Fired when touch completes successfully. |
| `LongPressCompleted` | `LongPressCompletedEventArgs` | Fired when long press completes. |
**Usage in Code-Behind:**
```csharp
public partial class MyPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
// Get the effect instance
var effect = MyView.Effects.OfType<TouchEffect>().FirstOrDefault();
if (effect != null)
{
effect.Completed += OnTouchCompleted;
effect.StateChanged += OnStateChanged;
}
}
private void OnTouchCompleted(object sender, TouchCompletedEventArgs e)
{
Debug.WriteLine($"Touch completed with parameter: {e.Parameter}");
}
private void OnStateChanged(object sender, TouchStateChangedEventArgs e)
{
Debug.WriteLine($"State changed to: {e.State}");
}
}
```
### Static Methods
#### SetLogger
```csharp
public static void SetLogger(ITouchEffectLogger? logger)
```
Sets the logger instance for all TouchEffect operations.
**Parameters:**
- `logger`: The logger implementation. Pass `null` to disable logging.
**Usage:**
```csharp
// Enable default console logging
TouchEffect.SetLogger(new DefaultTouchEffectLogger());
// Disable logging
TouchEffect.SetLogger(null);
// Custom logging
TouchEffect.SetLogger(new MyCustomLogger());
```
---
## TouchBehavior Class
**NEW in v2.0.0**
A Behavior-based alternative to `TouchEffect` for attaching touch feedback to elements. MAUI is moving toward Behaviors over Effects, making this the preferred modern API.
**Namespace:** `MarketAlly.TouchEffect.Maui`
**Inheritance:** `Behavior<VisualElement>`
### Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Command` | `ICommand` | `null` | Command executed on tap. |
| `CommandParameter` | `object` | `null` | Parameter for the command. |
| `LongPressCommand` | `ICommand` | `null` | Command for long press. |
| `LongPressCommandParameter` | `object` | `null` | Parameter for long press command. |
| `LongPressDuration` | `int` | `500` | Long press duration in ms. |
| `PressedScale` | `double` | `1.0` | Scale when pressed. |
| `PressedOpacity` | `double` | `1.0` | Opacity when pressed. |
| `PressedBackgroundColor` | `Color` | `null` | Background color when pressed. |
| `HoveredScale` | `double` | `1.0` | Scale when hovered. |
| `HoveredOpacity` | `double` | `1.0` | Opacity when hovered. |
| `HoveredBackgroundColor` | `Color` | `null` | Background color when hovered. |
| `NormalScale` | `double` | `1.0` | Scale in normal state. |
| `NormalOpacity` | `double` | `1.0` | Opacity in normal state. |
| `NormalBackgroundColor` | `Color` | `null` | Background color in normal state. |
| `AnimationDuration` | `int` | `100` | Animation duration in ms. |
| `AnimationEasing` | `Easing` | `null` | Animation easing function. |
| `NativeAnimation` | `bool` | `false` | Enable native platform animations. |
| `NativeAnimationColor` | `Color` | `null` | Color for native animations. |
| `IsToggled` | `bool?` | `null` | Toggle state (two-way bindable). |
| `IsAvailable` | `bool` | `true` | Enable/disable the behavior. |
### Usage
**XAML:**
```xml
<Button Text="Click Me">
<Button.Behaviors>
<touch:TouchBehavior
PressedScale="0.95"
PressedOpacity="0.8"
AnimationDuration="100"
AnimationEasing="{x:Static Easing.CubicOut}"
Command="{Binding TapCommand}" />
</Button.Behaviors>
</Button>
```
**C#:**
```csharp
var button = new Button { Text = "Click Me" };
button.Behaviors.Add(new TouchBehavior
{
PressedScale = 0.95,
PressedOpacity = 0.8,
AnimationDuration = 100,
Command = viewModel.TapCommand
});
```
---
## TouchEffectBuilder Class
Fluent builder for configuring TouchEffect with a clean, chainable API.
**Namespace:** `MarketAlly.TouchEffect.Maui`
### Static Methods
#### For
```csharp
public static TouchEffectBuilder For(VisualElement element)
```
Creates a builder for the specified element.
### Instance Methods
#### Command Configuration
| Method | Description |
|--------|-------------|
| `WithCommand(ICommand command, object? parameter = null)` | Sets the tap command. |
| `WithLongPressCommand(ICommand command, object? parameter = null)` | Sets the long press command. |
| `WithLongPressDuration(int milliseconds)` | Sets long press duration. |
#### Visual Configuration
| Method | Description |
|--------|-------------|
| `WithPressedState(double? opacity, double? scale, Color? backgroundColor)` | Configures pressed state. |
| `WithHoveredState(double? opacity, double? scale, Color? backgroundColor)` | Configures hovered state. |
| `WithNormalState(double? opacity, double? scale, Color? backgroundColor)` | Configures normal state. |
| `WithPressedOpacity(double opacity)` | Sets pressed opacity. |
| `WithPressedScale(double scale)` | Sets pressed scale. |
| `WithPressedBackgroundColor(Color color)` | Sets pressed background color. |
| `WithHoveredScale(double scale)` | Sets hovered scale. |
#### Animation Configuration
| Method | Description |
|--------|-------------|
| `WithAnimation(int duration, Easing? easing = null)` | Sets animation duration and easing. |
| `WithPressedAnimation(int duration, Easing? easing = null)` | Sets pressed state animation. |
| `WithHoveredAnimation(int duration, Easing? easing = null)` | Sets hovered state animation. |
| `WithPulse(int count)` | Sets pulse count. |
| `WithInfinitePulse()` | Enables infinite pulsing. |
#### Native Animation Configuration
| Method | Description |
|--------|-------------|
| `WithNativeAnimation(Color? color = null, int radius = -1)` | Enables native platform animations. |
#### Toggle Configuration
| Method | Description |
|--------|-------------|
| `AsToggle(bool initialState = false)` | Enables toggle behavior. |
#### Other Configuration
| Method | Description |
|--------|-------------|
| `WithDisallowThreshold(int pixels)` | Sets movement cancellation threshold. |
| `Disable()` | Disables the effect. |
#### Preset Methods
| Method | Description |
|--------|-------------|
| `AsButton()` | Applies standard button preset. |
| `AsCard()` | Applies card preset with scale effect. |
| `AsListItem()` | Applies list item preset with background color. |
| `AsFloatingActionButton()` | Applies FAB preset with native animation. |
#### Build Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `Build()` | `VisualElement` | Applies configuration and returns the element. |
| `Apply()` | `TouchEffectBuilder` | Applies configuration and returns builder for chaining. |
### Usage
```csharp
// Basic usage
var button = TouchEffectBuilder.For(myFrame)
.WithPressedScale(0.95)
.WithPressedOpacity(0.8)
.WithAnimation(100, Easing.CubicOut)
.WithCommand(tapCommand)
.Build();
// Using presets
var card = TouchEffectBuilder.For(myCard)
.AsCard()
.WithCommand(selectCommand)
.Build();
// Complex configuration
var interactive = TouchEffectBuilder.For(element)
.WithPressedState(opacity: 0.7, scale: 0.95, backgroundColor: Colors.LightGray)
.WithHoveredState(opacity: 1.0, scale: 1.02, backgroundColor: Colors.White)
.WithAnimation(150, Easing.CubicInOut)
.WithLongPressCommand(contextMenuCommand)
.WithLongPressDuration(800)
.AsToggle()
.Build();
```
### Extension Methods
```csharp
// Create a builder for any VisualElement
public static TouchEffectBuilder ConfigureTouchEffect(this VisualElement element)
// Quick presets
public static VisualElement WithButtonEffect(this VisualElement element, ICommand? command = null)
public static VisualElement WithCardEffect(this VisualElement element, ICommand? command = null)
```
---
## TouchEffectPresets Class
Predefined TouchEffect configurations for common UI patterns.
**Namespace:** `MarketAlly.TouchEffect.Maui`
### Button Presets
| Method | Description |
|--------|-------------|
| `Button.Apply(element)` | Standard button with opacity feedback (0.7 pressed). |
| `Button.ApplyPrimary(element)` | Primary button with scale (0.95) and opacity (0.8). |
| `Button.ApplySecondary(element)` | Secondary button with subtle opacity (0.6). |
| `Button.ApplyText(element)` | Text button with minimal feedback (0.5 opacity, instant). |
### Card Presets
| Method | Description |
|--------|-------------|
| `Card.Apply(element)` | Standard card with subtle scale (0.97). |
| `Card.ApplyElevated(element)` | Elevated card with scale (0.95), opacity (0.9), hover scale (1.02). |
| `Card.ApplyInteractive(element)` | Interactive card with hover background highlight. |
### ListItem Presets
| Method | Description |
|--------|-------------|
| `ListItem.Apply(element)` | Standard list item with background highlight. |
| `ListItem.ApplySelectable(element)` | Selectable item with toggle behavior. |
| `ListItem.ApplySwipeable(element)` | Swipeable item with scale feedback. |
### IconButton Presets
| Method | Description |
|--------|-------------|
| `IconButton.Apply(element)` | Standard icon button with scale (0.85), spring animation. |
| `IconButton.ApplyFloatingAction(element)` | FAB with scale (0.9), native animation. |
| `IconButton.ApplyToolbar(element)` | Toolbar icon with subtle opacity (0.5). |
### Toggle Presets
| Method | Description |
|--------|-------------|
| `Toggle.Apply(element)` | Standard toggle with scale effect. |
| `Toggle.ApplyCheckbox(element)` | Checkbox-style with bounce animation. |
### Image Presets
| Method | Description |
|--------|-------------|
| `Image.ApplyThumbnail(element)` | Thumbnail with scale (0.95 pressed, 1.05 hover). |
| `Image.ApplyGallery(element)` | Gallery image with zoom effect (1.1 hover). |
| `Image.ApplyAvatar(element)` | Avatar with subtle feedback. |
### Native Presets
| Method | Description |
|--------|-------------|
| `Native.ApplyRipple(element, color?)` | Android ripple effect. |
| `Native.ApplyHaptic(element)` | iOS-style haptic feedback. |
### Special Presets
| Method | Description |
|--------|-------------|
| `Special.ApplyPulse(element, count)` | Pulse effect with repeating animation. |
| `Special.ApplyBounce(element)` | Bounce effect with spring animation. |
| `Special.ApplyShake(element)` | Shake effect with rotation. |
| `Special.ApplyDisabled(element)` | Disabled state with no interaction. |
### Extension Methods
```csharp
element.WithButtonPreset();
element.WithCardPreset();
element.WithListItemPreset();
element.WithIconButtonPreset();
element.WithNativeEffect(color?);
```
---
## Enumerations
### TouchStatus
**Namespace:** `MarketAlly.TouchEffect.Maui.Enums`
| Value | Description |
|-------|-------------|
| `Started` | Touch has started. |
| `Completed` | Touch completed successfully. |
| `Canceled` | Touch was canceled (moved outside, interrupted). |
### TouchState
**Namespace:** `MarketAlly.TouchEffect.Maui.Enums`
| Value | Description |
|-------|-------------|
| `Normal` | Element is in normal state. |
| `Pressed` | Element is being pressed. |
### TouchInteractionStatus
**Namespace:** `MarketAlly.TouchEffect.Maui.Enums`
| Value | Description |
|-------|-------------|
| `Started` | User interaction started. |
| `Completed` | User interaction completed. |
### HoverStatus
**Namespace:** `MarketAlly.TouchEffect.Maui.Enums`
| Value | Description |
|-------|-------------|
| `Entered` | Pointer entered the element. |
| `Exited` | Pointer exited the element. |
### HoverState
**Namespace:** `MarketAlly.TouchEffect.Maui.Enums`
| Value | Description |
|-------|-------------|
| `Normal` | Element is not being hovered. |
| `Hovered` | Element is being hovered. |
---
## Interfaces
### ITouchEffectLogger
**Namespace:** `MarketAlly.TouchEffect.Maui.Interfaces`
Interface for custom logging implementations.
```csharp
public interface ITouchEffectLogger
{
void LogError(Exception ex, string context, string? additionalInfo = null);
void LogWarning(string message, string context);
void LogInfo(string message, string context);
}
```
### Built-in Implementations
#### DefaultTouchEffectLogger
Logs to `Debug.WriteLine` with formatted output.
```csharp
TouchEffect.SetLogger(new DefaultTouchEffectLogger());
```
#### NullTouchEffectLogger
No-op logger that discards all messages. Used by default.
```csharp
// Singleton instance
NullTouchEffectLogger.Instance
```
---
## Constants
### TouchEffectConstants
**Namespace:** `MarketAlly.TouchEffect.Maui`
#### Defaults
| Constant | Value | Description |
|----------|-------|-------------|
| `LongPressDuration` | `500` | Default long press duration (ms). |
| `Opacity` | `1.0` | Default opacity. |
| `Scale` | `1.0` | Default scale. |
| `TranslationX` | `0.0` | Default X translation. |
| `TranslationY` | `0.0` | Default Y translation. |
| `Rotation` | `0.0` | Default rotation. |
| `PulseCount` | `0` | Default pulse count. |
| `DisallowTouchThreshold` | `0` | Default movement threshold. |
| `NativeAnimationRadius` | `-1` | Default native animation radius. |
| `NativeAnimationShadowRadius` | `-1` | Default shadow radius. |
#### Animation
| Constant | Value | Description |
|----------|-------|-------------|
| `DefaultDuration` | `0` | Default animation duration. |
| `DefaultProgressDelay` | `10` | Delay between animation frames. |
| `TargetFrameRate` | `60` | Target animation frame rate. |
#### PresetDurations
| Constant | Value | Description |
|----------|-------|-------------|
| `Instant` | `0` | No animation. |
| `VeryFast` | `50` | Very fast animations (ms). |
| `Fast` | `100` | Fast animations (ms). |
| `Normal` | `200` | Normal animations (ms). |
| `Slow` | `300` | Slow animations (ms). |
#### VisualStates
| Constant | Value |
|----------|-------|
| `Unpressed` | `"Unpressed"` |
| `Pressed` | `"Pressed"` |
| `Hovered` | `"Hovered"` |
#### Platform.Android
| Constant | Value | Description |
|----------|-------|-------------|
| `DefaultRippleColor` | `128` | Default ripple RGB value. |
| `DefaultRippleAlpha` | `80` | Default ripple alpha. |
| `MinRippleRadius` | `48` | Minimum ripple radius (dp). |
#### Platform.iOS
| Constant | Value | Description |
|----------|-------|-------------|
| `HighlightAlpha` | `0.5f` | Default highlight alpha. |
---
## Event Args Classes
### TouchStatusChangedEventArgs
```csharp
public class TouchStatusChangedEventArgs : EventArgs
{
public TouchStatus Status { get; }
}
```
### TouchStateChangedEventArgs
```csharp
public class TouchStateChangedEventArgs : EventArgs
{
public TouchState State { get; }
}
```
### TouchInteractionStatusChangedEventArgs
```csharp
public class TouchInteractionStatusChangedEventArgs : EventArgs
{
public TouchInteractionStatus InteractionStatus { get; }
}
```
### HoverStatusChangedEventArgs
```csharp
public class HoverStatusChangedEventArgs : EventArgs
{
public HoverStatus Status { get; }
}
```
### HoverStateChangedEventArgs
```csharp
public class HoverStateChangedEventArgs : EventArgs
{
public HoverState State { get; }
}
```
### TouchCompletedEventArgs
```csharp
public class TouchCompletedEventArgs : EventArgs
{
public object? Parameter { get; }
}
```
### LongPressCompletedEventArgs
```csharp
public class LongPressCompletedEventArgs : EventArgs
{
public object? Parameter { get; }
}
```
---
## Migration Guide
### From v1.x to v2.0
1. **Update package reference** to version 2.0.0
2. **Update target framework** to .NET 10 if needed
3. **Optional: Use TouchBehavior** instead of attached properties for new code
4. **Optional: Configure logging** using `TouchEffect.SetLogger()`
No breaking changes - all v1.x code continues to work.
### From Xamarin TouchEffect
1. Update namespace from `Xamarin.CommunityToolkit.Effects` to `MarketAlly.TouchEffect.Maui`
2. Replace `TouchEff` with `TouchEffect` in XAML
3. Update `UseMauiApp<App>()` to include `.UseMauiTouchEffect()`
---
*Last updated: January 2025 | Version 2.0.0*

238
README.md
View File

@ -3,26 +3,38 @@
[![NuGet](https://img.shields.io/nuget/v/MarketAlly.TouchEffect.Maui.svg)](https://www.nuget.org/packages/MarketAlly.TouchEffect.Maui)
[![NuGet Downloads](https://img.shields.io/nuget/dt/MarketAlly.TouchEffect.Maui.svg)](https://www.nuget.org/packages/MarketAlly.TouchEffect.Maui)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![.NET 10](https://img.shields.io/badge/.NET-10.0-blue.svg)](https://dotnet.microsoft.com/)
A comprehensive touch effect library for .NET MAUI applications by **MarketAlly**, providing rich interaction feedback and animations across all platforms. MarketAlly.TouchEffect.Maui brings advanced touch handling, hover states, long press detection, and smooth animations to any MAUI view.
## What's New in v2.0.0
- **.NET 10 Support** - Updated to target .NET 10 with latest MAUI
- **TouchBehavior** - New Behavior-based API as modern alternative to Effects
- **Thread-Safe Architecture** - Complete thread-safety overhaul with proper synchronization
- **Enhanced Logging** - Integrated logging throughout with `ITouchEffectLogger` interface
- **Improved Code Organization** - TouchEffect split into partial classes for maintainability
- **Bug Fixes** - Fixed `ForceUpdateStateWithoutAnimation` to actually disable animations
- **Performance** - Replaced LINQ with for-loops in hot paths to reduce allocations
## Features
### 🎯 Core Capabilities
### Core Capabilities
- **Universal Touch Feedback** - Consistent touch interactions across iOS, Android, and Windows
- **50+ Customizable Properties** - Fine-grained control over every aspect of the touch experience
- **Hardware-Accelerated Animations** - Smooth, performant transitions using platform-native acceleration
- **Accessibility First** - Full keyboard, screen reader, and assistive technology support
- **Memory Efficient** - WeakEventManager pattern prevents memory leaks
- **Thread-Safe** - Proper synchronization for animation state management
### 🎨 Visual Effects
### Visual Effects
- **Opacity Animations** - Fade effects on touch with customizable values
- **Scale Transformations** - Grow or shrink elements during interaction
- **Color Transitions** - Dynamic background color changes for different states
- **Translation & Rotation** - Move and rotate elements during touch
- **Native Platform Effects** - Android ripple effects and iOS haptic feedback
### 🔧 Advanced Features
### Advanced Features
- **Long Press Detection** - Configurable duration with separate command binding
- **Hover Support** - Mouse and stylus hover states on supported platforms
- **Toggle Behavior** - Switch-like functionality with persistent state
@ -36,24 +48,24 @@ A comprehensive touch effect library for .NET MAUI applications by **MarketAlly*
| iOS | 13.0+ | Full support with haptic feedback |
| Android | API 24+ | Full support with native ripple effects |
| Windows | 10.0.17763+ | Full support with WinUI 3 animations |
| Mac Catalyst| | Not currently supported |
| Tizen | | Not currently supported |
| Mac Catalyst| - | Not currently supported |
| Tizen | - | Not currently supported |
## Installation
### Package Manager
```bash
Install-Package MarketAlly.TouchEffect.Maui -Version 1.0.0
Install-Package MarketAlly.TouchEffect.Maui -Version 2.0.0
```
### .NET CLI
```bash
dotnet add package MarketAlly.TouchEffect.Maui --version 1.0.0
dotnet add package MarketAlly.TouchEffect.Maui --version 2.0.0
```
### PackageReference
```xml
<PackageReference Include="MarketAlly.TouchEffect.Maui" Version="1.0.0" />
<PackageReference Include="MarketAlly.TouchEffect.Maui" Version="2.0.0" />
```
## Quick Start
@ -85,7 +97,7 @@ public static class MauiProgram
### 2. Add Touch Effects to Your Views
#### XAML Approach
#### XAML Approach (Attached Properties)
```xml
<?xml version="1.0" encoding="utf-8" ?>
@ -124,7 +136,28 @@ public static class MauiProgram
</ContentPage>
```
#### 🆕 Fluent Builder Approach (New!)
#### NEW: TouchBehavior Approach (v2.0.0)
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:touch="clr-namespace:MarketAlly.TouchEffect.Maui;assembly=MarketAlly.TouchEffect.Maui"
x:Class="YourApp.MainPage">
<Button Text="Click Me">
<Button.Behaviors>
<touch:TouchBehavior
PressedScale="0.95"
PressedOpacity="0.8"
AnimationDuration="100"
Command="{Binding TapCommand}" />
</Button.Behaviors>
</Button>
</ContentPage>
```
#### Fluent Builder Approach
```csharp
using MarketAlly.TouchEffect.Maui;
@ -148,7 +181,7 @@ var listItem = new StackLayout()
.WithListItemPreset();
```
#### 🆕 Using Presets (New!)
#### Using Presets
```csharp
// Apply common UI patterns instantly
@ -163,55 +196,34 @@ myCard.WithCardPreset();
myListItem.WithListItemPreset();
```
## 🆕 New Features in v1.0.0
## Logging Configuration
### Fluent Builder Pattern
Configure touch effects with a clean, chainable API:
Configure logging to debug touch effect issues:
```csharp
element.ConfigureTouchEffect()
.WithPressedScale(0.95)
.WithPressedOpacity(0.7)
.WithAnimation(100, Easing.CubicOut)
.WithCommand(tapCommand)
.Build();
```
// Use the default console logger
TouchEffect.SetLogger(new DefaultTouchEffectLogger());
### Preset Configurations
Pre-built configurations for common UI patterns:
- **Button Presets**: Primary, Secondary, Text
- **Card Presets**: Standard, Elevated, Interactive
- **List Item Presets**: Standard, Selectable, Swipeable
- **Icon Button Presets**: Standard, FAB, Toolbar
- **Toggle Presets**: Standard, Checkbox
- **Image Presets**: Thumbnail, Gallery, Avatar
- **Native Effects**: Ripple, Haptic
- **Special Effects**: Pulse, Bounce, Shake
### Centralized Constants
All magic numbers replaced with semantic constants:
```csharp
TouchEffectConstants.Defaults.LongPressDuration // 500ms
TouchEffectConstants.Animation.TargetFrameRate // 60fps
TouchEffectConstants.Platform.Android.MinRippleRadius // 48dp
```
### Enhanced Error Handling
Comprehensive logging interface for debugging:
```csharp
// Implement custom logging
// Or implement custom logging
public class MyLogger : ITouchEffectLogger
{
public void LogError(Exception ex, string context, string? info = null)
{
// Log to your preferred service
// Log to your preferred service (App Center, Sentry, etc.)
}
public void LogWarning(string message, string context)
{
Debug.WriteLine($"[TouchEffect Warning] {context}: {message}");
}
public void LogInfo(string message, string context)
{
// Optional info logging
}
}
// Configure in your app
// Configure in your app startup
TouchEffect.SetLogger(new MyLogger());
```
@ -249,79 +261,20 @@ TouchEffect.SetLogger(new MyLogger());
touch:TouchEffect.LongPressDuration="500" />
```
## Properties Reference
## API Reference
### State Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| IsAvailable | bool | true | Enables/disables the effect |
| IsToggled | bool? | null | Toggle state (null = no toggle behavior) |
| Status | TouchStatus | Completed | Current touch status |
| State | TouchState | Normal | Current touch state |
For complete API documentation, see [API_Reference.md](API_Reference.md).
### Animation Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| AnimationDuration | int | 0 | Animation duration in milliseconds |
| AnimationEasing | Easing | null | Animation easing function |
| PulseCount | int | 0 | Number of pulse repetitions (-1 for infinite) |
### Quick Reference
### Visual Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| PressedOpacity | double | 1.0 | Opacity when pressed |
| PressedScale | double | 1.0 | Scale when pressed |
| PressedBackgroundColor | Color | Default | Background color when pressed |
| HoveredOpacity | double | 1.0 | Opacity when hovered |
| HoveredScale | double | 1.0 | Scale when hovered |
| NormalOpacity | double | 1.0 | Normal state opacity |
### Command Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| Command | ICommand | null | Command to execute on tap |
| CommandParameter | object | null | Parameter for command |
| LongPressCommand | ICommand | null | Command for long press |
| LongPressDuration | int | 500 | Long press duration in ms |
### Platform-Specific Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| NativeAnimation | bool | false | Use platform native animations |
| NativeAnimationColor | Color | Default | Native animation color |
| NativeAnimationRadius | int | -1 | Animation radius (Android/iOS) |
## Events
```csharp
public partial class MyPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
// Subscribe to events
TouchEffect.SetStatusChanged(MyView, OnTouchStatusChanged);
TouchEffect.SetStateChanged(MyView, OnTouchStateChanged);
TouchEffect.SetCompleted(MyView, OnTouchCompleted);
}
void OnTouchStatusChanged(object sender, TouchStatusChangedEventArgs e)
{
Debug.WriteLine($"Touch Status: {e.Status}");
}
void OnTouchStateChanged(object sender, TouchStateChangedEventArgs e)
{
Debug.WriteLine($"Touch State: {e.State}");
}
void OnTouchCompleted(object sender, TouchCompletedEventArgs e)
{
Debug.WriteLine("Touch completed!");
}
}
```
| Category | Key Properties |
|----------|---------------|
| **State** | `IsAvailable`, `IsToggled`, `Status`, `State` |
| **Commands** | `Command`, `CommandParameter`, `LongPressCommand`, `LongPressDuration` |
| **Visual** | `PressedOpacity`, `PressedScale`, `PressedBackgroundColor` |
| **Hover** | `HoveredOpacity`, `HoveredScale`, `HoveredBackgroundColor` |
| **Animation** | `AnimationDuration`, `AnimationEasing`, `PulseCount` |
| **Native** | `NativeAnimation`, `NativeAnimationColor`, `NativeAnimationRadius` |
## Performance Tips
@ -335,10 +288,10 @@ public partial class MyPage : ContentPage
TouchEffect.Maui is fully accessible by default:
- **Keyboard Navigation** - Full support for Tab, Enter, and Space keys
- **Screen Readers** - Compatible with VoiceOver, TalkBack, and Narrator
- **Focus Indicators** - Proper focus visualization
- **Touch Exploration** - Support for accessibility touch modes
- **Keyboard Navigation** - Full support for Tab, Enter, and Space keys
- **Screen Readers** - Compatible with VoiceOver, TalkBack, and Narrator
- **Focus Indicators** - Proper focus visualization
- **Touch Exploration** - Support for accessibility touch modes
```xml
<!-- Accessible button with semantic properties -->
@ -355,6 +308,7 @@ TouchEffect.Maui is fully accessible by default:
- Verify `IsAvailable` is true
- Check parent view `InputTransparent` settings
- Ensure view has appropriate size (not 0x0)
- Enable logging to see detailed diagnostics
### Animations Stuttering
- Reduce `AnimationDuration`
@ -377,7 +331,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
```bash
# Clone the repository
git clone https://github.com/felipebaltazar/TouchEffect.git
git clone https://github.com/MarketAlly/TouchEffect.git
# Build the project
dotnet build src/Maui.TouchEffect/TouchEffect.Maui.csproj
@ -391,15 +345,25 @@ dotnet pack src/Maui.TouchEffect/TouchEffect.Maui.csproj
## Changelog
### Version 2.0.0 (2025-01)
- **.NET 10 Support**: Updated to target .NET 10 with latest MAUI packages
- **TouchBehavior**: New `TouchBehavior` class as modern Behavior-based alternative to Effects
- **Thread-Safety**: Complete overhaul with proper `lock` synchronization in `GestureManager`
- **Logging Integration**: Full logging throughout codebase via `ITouchEffectLogger`
- **Code Organization**: `TouchEffect` split into partial classes (Core, Properties, Accessors)
- **Bug Fix**: `ForceUpdateStateWithoutAnimation` now correctly passes `animated: false`
- **Performance**: Replaced LINQ with for-loops in hot paths (`HasTouchEffect`, `GetFrom`, `PickFrom`)
- **CancellationToken Disposal**: Proper null-before-dispose pattern to prevent race conditions
### Version 1.0.0 (2024-11)
- 🆕 **Fluent Builder Pattern**: New intuitive API for configuring touch effects
- 🆕 **Preset Configurations**: 20+ pre-built configurations for common UI patterns
- 🆕 **Centralized Constants**: Eliminated magic numbers throughout codebase
- 🆕 **Logging Interface**: Comprehensive error handling and debugging support
- 🆕 **Windows Support**: Full Windows platform implementation with WinUI 3
- ✨ **Code Quality**: Partial classes, improved organization, and documentation
- 🐛 **Bug Fixes**: Fixed .NET 9 compatibility issues
- 📝 **Documentation**: Enhanced XML documentation for all public APIs
- Fluent Builder Pattern: New intuitive API for configuring touch effects
- Preset Configurations: 20+ pre-built configurations for common UI patterns
- Centralized Constants: Eliminated magic numbers throughout codebase
- Logging Interface: Comprehensive error handling and debugging support
- Windows Support: Full Windows platform implementation with WinUI 3
- Code Quality: Partial classes, improved organization, and documentation
- Bug Fixes: Fixed .NET 9 compatibility issues
- Documentation: Enhanced XML documentation for all public APIs
### Version 8.1.0
- Initial release as MarketAlly.TouchEffect.Maui
@ -412,18 +376,18 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## Acknowledgments
- Based on the original [TouchEffect](https://github.com/felipebaltazar/TouchEffect) by Andrei (MIT License)
- Based on the original [TouchEffect](https://github.com/AbuMandworking/TouchEffect) by Andrei (MIT License)
- Original [Xamarin Community Toolkit](https://github.com/xamarin/XamarinCommunityToolkit) team
- [.NET MAUI](https://github.com/dotnet/maui) team
- All [contributors](https://github.com/MarketAlly/TouchEffect/graphs/contributors)
## Support
- 🐛 [Report Issues](https://github.com/MarketAlly/TouchEffect.Maui/issues)
- Star this repository if you find it helpful!
- [Report Issues](https://github.com/MarketAlly/TouchEffect.Maui/issues)
- Star this repository if you find it helpful!
---
Made with ❤️ by **MarketAlly** for the .NET MAUI Community
Made with care by **MarketAlly** for the .NET MAUI Community
*Based on the original TouchEffect by Andrei - Used under MIT License*
*Based on the original TouchEffect by Andrei - Used under MIT License*

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks>net10.0-android;net10.0-ios</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net10.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
@ -39,25 +39,25 @@
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/>
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208"/>
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/>
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.82" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.11" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect.Enums;
namespace MarketAlly.TouchEffect.Maui.Enums;
public enum HoverState
{

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect.Enums;
namespace MarketAlly.TouchEffect.Maui.Enums;
public enum HoverStatus
{

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect.Enums;
namespace MarketAlly.TouchEffect.Maui.Enums;
public enum TouchInteractionStatus
{

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect.Enums;
namespace MarketAlly.TouchEffect.Maui.Enums;
public enum TouchState
{

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect.Enums;
namespace MarketAlly.TouchEffect.Maui.Enums;
public enum TouchStatus
{

View File

@ -1,5 +1,6 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
public class HoverStateChangedEventArgs : EventArgs
{

View File

@ -1,5 +1,6 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
public class HoverStatusChangedEventArgs : EventArgs
{

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
public class LongPressCompletedEventArgs : EventArgs
{

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
public class TouchCompletedEventArgs : EventArgs
{

View File

@ -1,5 +1,6 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
public class TouchInteractionStatusChangedEventArgs : EventArgs
{

View File

@ -1,5 +1,6 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
public class TouchStateChangedEventArgs : EventArgs
{

View File

@ -1,5 +1,6 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
public class TouchStatusChangedEventArgs : EventArgs
{

View File

@ -1,4 +1,4 @@
namespace MauiTouchEffect.Extensions;
namespace MarketAlly.TouchEffect.Maui.Extensions;
/// <summary>
/// Extension methods for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect.Extensions;
namespace MarketAlly.TouchEffect.Maui.Extensions;
/// <summary>
/// Extension methods for <see cref="VisualElement" />.
@ -51,7 +51,7 @@ public static class VisualElementExtension
}
}
internal static bool TryFindParentElementWithParentOfType<T>(this VisualElement element, out VisualElement result, out T parent) where T : VisualElement
internal static bool TryFindParentElementWithParentOfType<T>(this VisualElement? element, out VisualElement? result, out T? parent) where T : VisualElement
{
result = null;
parent = null;
@ -73,7 +73,7 @@ public static class VisualElementExtension
return false;
}
internal static bool TryFindParentOfType<T>(this VisualElement element, out T parent) where T : VisualElement
internal static bool TryFindParentOfType<T>(this VisualElement element, out T? parent) where T : VisualElement
{
return element.TryFindParentElementWithParentOfType(out _, out parent);
}

View File

@ -1,16 +1,16 @@
using Maui.TouchEffect.Enums;
using Maui.TouchEffect.Extensions;
using MarketAlly.TouchEffect.Maui.Enums;
using MarketAlly.TouchEffect.Maui.Extensions;
using static System.Math;
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
internal sealed class GestureManager
{
private const int _animationProgressDelay = 10;
private readonly object _syncLock = new();
private Color? _defaultBackgroundColor;
private CancellationTokenSource? _longPressTokenSource;
private CancellationTokenSource? _animationTokenSource;
private Func<TouchEffect, TouchState, HoverState, int, Easing, CancellationToken, Task>? _animationTaskFactory;
private Func<TouchEffect, TouchState, HoverState, int, Easing?, CancellationToken, Task>? _animationTaskFactory;
private double? _durationMultiplier;
private double _animationProgress;
private TouchState? _animationState;
@ -36,8 +36,11 @@ internal sealed class GestureManager
if (status == TouchStatus.Started)
{
_animationProgress = 0;
_animationState = state;
lock (_syncLock)
{
_animationProgress = 0;
_animationState = state;
}
}
var isToggled = sender.IsToggled;
@ -45,10 +48,13 @@ internal sealed class GestureManager
{
if (status != TouchStatus.Started)
{
_durationMultiplier = _animationState == TouchState.Pressed && !isToggled.Value ||
_animationState == TouchState.Normal && isToggled.Value
? 1 - _animationProgress
: _animationProgress;
lock (_syncLock)
{
_durationMultiplier = _animationState == TouchState.Pressed && !isToggled.Value ||
_animationState == TouchState.Normal && isToggled.Value
? 1 - _animationProgress
: _animationProgress;
}
UpdateStatusAndState(sender, status, state);
@ -117,8 +123,14 @@ internal sealed class GestureManager
var hoverState = sender.HoverState;
AbortAnimations(sender);
_animationTokenSource = new CancellationTokenSource();
var token = _animationTokenSource.Token;
CancellationTokenSource tokenSource;
lock (_syncLock)
{
_animationTokenSource = new CancellationTokenSource();
tokenSource = _animationTokenSource;
}
var token = tokenSource.Token;
var isToggled = sender.IsToggled;
@ -136,10 +148,14 @@ internal sealed class GestureManager
: TouchState.Normal;
}
var durationMultiplier = _durationMultiplier;
_durationMultiplier = null;
double? durationMultiplier;
lock (_syncLock)
{
durationMultiplier = _durationMultiplier;
_durationMultiplier = null;
}
await RunAnimationTask(sender, state, hoverState, _animationTokenSource.Token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false);
await RunAnimationTask(sender, state, hoverState, token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false);
return;
}
@ -156,7 +172,7 @@ internal sealed class GestureManager
: TouchState.Pressed;
}
await RunAnimationTask(sender, state, hoverState, _animationTokenSource.Token).ConfigureAwait(false);
await RunAnimationTask(sender, state, hoverState, token).ConfigureAwait(false);
return;
}
@ -166,7 +182,7 @@ internal sealed class GestureManager
? TouchState.Normal
: TouchState.Pressed;
await RunAnimationTask(sender, rippleState, hoverState, _animationTokenSource.Token);
await RunAnimationTask(sender, rippleState, hoverState, token);
if (token.IsCancellationRequested)
{
return;
@ -176,7 +192,7 @@ internal sealed class GestureManager
? TouchState.Pressed
: TouchState.Normal;
await RunAnimationTask(sender, rippleState, hoverState, _animationTokenSource.Token);
await RunAnimationTask(sender, rippleState, hoverState, token);
if (token.IsCancellationRequested)
{
return;
@ -188,9 +204,7 @@ internal sealed class GestureManager
{
if (sender.State == TouchState.Normal)
{
_longPressTokenSource?.Cancel();
_longPressTokenSource?.Dispose();
_longPressTokenSource = null;
CancelAndDisposeLongPressToken();
return;
}
@ -199,37 +213,66 @@ internal sealed class GestureManager
return;
}
_longPressTokenSource = new CancellationTokenSource();
_ = Task.Delay(sender.LongPressDuration, _longPressTokenSource.Token).ContinueWith(t =>
CancelAndDisposeLongPressToken();
var tokenSource = new CancellationTokenSource();
lock (_syncLock)
{
if (t.IsFaulted && t.Exception != null)
{
throw t.Exception;
}
_longPressTokenSource = tokenSource;
}
if (t.IsCanceled)
{
_ = HandleLongPressAsync(sender, tokenSource.Token);
}
private async Task HandleLongPressAsync(TouchEffect sender, CancellationToken token)
{
try
{
await Task.Delay(sender.LongPressDuration, token).ConfigureAwait(false);
if (token.IsCancellationRequested)
return;
}
var longPressAction = new Action(() =>
await MainThread.InvokeOnMainThreadAsync(() =>
{
sender.HandleUserInteraction(TouchInteractionStatus.Completed);
sender.RaiseLongPressCompleted();
});
if (MainThread.IsMainThread)
{
MainThread.BeginInvokeOnMainThread(longPressAction);
}
else
{
longPressAction.Invoke();
}
});
}).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// Expected when long press is canceled
}
catch (Exception ex)
{
TouchEffect.Logger.LogError(ex, "HandleLongPressAsync", "Error during long press handling");
}
}
internal void SetCustomAnimationTask(Func<TouchEffect, TouchState, HoverState, int, Easing, CancellationToken, Task>? animationTaskFactory)
private void CancelAndDisposeLongPressToken()
{
CancellationTokenSource? tokenSource;
lock (_syncLock)
{
tokenSource = _longPressTokenSource;
_longPressTokenSource = null;
}
if (tokenSource != null)
{
try
{
tokenSource.Cancel();
tokenSource.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed
}
}
}
internal void SetCustomAnimationTask(Func<TouchEffect, TouchState, HoverState, int, Easing?, CancellationToken, Task>? animationTaskFactory)
{
_animationTaskFactory = animationTaskFactory;
}
@ -262,7 +305,7 @@ internal sealed class GestureManager
private static void HandleCollectionViewSelection(TouchEffect sender)
{
if (!sender.Element.TryFindParentElementWithParentOfType(out var result, out CollectionView parent))
if (sender.Element == null || !sender.Element.TryFindParentElementWithParentOfType(out var result, out CollectionView? parent))
{
return;
}
@ -294,9 +337,26 @@ internal sealed class GestureManager
internal void AbortAnimations(TouchEffect sender)
{
_animationTokenSource?.Cancel();
_animationTokenSource?.Dispose();
_animationTokenSource = null;
CancellationTokenSource? tokenSource;
lock (_syncLock)
{
tokenSource = _animationTokenSource;
_animationTokenSource = null;
}
if (tokenSource != null)
{
try
{
tokenSource.Cancel();
tokenSource.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed
}
}
var element = sender.Element;
if (element == null)
{
@ -395,7 +455,7 @@ internal sealed class GestureManager
}
}
private Task SetBackgroundColor(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private Task SetBackgroundColor(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalBackgroundColor = sender.NormalBackgroundColor;
var pressedBackgroundColor = sender.PressedBackgroundColor;
@ -436,7 +496,7 @@ internal sealed class GestureManager
return element.ColorTo(color, (uint)duration, easing);
}
private static Task? SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private static Task? SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalOpacity = sender.NormalOpacity;
var pressedOpacity = sender.PressedOpacity;
@ -471,7 +531,7 @@ internal sealed class GestureManager
return element?.FadeTo(opacity, (uint)Abs(duration), easing);
}
private Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalScale = sender.NormalScale;
var pressedScale = sender.PressedScale;
@ -521,7 +581,7 @@ internal sealed class GestureManager
return animationCompletionSource.Task;
}
private static Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private static Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalTranslationX = sender.NormalTranslationX;
var pressedTranslationX = sender.PressedTranslationX;
@ -574,7 +634,7 @@ internal sealed class GestureManager
return element?.TranslateTo(translationX, translationY, (uint)Abs(duration), easing) ?? Task.FromResult(false);
}
private static Task SetRotation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private static Task SetRotation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalRotation = sender.NormalRotation;
var pressedRotation = sender.PressedRotation;
@ -609,7 +669,7 @@ internal sealed class GestureManager
return element?.RotateTo(rotation, (uint)Abs(duration), easing) ?? Task.FromResult(false);
}
private static Task SetRotationX(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private static Task SetRotationX(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalRotationX = sender.NormalRotationX;
var pressedRotationX = sender.PressedRotationX;
@ -644,7 +704,7 @@ internal sealed class GestureManager
return element?.RotateXTo(rotationX, (uint)Abs(duration), easing) ?? Task.FromResult(false);
}
private static Task SetRotationY(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
private static Task SetRotationY(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing)
{
var normalRotationY = sender.NormalRotationY;
var pressedRotationY = sender.PressedRotationY;
@ -744,29 +804,48 @@ internal sealed class GestureManager
_animationTaskFactory?.Invoke(sender, touchState, hoverState, duration, easing, token) ?? Task.FromResult(true),
SetBackgroundImageAsync(sender, touchState, hoverState, duration, token),
SetBackgroundColor(sender, touchState, hoverState, duration, easing),
SetOpacity(sender, touchState, hoverState, duration, easing),
SetOpacity(sender, touchState, hoverState, duration, easing) ?? Task.CompletedTask,
SetScale(sender, touchState, hoverState, duration, easing),
SetTranslation(sender, touchState, hoverState, duration, easing),
SetRotation(sender, touchState, hoverState, duration, easing),
SetRotationX(sender, touchState, hoverState, duration, easing),
SetRotationY(sender, touchState, hoverState, duration, easing),
Task.Run(async () =>
TrackAnimationProgressAsync(touchState, duration, token));
}
private async Task TrackAnimationProgressAsync(TouchState touchState, int duration, CancellationToken token)
{
lock (_syncLock)
{
_animationProgress = 0;
_animationState = touchState;
}
for (var progress = TouchEffectConstants.Animation.DefaultProgressDelay; progress < duration; progress += TouchEffectConstants.Animation.DefaultProgressDelay)
{
try
{
_animationProgress = 0;
_animationState = touchState;
await Task.Delay(TouchEffectConstants.Animation.DefaultProgressDelay, token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
return;
}
for (var progress = _animationProgressDelay; progress < duration; progress += _animationProgressDelay)
{
await Task.Delay(_animationProgressDelay).ConfigureAwait(false);
if (token.IsCancellationRequested)
{
return;
}
if (token.IsCancellationRequested)
{
return;
}
_animationProgress = (double)progress / duration;
}
lock (_syncLock)
{
_animationProgress = (double)progress / duration;
}
}
_animationProgress = 1;
}));
lock (_syncLock)
{
_animationProgress = 1;
}
}
}

View File

@ -1,6 +1,7 @@
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Hosting;
namespace Maui.TouchEffect.Hosting;
namespace MarketAlly.TouchEffect.Maui.Hosting;
public static class AppBuilderExtensions
{

View File

@ -1,6 +1,4 @@
using System;
namespace Maui.TouchEffect.Interfaces;
namespace MarketAlly.TouchEffect.Maui.Interfaces;
/// <summary>
/// Interface for logging TouchEffect events and errors.
@ -75,12 +73,6 @@ public class DefaultTouchEffectLogger : ITouchEffectLogger
System.Diagnostics.Debug.WriteLine(message);
System.Diagnostics.Debug.WriteLine($"Stack Trace: {exception.StackTrace}");
// In production, you might want to send this to a crash reporting service
#if !DEBUG
// Example: AppCenter, Sentry, Application Insights, etc.
// CrashReporting.TrackError(exception, new Dictionary<string, string> { { "Context", context } });
#endif
}
/// <inheritdoc/>
@ -121,12 +113,6 @@ public class DefaultTouchEffectLogger : ITouchEffectLogger
}
System.Diagnostics.Debug.WriteLine(message);
// In production, you might want to send this to analytics
#if !DEBUG
// Example: Application Insights, Google Analytics, etc.
// Analytics.TrackMetric(operationName, elapsedMilliseconds, additionalMetrics);
#endif
}
}
@ -147,4 +133,4 @@ public class NullTouchEffectLogger : ITouchEffectLogger
public void LogInformation(string message, string context) { }
public void LogDebug(string message, string context) { }
public void LogPerformance(string operationName, double elapsedMilliseconds, Dictionary<string, object>? additionalMetrics = null) { }
}
}

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
internal static class JavaObjectExtensions
{
@ -13,4 +13,4 @@ internal static class JavaObjectExtensions
public static bool IsAlive(this global::Android.Runtime.IJavaObject obj)
=> obj != null && !obj.IsDisposed();
}
}

View File

@ -7,17 +7,21 @@ using Android.Views.Accessibility;
using Android.Widget;
using Microsoft.Maui.Platform;
using System.ComponentModel;
using MarketAlly.TouchEffect.Maui.Enums;
using AView = Android.Views.View;
using Color = Android.Graphics.Color;
using Mview = Microsoft.Maui.Controls.View;
using Mcolor = Microsoft.Maui.Graphics.Color;
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffect
{
private static readonly Mcolor defaultNativeAnimationColor = new(128, 128, 128, 64);
private static readonly Mcolor defaultNativeAnimationColor = new(
TouchEffectConstants.Platform.Android.DefaultRippleColor,
TouchEffectConstants.Platform.Android.DefaultRippleColor,
TouchEffectConstants.Platform.Android.DefaultRippleColor,
TouchEffectConstants.Platform.Android.DefaultRippleAlpha);
AccessibilityManager? accessibilityManager;
AccessibilityListener? accessibilityListener;
@ -28,7 +32,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
float startX;
float startY;
Mcolor? rippleColor;
int rippleRadius = -1;
int rippleRadius = TouchEffectConstants.Defaults.NativeAnimationRadius;
AView view => Control ?? Container;
@ -139,9 +143,9 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
ripple?.Dispose();
ripple = null;
}
catch (ObjectDisposedException)
catch (ObjectDisposedException ex)
{
// Suppress exception
TouchEffect.Logger.LogWarning($"Object already disposed during OnDetached: {ex.Message}", "PlatformTouchEffect.Android");
}
isHoverSupported = false;
}
@ -166,7 +170,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
}
}
void OnTouch(object sender, AView.TouchEventArgs e)
void OnTouch(object? sender, AView.TouchEventArgs e)
{
e.Handled = false;
@ -223,7 +227,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
void OnTouchCancel()
=> HandleEnd(TouchStatus.Canceled);
void OnTouchMove(object sender, AView.TouchEventArgs e)
void OnTouchMove(object? sender, AView.TouchEventArgs e)
{
if (IsCanceled || e.Event == null)
return;
@ -273,7 +277,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
effect?.HandleHover(HoverStatus.Exited);
}
void OnClick(object sender, System.EventArgs args)
void OnClick(object? sender, System.EventArgs args)
{
if (effect?.IsDisabled ?? true)
return;
@ -385,7 +389,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
new[] { (int)nativeAnimationColor.ToPlatform() });
}
void OnLayoutChange(object sender, AView.LayoutChangeEventArgs e)
void OnLayoutChange(object? sender, AView.LayoutChangeEventArgs e)
{
if (sender is not AView layoutView || group == null || rippleView == null)
return;
@ -398,7 +402,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
AccessibilityManager.IAccessibilityStateChangeListener,
AccessibilityManager.ITouchExplorationStateChangeListener
{
PlatformTouchEffect platformTouchEffect;
PlatformTouchEffect? platformTouchEffect;
internal AccessibilityListener(PlatformTouchEffect platformTouchEffect)
=> this.platformTouchEffect = platformTouchEffect;
@ -417,4 +421,4 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
base.Dispose(disposing);
}
}
}
}

View File

@ -1,5 +1,5 @@
using Maui.TouchEffect.Enums;
using MauiTouchEffect.Extensions;
using MarketAlly.TouchEffect.Maui.Enums;
using MarketAlly.TouchEffect.Maui.Extensions;
using Microsoft.Maui.Controls.Platform;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
@ -13,7 +13,7 @@ using WinBrush = Microsoft.UI.Xaml.Media.Brush;
using WinSolidColorBrush = Microsoft.UI.Xaml.Media.SolidColorBrush;
using WinPoint = Windows.Foundation.Point;
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffect
{
@ -98,9 +98,9 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
_effect = null;
_view = null;
}
catch (Exception)
catch (Exception ex)
{
// Suppress exceptions during cleanup
TouchEffect.Logger.LogError(ex, "PlatformTouchEffect.Windows.OnDetached", "Error during cleanup");
}
}
@ -193,9 +193,9 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
{
_view.CapturePointer(e.Pointer);
}
catch
catch (Exception ex)
{
// Some controls may not support pointer capture
TouchEffect.Logger.LogWarning($"Failed to capture pointer: {ex.Message}", "PlatformTouchEffect.Windows");
}
// Handle the touch interaction
@ -224,14 +224,16 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
{
_view?.ReleasePointerCapture(e.Pointer);
}
catch
catch (Exception ex)
{
// Ignore capture release errors
TouchEffect.Logger.LogWarning($"Failed to release pointer capture: {ex.Message}", "PlatformTouchEffect.Windows");
}
// Determine if this is a completed tap or canceled
var distance = CalculateDistance(_startPoint, pointer.Position);
var threshold = _effect.DisallowTouchThreshold > 0 ? _effect.DisallowTouchThreshold : 20;
var threshold = _effect.DisallowTouchThreshold > 0
? _effect.DisallowTouchThreshold
: TouchEffectConstants.Platform.Windows.DefaultMovementThreshold;
if (distance <= threshold)
{
@ -266,7 +268,9 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
var pointer = e.GetCurrentPoint(_view);
var distance = CalculateDistance(_startPoint, pointer.Position);
var threshold = _effect.DisallowTouchThreshold > 0 ? _effect.DisallowTouchThreshold : 20;
var threshold = _effect.DisallowTouchThreshold > 0
? _effect.DisallowTouchThreshold
: TouchEffectConstants.Platform.Windows.DefaultMovementThreshold;
// Cancel if moved too far
if (distance > threshold)
@ -368,10 +372,10 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
// Determine target values based on state
double targetOpacity = _originalOpacity;
double targetScale = 1.0;
double targetTranslationX = 0;
double targetTranslationY = 0;
double targetRotation = 0;
double targetScale = TouchEffectConstants.Defaults.Scale;
double targetTranslationX = TouchEffectConstants.Defaults.TranslationX;
double targetTranslationY = TouchEffectConstants.Defaults.TranslationY;
double targetRotation = TouchEffectConstants.Defaults.Rotation;
WinBrush? targetBrush = _originalBrush;
if (_isPressed && _effect.State == TouchState.Pressed)
@ -540,7 +544,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
// Handle pulse/ripple count
if (_effect?.PulseCount != 0)
{
var pulseCount = _effect?.PulseCount ?? 0;
var pulseCount = _effect?.PulseCount ?? TouchEffectConstants.Defaults.PulseCount;
if (pulseCount < 0)
{
storyboard.RepeatBehavior = RepeatBehavior.Forever;
@ -558,7 +562,7 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
private int GetAnimationDuration()
{
if (_effect == null)
return 0;
return TouchEffectConstants.Animation.DefaultDuration;
// Determine which animation duration to use
if (_isPressed)
@ -656,35 +660,49 @@ public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffe
if (_effect == null || _effect.LongPressCommand == null)
return;
_longPressCts?.Cancel();
CancelLongPressDetection();
_longPressCts = new System.Threading.CancellationTokenSource();
try
{
var duration = _effect.LongPressDuration > 0 ? _effect.LongPressDuration : 500;
var duration = _effect.LongPressDuration > 0
? _effect.LongPressDuration
: TouchEffectConstants.Defaults.LongPressDuration;
await Task.Delay(duration, _longPressCts.Token);
if (!_longPressCts.Token.IsCancellationRequested && _isPressed)
{
_effect.RaiseLongPressCompleted();
// Optional: Provide haptic feedback if available
// Windows doesn't have built-in haptic API like mobile platforms
// Could potentially use Windows.Devices.Haptics if available
}
}
catch (TaskCanceledException)
{
// Expected when gesture is canceled
}
catch (Exception ex)
{
TouchEffect.Logger.LogError(ex, "PlatformTouchEffect.Windows.StartLongPressDetection", "Error during long press detection");
}
}
private void CancelLongPressDetection()
{
_longPressCts?.Cancel();
_longPressCts?.Dispose();
var cts = _longPressCts;
_longPressCts = null;
if (cts != null)
{
try
{
cts.Cancel();
cts.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed
}
}
}
#endregion
}
}

View File

@ -1,21 +1,21 @@
using CoreGraphics;
using Foundation;
using Maui.TouchEffect.Enums;
using MauiTouchEffect.Extensions;
using MarketAlly.TouchEffect.Maui.Enums;
using MarketAlly.TouchEffect.Maui.Extensions;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Platform;
using UIKit;
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
public partial class PlatformTouchEffect : PlatformEffect
{
private UIGestureRecognizer? touchGesture;
private UIGestureRecognizer? hoverGesture;
TouchEffect? effect;
UIView View => Container ?? Control;
protected override void OnAttached()
{
effect = TouchEffect.PickFrom(Element);
@ -37,7 +37,7 @@ public partial class PlatformTouchEffect : PlatformEffect
View.AddGestureRecognizer(touchGesture);
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
if (UIDevice.CurrentDevice.CheckSystemVersion(TouchEffectConstants.Platform.iOS.MinHoverGestureVersion, 0))
{
hoverGesture = new UIHoverGestureRecognizer(OnHover);
View.AddGestureRecognizer(hoverGesture);
@ -104,7 +104,7 @@ public partial class PlatformTouchEffect : PlatformEffect
internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
{
private TouchEffect touchEffect;
private TouchEffect? touchEffect;
private float? defaultRadius;
private float? defaultShadowRadius;
private float? defaultShadowOpacity;
@ -131,7 +131,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
IsCanceled = false;
startPoint = GetTouchPoint(touches);
HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started).SafeFireAndForget();
HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started).SafeFireAndForget(
ex => TouchEffect.Logger.LogError(ex, "TouchesBegan", "Error handling touch start"));
base.TouchesBegan(touches, evt);
}
@ -143,7 +144,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
return;
}
HandleTouch(touchEffect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
HandleTouch(touchEffect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(
ex => TouchEffect.Logger.LogError(ex, "TouchesEnded", "Error handling touch end"));
IsCanceled = true;
@ -157,7 +159,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
return;
}
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(
ex => TouchEffect.Logger.LogError(ex, "TouchesCancelled", "Error handling touch cancel"));
IsCanceled = true;
@ -180,7 +183,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
var maxDiff = Math.Max(diffX, diffY);
if (maxDiff > disallowTouchThreshold)
{
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(
ex => TouchEffect.Logger.LogError(ex, "TouchesMoved", "Error handling touch cancel from threshold"));
IsCanceled = true;
base.TouchesMoved(touches, evt);
return;
@ -193,7 +197,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
if (touchEffect?.Status != status)
{
HandleTouch(status).SafeFireAndForget();
HandleTouch(status).SafeFireAndForget(
ex => TouchEffect.Logger.LogError(ex, "TouchesMoved", "Error handling touch status change"));
}
if (status == TouchStatus.Canceled)
@ -254,7 +259,7 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
else
{
var backgroundColor = touchEffect.Element.BackgroundColor ?? Colors.Transparent;
View.Layer.BackgroundColor = (isStarted ? color : backgroundColor).ToCGColor();
}
@ -273,7 +278,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
{
if (disposing)
{
Delegate.Dispose();
Delegate?.Dispose();
touchEffect = null;
}
base.Dispose(disposing);
@ -291,7 +297,8 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
if (gestureRecognizer is TouchUITapGestureRecognizer touchGesture && otherGestureRecognizer is UIPanGestureRecognizer &&
otherGestureRecognizer.State == UIGestureRecognizerState.Began)
{
touchGesture.HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
touchGesture.HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(
ex => TouchEffect.Logger.LogError(ex, "ShouldRecognizeSimultaneously", "Error handling touch cancel"));
touchGesture.IsCanceled = true;
}
@ -308,4 +315,4 @@ internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
return recognizer.View.Subviews.Any(view => view == touch.View);
}
}
}
}

View File

@ -1,2 +1,2 @@
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/2021/maui", "Maui.TouchEffect")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/2021/maui", "Maui.TouchEffect.Enums")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/2021/maui", "MarketAlly.TouchEffect.Maui")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/2021/maui", "MarketAlly.TouchEffect.Maui.Enums")]

View File

@ -0,0 +1,312 @@
using System.Windows.Input;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
/// <summary>
/// A Behavior-based alternative to TouchEffect for attaching touch feedback to elements.
/// MAUI is moving toward Behaviors over Effects, so this provides a more modern API.
/// </summary>
/// <example>
/// XAML Usage:
/// <code>
/// &lt;Button&gt;
/// &lt;Button.Behaviors&gt;
/// &lt;touch:TouchBehavior PressedScale="0.95" Command="{Binding TapCommand}" /&gt;
/// &lt;/Button.Behaviors&gt;
/// &lt;/Button&gt;
/// </code>
/// </example>
public class TouchBehavior : Behavior<VisualElement>
{
private VisualElement? _associatedElement;
#region Bindable Properties
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(TouchBehavior));
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(TouchBehavior));
public static readonly BindableProperty LongPressCommandProperty =
BindableProperty.Create(nameof(LongPressCommand), typeof(ICommand), typeof(TouchBehavior));
public static readonly BindableProperty LongPressCommandParameterProperty =
BindableProperty.Create(nameof(LongPressCommandParameter), typeof(object), typeof(TouchBehavior));
public static readonly BindableProperty LongPressDurationProperty =
BindableProperty.Create(nameof(LongPressDuration), typeof(int), typeof(TouchBehavior), TouchEffectConstants.Defaults.LongPressDuration);
public static readonly BindableProperty PressedScaleProperty =
BindableProperty.Create(nameof(PressedScale), typeof(double), typeof(TouchBehavior), TouchEffectConstants.Defaults.Scale);
public static readonly BindableProperty PressedOpacityProperty =
BindableProperty.Create(nameof(PressedOpacity), typeof(double), typeof(TouchBehavior), TouchEffectConstants.Defaults.Opacity);
public static readonly BindableProperty PressedBackgroundColorProperty =
BindableProperty.Create(nameof(PressedBackgroundColor), typeof(Color), typeof(TouchBehavior));
public static readonly BindableProperty HoveredScaleProperty =
BindableProperty.Create(nameof(HoveredScale), typeof(double), typeof(TouchBehavior), TouchEffectConstants.Defaults.Scale);
public static readonly BindableProperty HoveredOpacityProperty =
BindableProperty.Create(nameof(HoveredOpacity), typeof(double), typeof(TouchBehavior), TouchEffectConstants.Defaults.Opacity);
public static readonly BindableProperty HoveredBackgroundColorProperty =
BindableProperty.Create(nameof(HoveredBackgroundColor), typeof(Color), typeof(TouchBehavior));
public static readonly BindableProperty NormalScaleProperty =
BindableProperty.Create(nameof(NormalScale), typeof(double), typeof(TouchBehavior), TouchEffectConstants.Defaults.Scale);
public static readonly BindableProperty NormalOpacityProperty =
BindableProperty.Create(nameof(NormalOpacity), typeof(double), typeof(TouchBehavior), TouchEffectConstants.Defaults.Opacity);
public static readonly BindableProperty NormalBackgroundColorProperty =
BindableProperty.Create(nameof(NormalBackgroundColor), typeof(Color), typeof(TouchBehavior));
public static readonly BindableProperty AnimationDurationProperty =
BindableProperty.Create(nameof(AnimationDuration), typeof(int), typeof(TouchBehavior), TouchEffectConstants.PresetDurations.Fast);
public static readonly BindableProperty AnimationEasingProperty =
BindableProperty.Create(nameof(AnimationEasing), typeof(Easing), typeof(TouchBehavior));
public static readonly BindableProperty NativeAnimationProperty =
BindableProperty.Create(nameof(NativeAnimation), typeof(bool), typeof(TouchBehavior), false);
public static readonly BindableProperty NativeAnimationColorProperty =
BindableProperty.Create(nameof(NativeAnimationColor), typeof(Color), typeof(TouchBehavior));
public static readonly BindableProperty IsToggledProperty =
BindableProperty.Create(nameof(IsToggled), typeof(bool?), typeof(TouchBehavior), null, BindingMode.TwoWay);
public static readonly BindableProperty IsAvailableProperty =
BindableProperty.Create(nameof(IsAvailable), typeof(bool), typeof(TouchBehavior), true);
#endregion
#region Properties
public ICommand? Command
{
get => (ICommand?)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public object? CommandParameter
{
get => GetValue(CommandParameterProperty);
set => SetValue(CommandParameterProperty, value);
}
public ICommand? LongPressCommand
{
get => (ICommand?)GetValue(LongPressCommandProperty);
set => SetValue(LongPressCommandProperty, value);
}
public object? LongPressCommandParameter
{
get => GetValue(LongPressCommandParameterProperty);
set => SetValue(LongPressCommandParameterProperty, value);
}
public int LongPressDuration
{
get => (int)GetValue(LongPressDurationProperty);
set => SetValue(LongPressDurationProperty, value);
}
public double PressedScale
{
get => (double)GetValue(PressedScaleProperty);
set => SetValue(PressedScaleProperty, value);
}
public double PressedOpacity
{
get => (double)GetValue(PressedOpacityProperty);
set => SetValue(PressedOpacityProperty, value);
}
public Color? PressedBackgroundColor
{
get => (Color?)GetValue(PressedBackgroundColorProperty);
set => SetValue(PressedBackgroundColorProperty, value);
}
public double HoveredScale
{
get => (double)GetValue(HoveredScaleProperty);
set => SetValue(HoveredScaleProperty, value);
}
public double HoveredOpacity
{
get => (double)GetValue(HoveredOpacityProperty);
set => SetValue(HoveredOpacityProperty, value);
}
public Color? HoveredBackgroundColor
{
get => (Color?)GetValue(HoveredBackgroundColorProperty);
set => SetValue(HoveredBackgroundColorProperty, value);
}
public double NormalScale
{
get => (double)GetValue(NormalScaleProperty);
set => SetValue(NormalScaleProperty, value);
}
public double NormalOpacity
{
get => (double)GetValue(NormalOpacityProperty);
set => SetValue(NormalOpacityProperty, value);
}
public Color? NormalBackgroundColor
{
get => (Color?)GetValue(NormalBackgroundColorProperty);
set => SetValue(NormalBackgroundColorProperty, value);
}
public int AnimationDuration
{
get => (int)GetValue(AnimationDurationProperty);
set => SetValue(AnimationDurationProperty, value);
}
public Easing? AnimationEasing
{
get => (Easing?)GetValue(AnimationEasingProperty);
set => SetValue(AnimationEasingProperty, value);
}
public bool NativeAnimation
{
get => (bool)GetValue(NativeAnimationProperty);
set => SetValue(NativeAnimationProperty, value);
}
public Color? NativeAnimationColor
{
get => (Color?)GetValue(NativeAnimationColorProperty);
set => SetValue(NativeAnimationColorProperty, value);
}
public bool? IsToggled
{
get => (bool?)GetValue(IsToggledProperty);
set => SetValue(IsToggledProperty, value);
}
public bool IsAvailable
{
get => (bool)GetValue(IsAvailableProperty);
set => SetValue(IsAvailableProperty, value);
}
#endregion
#region Behavior Lifecycle
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
_associatedElement = bindable;
ApplyTouchEffect();
}
protected override void OnDetachingFrom(VisualElement bindable)
{
RemoveTouchEffect();
_associatedElement = null;
base.OnDetachingFrom(bindable);
}
protected override void OnPropertyChanged(string? propertyName = null)
{
base.OnPropertyChanged(propertyName);
// Reapply effect when properties change
if (_associatedElement != null && propertyName != null)
{
ApplyTouchEffect();
}
}
#endregion
#region Touch Effect Application
private void ApplyTouchEffect()
{
if (_associatedElement == null)
return;
// Apply all properties via the attached property system
TouchEffect.SetIsAvailable(_associatedElement, IsAvailable);
if (Command != null)
TouchEffect.SetCommand(_associatedElement, Command);
if (CommandParameter != null)
TouchEffect.SetCommandParameter(_associatedElement, CommandParameter);
if (LongPressCommand != null)
TouchEffect.SetLongPressCommand(_associatedElement, LongPressCommand);
if (LongPressCommandParameter != null)
TouchEffect.SetLongPressCommandParameter(_associatedElement, LongPressCommandParameter);
TouchEffect.SetLongPressDuration(_associatedElement, LongPressDuration);
// Visual properties
TouchEffect.SetPressedScale(_associatedElement, PressedScale);
TouchEffect.SetPressedOpacity(_associatedElement, PressedOpacity);
if (PressedBackgroundColor != null)
TouchEffect.SetPressedBackgroundColor(_associatedElement, PressedBackgroundColor);
TouchEffect.SetHoveredScale(_associatedElement, HoveredScale);
TouchEffect.SetHoveredOpacity(_associatedElement, HoveredOpacity);
if (HoveredBackgroundColor != null)
TouchEffect.SetHoveredBackgroundColor(_associatedElement, HoveredBackgroundColor);
TouchEffect.SetNormalScale(_associatedElement, NormalScale);
TouchEffect.SetNormalOpacity(_associatedElement, NormalOpacity);
if (NormalBackgroundColor != null)
TouchEffect.SetNormalBackgroundColor(_associatedElement, NormalBackgroundColor);
// Animation
TouchEffect.SetAnimationDuration(_associatedElement, AnimationDuration);
if (AnimationEasing != null)
TouchEffect.SetAnimationEasing(_associatedElement, AnimationEasing);
// Native
TouchEffect.SetNativeAnimation(_associatedElement, NativeAnimation);
if (NativeAnimationColor != null)
TouchEffect.SetNativeAnimationColor(_associatedElement, NativeAnimationColor);
// Toggle
if (IsToggled.HasValue)
TouchEffect.SetIsToggled(_associatedElement, IsToggled);
}
private void RemoveTouchEffect()
{
if (_associatedElement == null)
return;
// Remove any TouchEffect instances
var effects = _associatedElement.Effects;
for (int i = effects.Count - 1; i >= 0; i--)
{
if (effects[i] is TouchEffect)
{
effects.RemoveAt(i);
}
}
}
#endregion
}

View File

@ -0,0 +1,450 @@
using System.Windows.Input;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
/// <summary>
/// TouchEffect partial class containing all static Get/Set accessor methods.
/// </summary>
public partial class TouchEffect
{
#region State Accessors
public static bool GetIsAvailable(BindableObject? bindable)
=> (bool)(bindable?.GetValue(IsAvailableProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetIsAvailable(BindableObject? bindable, bool value)
=> bindable?.SetValue(IsAvailableProperty, value);
public static bool GetShouldMakeChildrenInputTransparent(BindableObject? bindable)
=> (bool)(bindable?.GetValue(ShouldMakeChildrenInputTransparentProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetShouldMakeChildrenInputTransparent(BindableObject? bindable, bool value)
=> bindable?.SetValue(ShouldMakeChildrenInputTransparentProperty, value);
public static TouchStatus GetStatus(BindableObject? bindable)
=> (TouchStatus)(bindable?.GetValue(StatusProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetStatus(BindableObject? bindable, TouchStatus value)
=> bindable?.SetValue(StatusProperty, value);
public static TouchState GetState(BindableObject? bindable)
=> (TouchState)(bindable?.GetValue(StateProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetState(BindableObject? bindable, TouchState value)
=> bindable?.SetValue(StateProperty, value);
public static TouchInteractionStatus GetInteractionStatus(BindableObject? bindable)
=> (TouchInteractionStatus)(bindable?.GetValue(InteractionStatusProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetInteractionStatus(BindableObject? bindable, TouchInteractionStatus value)
=> bindable?.SetValue(InteractionStatusProperty, value);
public static HoverStatus GetHoverStatus(BindableObject? bindable)
=> (HoverStatus)(bindable?.GetValue(HoverStatusProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoverStatus(BindableObject? bindable, HoverStatus value)
=> bindable?.SetValue(HoverStatusProperty, value);
public static HoverState GetHoverState(BindableObject? bindable)
=> (HoverState)(bindable?.GetValue(HoverStateProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoverState(BindableObject? bindable, HoverState value)
=> bindable?.SetValue(HoverStateProperty, value);
#endregion
#region Command Accessors
public static ICommand? GetCommand(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (ICommand?)bindable.GetValue(CommandProperty);
}
public static void SetCommand(BindableObject? bindable, ICommand value)
=> bindable?.SetValue(CommandProperty, value);
public static ICommand? GetLongPressCommand(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (ICommand?)bindable.GetValue(LongPressCommandProperty);
}
public static void SetLongPressCommand(BindableObject? bindable, ICommand value)
=> bindable?.SetValue(LongPressCommandProperty, value);
public static object? GetCommandParameter(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return bindable.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(BindableObject? bindable, object value)
=> bindable?.SetValue(CommandParameterProperty, value);
public static object? GetLongPressCommandParameter(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return bindable.GetValue(LongPressCommandParameterProperty);
}
public static void SetLongPressCommandParameter(BindableObject? bindable, object value)
=> bindable?.SetValue(LongPressCommandParameterProperty, value);
public static int GetLongPressDuration(BindableObject? bindable)
=> (int)(bindable?.GetValue(LongPressDurationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetLongPressDuration(BindableObject? bindable, int value)
=> bindable?.SetValue(LongPressDurationProperty, value);
#endregion
#region Background Color Accessors
public static Color? GetNormalBackgroundColor(BindableObject? bindable)
=> bindable?.GetValue(NormalBackgroundColorProperty) as Color;
public static void SetNormalBackgroundColor(BindableObject? bindable, Color value)
=> bindable?.SetValue(NormalBackgroundColorProperty, value);
public static Color? GetHoveredBackgroundColor(BindableObject? bindable)
=> bindable?.GetValue(HoveredBackgroundColorProperty) as Color;
public static void SetHoveredBackgroundColor(BindableObject? bindable, Color value)
=> bindable?.SetValue(HoveredBackgroundColorProperty, value);
public static Color? GetPressedBackgroundColor(BindableObject? bindable)
=> bindable?.GetValue(PressedBackgroundColorProperty) as Color;
public static void SetPressedBackgroundColor(BindableObject? bindable, Color value)
=> bindable?.SetValue(PressedBackgroundColorProperty, value);
#endregion
#region Opacity Accessors
public static double GetNormalOpacity(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalOpacityProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalOpacity(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalOpacityProperty, value);
public static double GetHoveredOpacity(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredOpacityProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredOpacity(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredOpacityProperty, value);
public static double GetPressedOpacity(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedOpacityProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedOpacity(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedOpacityProperty, value);
#endregion
#region Scale Accessors
public static double GetNormalScale(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalScaleProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalScale(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalScaleProperty, value);
public static double GetHoveredScale(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredScaleProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredScale(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredScaleProperty, value);
public static double GetPressedScale(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedScaleProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedScale(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedScaleProperty, value);
#endregion
#region Translation Accessors
public static double GetNormalTranslationX(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalTranslationXProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalTranslationX(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalTranslationXProperty, value);
public static double GetHoveredTranslationX(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredTranslationXProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredTranslationX(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredTranslationXProperty, value);
public static double GetPressedTranslationX(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedTranslationXProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedTranslationX(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedTranslationXProperty, value);
public static double GetNormalTranslationY(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalTranslationYProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalTranslationY(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalTranslationYProperty, value);
public static double GetHoveredTranslationY(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredTranslationYProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredTranslationY(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredTranslationYProperty, value);
public static double GetPressedTranslationY(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedTranslationYProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedTranslationY(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedTranslationYProperty, value);
#endregion
#region Rotation Accessors
public static double GetNormalRotation(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalRotationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalRotation(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalRotationProperty, value);
public static double GetHoveredRotation(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredRotationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredRotation(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredRotationProperty, value);
public static double GetPressedRotation(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedRotationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedRotation(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedRotationProperty, value);
public static double GetNormalRotationX(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalRotationXProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalRotationX(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalRotationXProperty, value);
public static double GetHoveredRotationX(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredRotationXProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredRotationX(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredRotationXProperty, value);
public static double GetPressedRotationX(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedRotationXProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedRotationX(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedRotationXProperty, value);
public static double GetNormalRotationY(BindableObject? bindable)
=> (double)(bindable?.GetValue(NormalRotationYProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalRotationY(BindableObject? bindable, double value)
=> bindable?.SetValue(NormalRotationYProperty, value);
public static double GetHoveredRotationY(BindableObject? bindable)
=> (double)(bindable?.GetValue(HoveredRotationYProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredRotationY(BindableObject? bindable, double value)
=> bindable?.SetValue(HoveredRotationYProperty, value);
public static double GetPressedRotationY(BindableObject? bindable)
=> (double)(bindable?.GetValue(PressedRotationYProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedRotationY(BindableObject? bindable, double value)
=> bindable?.SetValue(PressedRotationYProperty, value);
#endregion
#region Animation Accessors
public static int GetAnimationDuration(BindableObject? bindable)
=> (int)(bindable?.GetValue(AnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetAnimationDuration(BindableObject? bindable, int value)
=> bindable?.SetValue(AnimationDurationProperty, value);
public static Easing? GetAnimationEasing(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (Easing?)bindable.GetValue(AnimationEasingProperty);
}
public static void SetAnimationEasing(BindableObject? bindable, Easing? value)
=> bindable?.SetValue(AnimationEasingProperty, value);
public static int GetPressedAnimationDuration(BindableObject? bindable)
=> (int)(bindable?.GetValue(PressedAnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedAnimationDuration(BindableObject? bindable, int value)
=> bindable?.SetValue(PressedAnimationDurationProperty, value);
public static Easing? GetPressedAnimationEasing(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (Easing?)bindable.GetValue(PressedAnimationEasingProperty);
}
public static void SetPressedAnimationEasing(BindableObject? bindable, Easing? value)
=> bindable?.SetValue(PressedAnimationEasingProperty, value);
public static int GetNormalAnimationDuration(BindableObject? bindable)
=> (int)(bindable?.GetValue(NormalAnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalAnimationDuration(BindableObject? bindable, int value)
=> bindable?.SetValue(NormalAnimationDurationProperty, value);
public static Easing? GetNormalAnimationEasing(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (Easing?)bindable.GetValue(NormalAnimationEasingProperty);
}
public static void SetNormalAnimationEasing(BindableObject? bindable, Easing? value)
=> bindable?.SetValue(NormalAnimationEasingProperty, value);
public static int GetHoveredAnimationDuration(BindableObject? bindable)
=> (int)(bindable?.GetValue(HoveredAnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredAnimationDuration(BindableObject? bindable, int value)
=> bindable?.SetValue(HoveredAnimationDurationProperty, value);
public static Easing? GetHoveredAnimationEasing(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (Easing?)bindable.GetValue(HoveredAnimationEasingProperty);
}
public static void SetHoveredAnimationEasing(BindableObject? bindable, Easing? value)
=> bindable?.SetValue(HoveredAnimationEasingProperty, value);
public static int GetPulseCount(BindableObject? bindable)
=> (int)(bindable?.GetValue(PulseCountProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPulseCount(BindableObject? bindable, int value)
=> bindable?.SetValue(PulseCountProperty, value);
#endregion
#region Toggle and Threshold Accessors
public static bool? GetIsToggled(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (bool?)bindable.GetValue(IsToggledProperty);
}
public static void SetIsToggled(BindableObject? bindable, bool? value)
=> bindable?.SetValue(IsToggledProperty, value);
public static int GetDisallowTouchThreshold(BindableObject? bindable)
=> (int)(bindable?.GetValue(DisallowTouchThresholdProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetDisallowTouchThreshold(BindableObject? bindable, int value)
=> bindable?.SetValue(DisallowTouchThresholdProperty, value);
#endregion
#region Native Animation Accessors
public static bool GetNativeAnimation(BindableObject? bindable)
=> (bool)(bindable?.GetValue(NativeAnimationProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNativeAnimation(BindableObject? bindable, bool value)
=> bindable?.SetValue(NativeAnimationProperty, value);
public static Color? GetNativeAnimationColor(BindableObject? bindable)
=> bindable?.GetValue(NativeAnimationColorProperty) as Color;
public static void SetNativeAnimationColor(BindableObject? bindable, Color value)
=> bindable?.SetValue(NativeAnimationColorProperty, value);
public static int GetNativeAnimationRadius(BindableObject? bindable)
=> (int)(bindable?.GetValue(NativeAnimationRadiusProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNativeAnimationRadius(BindableObject? bindable, int value)
=> bindable?.SetValue(NativeAnimationRadiusProperty, value);
public static int GetNativeAnimationShadowRadius(BindableObject? bindable)
=> (int)(bindable?.GetValue(NativeAnimationShadowRadiusProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNativeAnimationShadowRadius(BindableObject? bindable, int value)
=> bindable?.SetValue(NativeAnimationShadowRadiusProperty, value);
public static bool GetNativeAnimationBorderless(BindableObject? bindable)
=> (bool)(bindable?.GetValue(NativeAnimationBorderlessProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNativeAnimationBorderless(BindableObject? bindable, bool value)
=> bindable?.SetValue(NativeAnimationBorderlessProperty, value);
#endregion
#region Background Image Accessors
public static ImageSource? GetNormalBackgroundImageSource(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (ImageSource?)bindable.GetValue(NormalBackgroundImageSourceProperty);
}
public static void SetNormalBackgroundImageSource(BindableObject? bindable, ImageSource value)
=> bindable?.SetValue(NormalBackgroundImageSourceProperty, value);
public static ImageSource? GetHoveredBackgroundImageSource(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (ImageSource?)bindable.GetValue(HoveredBackgroundImageSourceProperty);
}
public static void SetHoveredBackgroundImageSource(BindableObject? bindable, ImageSource value)
=> bindable?.SetValue(HoveredBackgroundImageSourceProperty, value);
public static ImageSource? GetPressedBackgroundImageSource(BindableObject? bindable)
{
if (bindable == null) throw new ArgumentNullException(nameof(bindable));
return (ImageSource?)bindable.GetValue(PressedBackgroundImageSourceProperty);
}
public static void SetPressedBackgroundImageSource(BindableObject? bindable, ImageSource value)
=> bindable?.SetValue(PressedBackgroundImageSourceProperty, value);
public static Aspect GetBackgroundImageAspect(BindableObject? bindable)
=> (Aspect)(bindable?.GetValue(BackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetBackgroundImageAspect(BindableObject? bindable, Aspect value)
=> bindable?.SetValue(BackgroundImageAspectProperty, value);
public static Aspect GetNormalBackgroundImageAspect(BindableObject? bindable)
=> (Aspect)(bindable?.GetValue(NormalBackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetNormalBackgroundImageAspect(BindableObject? bindable, Aspect value)
=> bindable?.SetValue(NormalBackgroundImageAspectProperty, value);
public static Aspect GetHoveredBackgroundImageAspect(BindableObject? bindable)
=> (Aspect)(bindable?.GetValue(HoveredBackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetHoveredBackgroundImageAspect(BindableObject? bindable, Aspect value)
=> bindable?.SetValue(HoveredBackgroundImageAspectProperty, value);
public static Aspect GetPressedBackgroundImageAspect(BindableObject? bindable)
=> (Aspect)(bindable?.GetValue(PressedBackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetPressedBackgroundImageAspect(BindableObject? bindable, Aspect value)
=> bindable?.SetValue(PressedBackgroundImageAspectProperty, value);
public static bool GetShouldSetImageOnAnimationEnd(BindableObject? bindable)
=> (bool)(bindable?.GetValue(ShouldSetImageOnAnimationEndProperty) ?? throw new ArgumentNullException(nameof(bindable)));
public static void SetShouldSetImageOnAnimationEnd(BindableObject? bindable, bool value)
=> bindable?.SetValue(ShouldSetImageOnAnimationEndProperty, value);
#endregion
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
@ -26,10 +26,10 @@
<Description>Advanced touch effects for .NET MAUI applications. Features fluent builder API, 20+ preset configurations, Windows platform support, comprehensive logging, and rich interaction feedback with animations, hover states, and platform-specific effects. Enterprise-ready with zero magic numbers and extensive documentation. A MarketAlly product based on the original TouchEffect.</Description>
<Copyright>Copyright © 2025 MarketAlly. Original TouchEffect Copyright © 2018 Andrei</Copyright>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>1.0.0</AssemblyVersion>
<AssemblyFileVersion>1.0.0</AssemblyFileVersion>
<Version>1.0.0</Version>
<PackageReleaseNotes>v1.0.0: New fluent builder API, 20+ preset configurations, Windows platform support, centralized constants, enhanced error handling with logging interface, and comprehensive documentation. Fixed .NET 9 compatibility issues.</PackageReleaseNotes>
<AssemblyVersion>2.0.0</AssemblyVersion>
<AssemblyFileVersion>2.0.0</AssemblyFileVersion>
<Version>2.0.0</Version>
<PackageReleaseNotes>v2.0.0: .NET 10 support, new TouchBehavior class (Behavior-based alternative to Effects), thread-safe architecture with proper synchronization, integrated logging via ITouchEffectLogger, code organization improvements (partial classes), ForceUpdateStateWithoutAnimation bug fix, performance improvements (LINQ replaced with for-loops in hot paths), proper CancellationTokenSource disposal pattern.</PackageReleaseNotes>
<PackageIcon>icon.png</PackageIcon>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">13.0</SupportedOSPlatformVersion>
@ -39,8 +39,8 @@
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
<None Include="..\..\LICENSE" Pack="true" PackagePath="\"/>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
<None Include="icon.png">
<Pack>true</Pack>
<PackagePath>\</PackagePath>
@ -50,7 +50,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.82" />
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.11" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,475 @@
using System.Windows.Input;
using MarketAlly.TouchEffect.Maui.Enums;
namespace MarketAlly.TouchEffect.Maui;
/// <summary>
/// TouchEffect partial class containing all BindableProperty definitions.
/// </summary>
public partial class TouchEffect
{
#region State Properties
public static readonly BindableProperty IsAvailableProperty = BindableProperty.CreateAttached(
nameof(IsAvailable),
typeof(bool),
typeof(TouchEffect),
true,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty ShouldMakeChildrenInputTransparentProperty = BindableProperty.CreateAttached(
nameof(ShouldMakeChildrenInputTransparent),
typeof(bool),
typeof(TouchEffect),
true,
propertyChanged: SetChildrenInputTransparentAndTryGenerateEffect);
public static readonly BindableProperty StatusProperty = BindableProperty.CreateAttached(
nameof(Status),
typeof(TouchStatus),
typeof(TouchEffect),
TouchStatus.Completed,
BindingMode.OneWayToSource);
public static readonly BindableProperty StateProperty = BindableProperty.CreateAttached(
nameof(State),
typeof(TouchState),
typeof(TouchEffect),
TouchState.Normal,
BindingMode.OneWayToSource);
public static readonly BindableProperty InteractionStatusProperty = BindableProperty.CreateAttached(
nameof(InteractionStatus),
typeof(TouchInteractionStatus),
typeof(TouchEffect),
TouchInteractionStatus.Completed,
BindingMode.OneWayToSource);
public static readonly BindableProperty HoverStatusProperty = BindableProperty.CreateAttached(
nameof(HoverStatus),
typeof(HoverStatus),
typeof(TouchEffect),
Enums.HoverStatus.Exited,
BindingMode.OneWayToSource);
public static readonly BindableProperty HoverStateProperty = BindableProperty.CreateAttached(
nameof(HoverState),
typeof(HoverState),
typeof(TouchEffect),
Enums.HoverState.Normal,
BindingMode.OneWayToSource);
#endregion
#region Command Properties
public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
nameof(Command),
typeof(ICommand),
typeof(TouchEffect),
default(ICommand),
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty LongPressCommandProperty = BindableProperty.CreateAttached(
nameof(LongPressCommand),
typeof(ICommand),
typeof(TouchEffect),
default(ICommand),
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.CreateAttached(
nameof(CommandParameter),
typeof(object),
typeof(TouchEffect),
default,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty LongPressCommandParameterProperty = BindableProperty.CreateAttached(
nameof(LongPressCommandParameter),
typeof(object),
typeof(TouchEffect),
default,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty LongPressDurationProperty = BindableProperty.CreateAttached(
nameof(LongPressDuration),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Defaults.LongPressDuration,
propertyChanged: TryGenerateEffect);
#endregion
#region Background Color Properties
public static readonly BindableProperty NormalBackgroundColorProperty = BindableProperty.CreateAttached(
nameof(NormalBackgroundColor),
typeof(Color),
typeof(TouchEffect),
KnownColor.Default,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredBackgroundColorProperty = BindableProperty.CreateAttached(
nameof(HoveredBackgroundColor),
typeof(Color),
typeof(TouchEffect),
KnownColor.Default,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedBackgroundColorProperty = BindableProperty.CreateAttached(
nameof(PressedBackgroundColor),
typeof(Color),
typeof(TouchEffect),
KnownColor.Default,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
#endregion
#region Opacity Properties
public static readonly BindableProperty NormalOpacityProperty = BindableProperty.CreateAttached(
nameof(NormalOpacity),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Opacity,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredOpacityProperty = BindableProperty.CreateAttached(
nameof(HoveredOpacity),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Opacity,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedOpacityProperty = BindableProperty.CreateAttached(
nameof(PressedOpacity),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Opacity,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
#endregion
#region Scale Properties
public static readonly BindableProperty NormalScaleProperty = BindableProperty.CreateAttached(
nameof(NormalScale),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Scale,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredScaleProperty = BindableProperty.CreateAttached(
nameof(HoveredScale),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Scale,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedScaleProperty = BindableProperty.CreateAttached(
nameof(PressedScale),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Scale,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
#endregion
#region Translation Properties
public static readonly BindableProperty NormalTranslationXProperty = BindableProperty.CreateAttached(
nameof(NormalTranslationX),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.TranslationX,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredTranslationXProperty = BindableProperty.CreateAttached(
nameof(HoveredTranslationX),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.TranslationX,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedTranslationXProperty = BindableProperty.CreateAttached(
nameof(PressedTranslationX),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.TranslationX,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty NormalTranslationYProperty = BindableProperty.CreateAttached(
nameof(NormalTranslationY),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.TranslationY,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredTranslationYProperty = BindableProperty.CreateAttached(
nameof(HoveredTranslationY),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.TranslationY,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedTranslationYProperty = BindableProperty.CreateAttached(
nameof(PressedTranslationY),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.TranslationY,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
#endregion
#region Rotation Properties
public static readonly BindableProperty NormalRotationProperty = BindableProperty.CreateAttached(
nameof(NormalRotation),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredRotationProperty = BindableProperty.CreateAttached(
nameof(HoveredRotation),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedRotationProperty = BindableProperty.CreateAttached(
nameof(PressedRotation),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty NormalRotationXProperty = BindableProperty.CreateAttached(
nameof(NormalRotationX),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredRotationXProperty = BindableProperty.CreateAttached(
nameof(HoveredRotationX),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedRotationXProperty = BindableProperty.CreateAttached(
nameof(PressedRotationX),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty NormalRotationYProperty = BindableProperty.CreateAttached(
nameof(NormalRotationY),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredRotationYProperty = BindableProperty.CreateAttached(
nameof(HoveredRotationY),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedRotationYProperty = BindableProperty.CreateAttached(
nameof(PressedRotationY),
typeof(double),
typeof(TouchEffect),
TouchEffectConstants.Defaults.Rotation,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
#endregion
#region Animation Properties
public static readonly BindableProperty AnimationDurationProperty = BindableProperty.CreateAttached(
nameof(AnimationDuration),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Animation.DefaultDuration,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty AnimationEasingProperty = BindableProperty.CreateAttached(
nameof(AnimationEasing),
typeof(Easing),
typeof(TouchEffect),
null,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty PressedAnimationDurationProperty = BindableProperty.CreateAttached(
nameof(PressedAnimationDuration),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Animation.DefaultDuration,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty PressedAnimationEasingProperty = BindableProperty.CreateAttached(
nameof(PressedAnimationEasing),
typeof(Easing),
typeof(TouchEffect),
null,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty NormalAnimationDurationProperty = BindableProperty.CreateAttached(
nameof(NormalAnimationDuration),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Animation.DefaultDuration,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty NormalAnimationEasingProperty = BindableProperty.CreateAttached(
nameof(NormalAnimationEasing),
typeof(Easing),
typeof(TouchEffect),
null,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty HoveredAnimationDurationProperty = BindableProperty.CreateAttached(
nameof(HoveredAnimationDuration),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Animation.DefaultDuration,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty HoveredAnimationEasingProperty = BindableProperty.CreateAttached(
nameof(HoveredAnimationEasing),
typeof(Easing),
typeof(TouchEffect),
null,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty PulseCountProperty = BindableProperty.CreateAttached(
nameof(PulseCount),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Defaults.PulseCount,
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
#endregion
#region Toggle and Threshold Properties
public static readonly BindableProperty IsToggledProperty = BindableProperty.CreateAttached(
nameof(IsToggled),
typeof(bool?),
typeof(TouchEffect),
default(bool?),
BindingMode.TwoWay,
propertyChanged: ForceUpdateStateWithoutAnimationAndTryGenerateEffect);
public static readonly BindableProperty DisallowTouchThresholdProperty = BindableProperty.CreateAttached(
nameof(DisallowTouchThreshold),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Defaults.DisallowTouchThreshold,
propertyChanged: TryGenerateEffect);
#endregion
#region Native Animation Properties
public static readonly BindableProperty NativeAnimationProperty = BindableProperty.CreateAttached(
nameof(NativeAnimation),
typeof(bool),
typeof(TouchEffect),
false,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty NativeAnimationColorProperty = BindableProperty.CreateAttached(
nameof(NativeAnimationColor),
typeof(Color),
typeof(TouchEffect),
KnownColor.Default,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty NativeAnimationRadiusProperty = BindableProperty.CreateAttached(
nameof(NativeAnimationRadius),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Defaults.NativeAnimationRadius,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty NativeAnimationShadowRadiusProperty = BindableProperty.CreateAttached(
nameof(NativeAnimationShadowRadius),
typeof(int),
typeof(TouchEffect),
TouchEffectConstants.Defaults.NativeAnimationShadowRadius,
propertyChanged: TryGenerateEffect);
public static readonly BindableProperty NativeAnimationBorderlessProperty = BindableProperty.CreateAttached(
nameof(NativeAnimationBorderless),
typeof(bool),
typeof(TouchEffect),
false,
propertyChanged: TryGenerateEffect);
#endregion
#region Background Image Properties
public static readonly BindableProperty NormalBackgroundImageSourceProperty = BindableProperty.CreateAttached(
nameof(NormalBackgroundImageSource),
typeof(ImageSource),
typeof(TouchEffect),
default(ImageSource),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredBackgroundImageSourceProperty = BindableProperty.CreateAttached(
nameof(HoveredBackgroundImageSource),
typeof(ImageSource),
typeof(TouchEffect),
default(ImageSource),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedBackgroundImageSourceProperty = BindableProperty.CreateAttached(
nameof(PressedBackgroundImageSource),
typeof(ImageSource),
typeof(TouchEffect),
default(ImageSource),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty BackgroundImageAspectProperty = BindableProperty.CreateAttached(
nameof(BackgroundImageAspect),
typeof(Aspect),
typeof(TouchEffect),
default(Aspect),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty NormalBackgroundImageAspectProperty = BindableProperty.CreateAttached(
nameof(NormalBackgroundImageAspect),
typeof(Aspect),
typeof(TouchEffect),
default(Aspect),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty HoveredBackgroundImageAspectProperty = BindableProperty.CreateAttached(
nameof(HoveredBackgroundImageAspect),
typeof(Aspect),
typeof(TouchEffect),
default(Aspect),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty PressedBackgroundImageAspectProperty = BindableProperty.CreateAttached(
nameof(PressedBackgroundImageAspect),
typeof(Aspect),
typeof(TouchEffect),
default(Aspect),
propertyChanged: ForceUpdateStateAndTryGenerateEffect);
public static readonly BindableProperty ShouldSetImageOnAnimationEndProperty = BindableProperty.CreateAttached(
nameof(ShouldSetImageOnAnimationEnd),
typeof(bool),
typeof(TouchEffect),
default(bool),
propertyChanged: TryGenerateEffect);
#endregion
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,6 @@
using System.Windows.Input;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Maui.TouchEffect;
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
/// <summary>
/// Fluent builder for configuring TouchEffect with a clean API.
@ -15,21 +12,21 @@ public class TouchEffectBuilder
private object? commandParameter;
private ICommand? longPressCommand;
private object? longPressCommandParameter;
private int longPressDuration = 500; // Default long press duration
private int longPressDuration = TouchEffectConstants.Defaults.LongPressDuration;
// Visual properties
private double pressedOpacity = 1.0;
private double pressedScale = 1.0;
private double pressedOpacity = TouchEffectConstants.Defaults.Opacity;
private double pressedScale = TouchEffectConstants.Defaults.Scale;
private Color? pressedBackgroundColor;
private double hoveredOpacity = 1.0;
private double hoveredScale = 1.0;
private double hoveredOpacity = TouchEffectConstants.Defaults.Opacity;
private double hoveredScale = TouchEffectConstants.Defaults.Scale;
private Color? hoveredBackgroundColor;
private double normalOpacity = 1.0;
private double normalScale = 1.0;
private double normalOpacity = TouchEffectConstants.Defaults.Opacity;
private double normalScale = TouchEffectConstants.Defaults.Scale;
private Color? normalBackgroundColor;
// Animation properties
private int animationDuration = 150;
private int animationDuration = TouchEffectConstants.PresetDurations.Normal;
private Easing? animationEasing;
private int pressedAnimationDuration;
private Easing? pressedAnimationEasing;
@ -40,11 +37,11 @@ public class TouchEffectBuilder
// Native animation properties
private bool nativeAnimation;
private Color? nativeAnimationColor;
private int nativeAnimationRadius = -1;
private int nativeAnimationRadius = TouchEffectConstants.Defaults.NativeAnimationRadius;
// Other properties
private bool? isToggled;
private int disallowTouchThreshold = 10;
private int disallowTouchThreshold = TouchEffectConstants.Defaults.DisallowTouchThreshold;
private bool isAvailable = true;
/// <summary>
@ -277,7 +274,7 @@ public class TouchEffectBuilder
public TouchEffectBuilder AsButton()
{
return WithPressedOpacity(0.7)
.WithAnimation(100, Easing.CubicOut);
.WithAnimation(TouchEffectConstants.PresetDurations.Fast, Easing.CubicOut);
}
/// <summary>
@ -286,7 +283,7 @@ public class TouchEffectBuilder
public TouchEffectBuilder AsCard()
{
return WithPressedScale(0.97)
.WithAnimation(150, Easing.CubicInOut)
.WithAnimation(TouchEffectConstants.PresetDurations.Normal, Easing.CubicInOut)
.WithHoveredScale(1.02);
}
@ -296,7 +293,7 @@ public class TouchEffectBuilder
public TouchEffectBuilder AsListItem()
{
return WithPressedBackgroundColor(Colors.LightGray.WithAlpha(0.3f))
.WithAnimation(50);
.WithAnimation(TouchEffectConstants.PresetDurations.VeryFast);
}
/// <summary>
@ -306,7 +303,7 @@ public class TouchEffectBuilder
{
return WithPressedScale(0.9)
.WithPressedOpacity(0.8)
.WithAnimation(100, Easing.SpringOut)
.WithAnimation(TouchEffectConstants.PresetDurations.Fast, Easing.SpringOut)
.WithNativeAnimation();
}
@ -425,4 +422,4 @@ public static class TouchEffectBuilderExtensions
builder.WithCommand(command);
return builder.Build();
}
}
}

View File

@ -1,4 +1,4 @@
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
/// <summary>
/// Constants used throughout the TouchEffect library.
@ -212,4 +212,4 @@ public static class TouchEffectConstants
/// </summary>
public const int VerySlow = 500;
}
}
}

View File

@ -1,7 +1,4 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace Maui.TouchEffect;
namespace MarketAlly.TouchEffect.Maui;
/// <summary>
/// Predefined TouchEffect configurations for common UI patterns.
@ -19,7 +16,7 @@ public static class TouchEffectPresets
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
@ -30,7 +27,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
@ -40,7 +37,7 @@ public static class TouchEffectPresets
public static void ApplySecondary(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.6);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.VeryFast);
}
/// <summary>
@ -49,7 +46,7 @@ public static class TouchEffectPresets
public static void ApplyText(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.5);
TouchEffect.SetAnimationDuration(element, 25); // Instant
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Instant);
}
}
@ -64,7 +61,7 @@ public static class TouchEffectPresets
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.97);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Normal);
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
@ -75,10 +72,10 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetPressedOpacity(element, 0.9);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Normal);
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
TouchEffect.SetHoveredScale(element, 1.02);
TouchEffect.SetHoveredAnimationDuration(element, 200); // Slow
TouchEffect.SetHoveredAnimationDuration(element, TouchEffectConstants.PresetDurations.Slow);
}
/// <summary>
@ -89,7 +86,7 @@ public static class TouchEffectPresets
TouchEffect.SetPressedScale(element, 0.98);
TouchEffect.SetHoveredScale(element, 1.01);
TouchEffect.SetHoveredBackgroundColor(element, Colors.Gray.WithAlpha(0.1f));
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
}
@ -105,7 +102,7 @@ public static class TouchEffectPresets
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedBackgroundColor(element, Colors.Gray.WithAlpha(0.2f));
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.VeryFast);
}
/// <summary>
@ -115,7 +112,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetIsToggled(element, false);
TouchEffect.SetPressedBackgroundColor(element, Colors.Blue.WithAlpha(0.3f));
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
}
/// <summary>
@ -125,7 +122,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedScale(element, 0.98);
TouchEffect.SetPressedBackgroundColor(element, Colors.Gray.WithAlpha(0.1f));
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.VeryFast);
}
}
@ -141,7 +138,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedScale(element, 0.85);
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.SpringOut);
}
@ -152,7 +149,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedScale(element, 0.9);
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.SpringOut);
TouchEffect.SetNativeAnimation(element, true);
}
@ -163,7 +160,7 @@ public static class TouchEffectPresets
public static void ApplyToolbar(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.5);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.VeryFast);
}
}
@ -179,7 +176,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetIsToggled(element, false);
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Normal);
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
@ -190,7 +187,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetIsToggled(element, false);
TouchEffect.SetPressedScale(element, 0.9);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.BounceOut);
}
}
@ -207,7 +204,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetHoveredScale(element, 1.05);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Normal);
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
@ -219,7 +216,7 @@ public static class TouchEffectPresets
TouchEffect.SetPressedScale(element, 0.98);
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetHoveredScale(element, 1.1);
TouchEffect.SetAnimationDuration(element, 200); // Slow
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Slow);
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
@ -230,7 +227,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedScale(element, 0.92);
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Fast);
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
}
@ -257,7 +254,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetNativeAnimation(element, true);
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.VeryFast);
}
}
@ -274,7 +271,7 @@ public static class TouchEffectPresets
TouchEffect.SetPressedScale(element, 1.1);
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetPulseCount(element, count);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Normal);
TouchEffect.SetAnimationEasing(element, Easing.SinInOut);
}
@ -284,7 +281,7 @@ public static class TouchEffectPresets
public static void ApplyBounce(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.8);
TouchEffect.SetAnimationDuration(element, 200); // Slow
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.Slow);
TouchEffect.SetAnimationEasing(element, Easing.SpringOut);
}
@ -295,7 +292,7 @@ public static class TouchEffectPresets
{
TouchEffect.SetPressedRotation(element, 5);
TouchEffect.SetPulseCount(element, 2);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationDuration(element, TouchEffectConstants.PresetDurations.VeryFast);
TouchEffect.SetAnimationEasing(element, Easing.BounceOut);
}
@ -360,4 +357,4 @@ public static class TouchEffectPresetExtensions
TouchEffectPresets.Native.ApplyRipple(element, color);
return element;
}
}
}