This commit is contained in:
2025-10-06 16:12:02 -04:00
parent 72b4bd3df1
commit ee568d292c
73 changed files with 8245 additions and 0 deletions

375
.editorconfig Normal file
View File

@@ -0,0 +1,375 @@
root = true
# All files
[*]
indent_style = space
# Xml files
[*.xml]
indent_size = 2
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
[*.{cs,vb}]
# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:warning
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
[*.cs]
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
[*.{cs,vb}]
# Naming rules
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
# Start of NO underscore prefix on private fields
# Define the 'private_fields' symbol group:
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
# Define the 'notunderscored' naming style
dotnet_naming_style.notunderscored.capitalization = camel_case
dotnet_naming_style.notunderscored.required_prefix =
# Define the 'private_fields_notunderscored' naming rule
dotnet_naming_rule.private_fields_notunderscored.symbols = private_fields
dotnet_naming_rule.private_fields_notunderscored.style = notunderscored
dotnet_naming_rule.private_fields_notunderscored.severity = error
# End of No underscore prefix on private fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
# Symbol specifications
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case

330
.gitignore vendored Normal file
View File

@@ -0,0 +1,330 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/

40
Maui.TouchEffect.sln Normal file
View File

@@ -0,0 +1,40 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11018.127 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TouchEffect.Maui", "src\Maui.TouchEffect\TouchEffect.Maui.csproj", "{12329E90-E390-46C8-BEB3-7241A5D738A1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EEA3A91E-2966-48FF-85C4-62FF93D6A6FB}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
global.json = global.json
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maui.TouchEffect.Sample", "samples\Maui.TouchEffect.Sample\Maui.TouchEffect.Sample\Maui.TouchEffect.Sample.csproj", "{2546AAEA-754E-BCB8-E3CC-42BCC3FC45E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{12329E90-E390-46C8-BEB3-7241A5D738A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12329E90-E390-46C8-BEB3-7241A5D738A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12329E90-E390-46C8-BEB3-7241A5D738A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12329E90-E390-46C8-BEB3-7241A5D738A1}.Release|Any CPU.Build.0 = Release|Any CPU
{2546AAEA-754E-BCB8-E3CC-42BCC3FC45E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2546AAEA-754E-BCB8-E3CC-42BCC3FC45E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2546AAEA-754E-BCB8-E3CC-42BCC3FC45E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2546AAEA-754E-BCB8-E3CC-42BCC3FC45E8}.Release|Any CPU.Build.0 = Release|Any CPU
{2546AAEA-754E-BCB8-E3CC-42BCC3FC45E8}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8428853D-8814-4C16-93BD-548A167516B8}
EndGlobalSection
EndGlobal

440
README.md Normal file
View File

@@ -0,0 +1,440 @@
# MarketAlly.TouchEffect.Maui
[![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)
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.
## Features
### 🎯 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
### 🎨 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
- **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
- **Gesture Threshold** - Configurable movement tolerance before cancellation
- **Command Pattern** - MVVM-friendly with ICommand support
## Platform Support
| Platform | Version | Features |
|-------------|------------|---------------------------------------------------|
| 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 |
## Installation
### Package Manager
```bash
Install-Package MarketAlly.TouchEffect.Maui -Version 1.0.0
```
### .NET CLI
```bash
dotnet add package MarketAlly.TouchEffect.Maui --version 1.0.0
```
### PackageReference
```xml
<PackageReference Include="MarketAlly.TouchEffect.Maui" Version="1.0.0" />
```
## Quick Start
### 1. Configure Your App
In your `MauiProgram.cs`:
```csharp
using MarketAlly.TouchEffect.Maui.Hosting;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiTouchEffect() // Add this line
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
return builder.Build();
}
}
```
### 2. Add Touch Effects to Your Views
#### XAML Approach
```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">
<StackLayout Padding="20">
<!-- Simple Button Effect -->
<Frame touch:TouchEffect.PressedScale="0.95"
touch:TouchEffect.PressedOpacity="0.7"
touch:TouchEffect.AnimationDuration="100"
touch:TouchEffect.Command="{Binding TapCommand}">
<Label Text="Tap Me" HorizontalOptions="Center" />
</Frame>
<!-- Card with Hover Effect -->
<Frame touch:TouchEffect.PressedScale="0.98"
touch:TouchEffect.HoveredScale="1.02"
touch:TouchEffect.HoveredBackgroundColor="LightGray"
touch:TouchEffect.AnimationDuration="200"
touch:TouchEffect.AnimationEasing="{x:Static Easing.CubicInOut}">
<Label Text="Hover or Touch Me" />
</Frame>
<!-- Native Ripple Effect (Android) -->
<Frame touch:TouchEffect.NativeAnimation="True"
touch:TouchEffect.NativeAnimationColor="Blue"
touch:TouchEffect.Command="{Binding SelectCommand}">
<Label Text="Native Effect" />
</Frame>
</StackLayout>
</ContentPage>
```
#### 🆕 Fluent Builder Approach (New!)
```csharp
using MarketAlly.TouchEffect.Maui;
// Configure a button with fluent API
var button = new Frame { Content = new Label { Text = "Click Me" } }
.ConfigureTouchEffect()
.AsButton()
.WithCommand(viewModel.TapCommand)
.Build();
// Create a card with hover effect
var card = new Frame { Content = contentView }
.ConfigureTouchEffect()
.AsCard()
.WithCommand(viewModel.SelectCommand)
.Build();
// Apply a preset
var listItem = new StackLayout()
.WithListItemPreset();
```
#### 🆕 Using Presets (New!)
```csharp
// Apply common UI patterns instantly
TouchEffectPresets.Button.ApplyPrimary(myButton);
TouchEffectPresets.Card.ApplyElevated(myCard);
TouchEffectPresets.ListItem.Apply(myListItem);
TouchEffectPresets.IconButton.ApplyFloatingAction(myFab);
// Or use extension methods
myButton.WithButtonPreset();
myCard.WithCardPreset();
myListItem.WithListItemPreset();
```
## 🆕 New Features in v1.0.0
### Fluent Builder Pattern
Configure touch effects with a clean, chainable API:
```csharp
element.ConfigureTouchEffect()
.WithPressedScale(0.95)
.WithPressedOpacity(0.7)
.WithAnimation(100, Easing.CubicOut)
.WithCommand(tapCommand)
.Build();
```
### 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
public class MyLogger : ITouchEffectLogger
{
public void LogError(Exception ex, string context, string? info = null)
{
// Log to your preferred service
}
}
// Configure in your app
TouchEffect.SetLogger(new MyLogger());
```
## Common Use Cases
### Interactive Cards
```xml
<Frame CornerRadius="10"
touch:TouchEffect.PressedScale="0.95"
touch:TouchEffect.AnimationDuration="150"
touch:TouchEffect.Command="{Binding OpenDetailCommand}"
touch:TouchEffect.CommandParameter="{Binding .}">
<StackLayout>
<Image Source="{Binding ImageUrl}" />
<Label Text="{Binding Title}" FontAttributes="Bold" />
<Label Text="{Binding Description}" />
</StackLayout>
</Frame>
```
### Toggle Buttons
```xml
<Frame touch:TouchEffect.IsToggled="{Binding IsSelected}"
touch:TouchEffect.PressedBackgroundColor="Green"
touch:TouchEffect.NormalBackgroundColor="Gray">
<Label Text="Toggle Me" />
</Frame>
```
### Long Press Actions
```xml
<Image Source="photo.jpg"
touch:TouchEffect.Command="{Binding TapCommand}"
touch:TouchEffect.LongPressCommand="{Binding ShowMenuCommand}"
touch:TouchEffect.LongPressDuration="500" />
```
## Properties 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 |
### 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) |
### 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!");
}
}
```
## Performance Tips
1. **Keep Animations Short** - Use durations under 300ms for responsive feel
2. **Prefer Scale Over Size** - Scale transformations are GPU-accelerated
3. **Use Native Animations** - Enable platform-specific effects when possible
4. **Limit Simultaneous Effects** - Too many concurrent animations can impact performance
5. **Test on Lower-End Devices** - Ensure smooth performance across all target devices
## Accessibility
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
```xml
<!-- Accessible button with semantic properties -->
<Frame touch:TouchEffect.Command="{Binding SubmitCommand}"
SemanticProperties.Description="Submit form button"
SemanticProperties.Hint="Double tap to submit">
<Label Text="Submit" />
</Frame>
```
## Troubleshooting
### Touch Not Working
- Verify `IsAvailable` is true
- Check parent view `InputTransparent` settings
- Ensure view has appropriate size (not 0x0)
### Animations Stuttering
- Reduce `AnimationDuration`
- Disable debug mode for testing
- Check for layout cycles during animation
### Platform-Specific Issues
**iOS**: Ensure `View.UserInteractionEnabled` is true in custom renderers
**Android**: For API 21+, native ripple requires a bounded view
**Windows**: Hover only works with mouse/pen, not touch input
## Migration from Xamarin
If migrating from Xamarin.Forms TouchEffect:
1. Update namespace: `Xamarin.CommunityToolkit``MarketAlly.TouchEffect.Maui`
2. Update package reference to `MarketAlly.TouchEffect.Maui`
3. Add `.UseMauiTouchEffect()` in MauiProgram.cs
4. Properties and behavior remain the same
## Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
### Building from Source
```bash
# Clone the repository
git clone https://github.com/felipebaltazar/TouchEffect.git
# Build the project
dotnet build src/Maui.TouchEffect/TouchEffect.Maui.csproj
# Run tests
dotnet test
# Pack NuGet package
dotnet pack src/Maui.TouchEffect/TouchEffect.Maui.csproj
```
## Changelog
### 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
### Version 8.1.0
- Initial release as MarketAlly.TouchEffect.Maui
- Ported from original TouchEffect for .NET MAUI
- Full iOS and Android support
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- Based on the original [TouchEffect](https://github.com/felipebaltazar/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
- 📖 [Documentation](https://github.com/MarketAlly/TouchEffect/wiki)
- 🐛 [Report Issues](https://github.com/MarketAlly/TouchEffect/issues)
- 💬 [Discussions](https://github.com/MarketAlly/TouchEffect/discussions)
- ⭐ Star this repository if you find it helpful!
---
Made with ❤️ by **MarketAlly** for the .NET MAUI Community
*Based on the original TouchEffect by Andrei - Used under MIT License*

View File

@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Maui.TouchEffect.Sample"
x:Class="Maui.TouchEffect.Sample.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,11 @@
namespace Maui.TouchEffect.Sample;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="Maui.TouchEffect.Sample.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Maui.TouchEffect.Sample.Pages"
Shell.FlyoutBehavior="Disabled"
Title="Maui.TouchEffect.Sample">
<TabBar>
<ShellContent
ContentTemplate="{DataTemplate pages:TouchEffectPage}"
Route="TouchEffectPage"
Title="Touch Effects" />
<ShellContent
ContentTemplate="{DataTemplate pages:FluentApiPage}"
Route="FluentApiPage"
Title="Fluent API" />
</TabBar>
</Shell>

View File

@@ -0,0 +1,9 @@
namespace Maui.TouchEffect.Sample;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,77 @@
<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>
<!-- 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> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifer>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- ex. <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>Maui.TouchEffect.Sample</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Display name -->
<ApplicationTitle>Maui.TouchEffect.Sample</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.maui.toucheffect.sample</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<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"/>
<!-- Images -->
<MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208"/>
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Maui.TouchEffect\TouchEffect.Maui.csproj" />
</ItemGroup>
<!-- Build Properties must be defined within these property groups to ensure successful publishing
to the Mac App Store. See: https://aka.ms/maui-publish-app-store#define-build-properties-in-your-project-file -->
<PropertyGroup Condition="$(TargetFramework.Contains('-maccatalyst')) and '$(Configuration)' == 'Debug'">
<CodesignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodesignEntitlements>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.Contains('-maccatalyst')) and '$(Configuration)' == 'Release'">
<CodesignEntitlements>Platforms/MacCatalyst/Entitlements.Release.plist</CodesignEntitlements>
<UseHardenedRuntime>true</UseHardenedRuntime>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,31 @@
using Maui.TouchEffect.Hosting;
using Maui.TouchEffect.Sample.Pages;
using Microsoft.Extensions.Logging;
namespace Maui.TouchEffect.Sample;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiTouchEffect()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
#if DEBUG
builder.Logging.AddDebug();
#endif
builder.Services.AddTransient<TouchEffectPage>();
builder.Services.AddTransient<FluentApiPage>();
return builder.Build();
}
}

View File

@@ -0,0 +1,55 @@
namespace Maui.TouchEffect.Sample.ObjectModel;
/// <summary>
/// Factory for Xamarin.Forms.Command
/// </summary>
public static partial class CommandFactory
{
/// <summary>
/// Initializes Xamarin.Forms.Command
/// </summary>
/// <param name="execute">The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
/// <returns>Xamarin.Forms.Command</returns>
public static Command Create(Action execute) =>
new Command(execute);
/// <summary>
/// Initializes Xamarin.Forms.Command
/// </summary>
/// <param name="execute">The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
/// <returns>Xamarin.Forms.Command</returns>
public static Command Create(Action execute, Func<bool> canExecute) =>
new Command(execute, canExecute);
/// <summary>
/// Initializes Xamarin.Forms.Command
/// </summary>
/// <param name="execute">The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
/// <returns>Xamarin.Forms.Command</returns>
public static Command Create(Action<object> execute) =>
new Command(execute);
/// <summary>
/// Initializes Xamarin.Forms.Command
/// </summary>
/// <param name="execute">The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
/// <returns>Xamarin.Forms.Command</returns>
public static Command Create(Action<object> execute, Func<object, bool> canExecute) =>
new Command(execute, canExecute);
/// <summary>
/// Initializes Xamarin.Forms.Command<typeparamref name="T"/>
/// </summary>
/// <param name="execute">The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
/// <returns>Xamarin.Forms.Command<typeparamref name="T"/></returns>
public static Command<T> Create<T>(Action<T> execute) =>
new Command<T>(execute);
/// <summary>
/// Initializes Xamarin.Forms.Command<typeparamref name="T"/>
/// </summary>
/// <param name="execute">The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
/// <returns>Xamarin.Forms.Command<typeparamref name="T"/></returns>
public static Command<T> Create<T>(Action<T> execute, Func<T, bool> canExecute) =>
new Command<T>(execute, canExecute);
}

View File

@@ -0,0 +1,404 @@
using Maui.TouchEffect;
using Microsoft.Maui.Controls;
using System.Windows.Input;
namespace Maui.TouchEffect.Sample.Pages;
/// <summary>
/// Sample page demonstrating the new Fluent API and Preset features.
/// </summary>
public class FluentApiPage : ContentPage
{
private int tapCount = 0;
private Label statusLabel;
public FluentApiPage()
{
Title = "Fluent API & Presets Demo";
BackgroundColor = Colors.White;
var tapCommand = new Command(() =>
{
tapCount++;
statusLabel.Text = $"Taps: {tapCount}";
});
statusLabel = new Label
{
Text = "Taps: 0",
FontSize = 20,
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20)
};
Content = new ScrollView
{
Content = new StackLayout
{
Padding = 20,
Spacing = 20,
Children =
{
new Label
{
Text = "Fluent API & Preset Demonstrations",
FontSize = 24,
FontAttributes = FontAttributes.Bold,
HorizontalOptions = LayoutOptions.Center
},
statusLabel,
CreateSectionLabel("Fluent Builder API"),
// Button created with fluent API
CreateFluentButton(tapCommand),
// Card with hover effect
CreateFluentCard(tapCommand),
// List item with background change
CreateFluentListItem(tapCommand),
CreateSectionLabel("Preset Configurations"),
// Primary button preset
CreatePresetPrimaryButton(tapCommand),
// Elevated card preset
CreatePresetElevatedCard(tapCommand),
// Icon button preset
CreatePresetIconButton(tapCommand),
// List item preset
CreatePresetListItem(tapCommand),
CreateSectionLabel("Special Effects"),
// Pulse effect
CreatePulseEffect(tapCommand),
// Bounce effect
CreateBounceEffect(tapCommand),
// Native ripple effect
CreateNativeRippleEffect(tapCommand)
}
}
};
}
private Label CreateSectionLabel(string text)
{
return new Label
{
Text = text,
FontSize = 18,
FontAttributes = FontAttributes.Bold,
TextColor = Colors.DarkGray,
Margin = new Thickness(0, 10, 0, 5)
};
}
private View CreateFluentButton(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.Blue,
CornerRadius = 8,
Padding = 15,
Content = new Label
{
Text = "Fluent API Button",
TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center
}
};
// Configure with fluent API
frame.ConfigureTouchEffect()
.WithPressedScale(0.95)
.WithPressedOpacity(0.7)
.WithAnimation(100, Easing.CubicOut)
.WithCommand(command)
.Build();
return frame;
}
private View CreateFluentCard(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.White,
BorderColor = Colors.LightGray,
CornerRadius = 10,
Padding = 20,
HasShadow = true,
Content = new StackLayout
{
Children =
{
new Label
{
Text = "Card with Hover",
FontSize = 16,
FontAttributes = FontAttributes.Bold
},
new Label
{
Text = "Hover over me (desktop) or tap me!",
TextColor = Colors.Gray
}
}
}
};
// Configure with fluent API for card effect
frame.ConfigureTouchEffect()
.AsCard()
.WithCommand(command)
.Build();
return frame;
}
private View CreateFluentListItem(ICommand command)
{
var stack = new StackLayout
{
Orientation = StackOrientation.Horizontal,
Padding = 15,
BackgroundColor = Colors.White,
Children =
{
new BoxView
{
Color = Colors.Green,
WidthRequest = 40,
HeightRequest = 40,
CornerRadius = 20
},
new Label
{
Text = "List Item with Background Effect",
VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(10, 0)
}
}
};
// Configure with fluent API for list item
stack.ConfigureTouchEffect()
.AsListItem()
.WithCommand(command)
.Build();
return new Frame
{
Padding = 0,
CornerRadius = 5,
Content = stack
};
}
private View CreatePresetPrimaryButton(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.Purple,
CornerRadius = 8,
Padding = 15,
Content = new Label
{
Text = "Primary Button Preset",
TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center
}
};
// Apply primary button preset
TouchEffectPresets.Button.ApplyPrimary(frame);
TouchEffect.SetCommand(frame, command);
return frame;
}
private View CreatePresetElevatedCard(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.White,
BorderColor = Colors.Transparent,
CornerRadius = 12,
Padding = 20,
HasShadow = true,
Content = new StackLayout
{
Children =
{
new Label
{
Text = "Elevated Card Preset",
FontSize = 16,
FontAttributes = FontAttributes.Bold
},
new Label
{
Text = "With scale and hover effects",
TextColor = Colors.Gray
}
}
}
};
// Apply elevated card preset
TouchEffectPresets.Card.ApplyElevated(frame);
TouchEffect.SetCommand(frame, command);
return frame;
}
private View CreatePresetIconButton(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.Orange,
CornerRadius = 30,
WidthRequest = 60,
HeightRequest = 60,
HorizontalOptions = LayoutOptions.Center,
Padding = 0,
Content = new Label
{
Text = "+",
FontSize = 30,
TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center
}
};
// Apply FAB preset
TouchEffectPresets.IconButton.ApplyFloatingAction(frame);
TouchEffect.SetCommand(frame, command);
return frame;
}
private View CreatePresetListItem(ICommand command)
{
var stack = new StackLayout
{
Orientation = StackOrientation.Horizontal,
Padding = 15,
BackgroundColor = Colors.White,
Children =
{
new Image
{
Source = "dotnet_bot.svg",
WidthRequest = 40,
HeightRequest = 40
},
new StackLayout
{
Margin = new Thickness(10, 0),
VerticalOptions = LayoutOptions.Center,
Children =
{
new Label
{
Text = "List Item Preset",
FontAttributes = FontAttributes.Bold
},
new Label
{
Text = "With subtitle",
FontSize = 12,
TextColor = Colors.Gray
}
}
}
}
};
// Apply list item preset
TouchEffectPresets.ListItem.Apply(stack);
TouchEffect.SetCommand(stack, command);
return new Frame
{
Padding = 0,
CornerRadius = 5,
Content = stack
};
}
private View CreatePulseEffect(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.Red,
CornerRadius = 10,
Padding = 15,
Content = new Label
{
Text = "Pulse Effect (3 pulses)",
TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center
}
};
// Apply pulse effect
TouchEffectPresets.Special.ApplyPulse(frame, 3);
TouchEffect.SetCommand(frame, command);
return frame;
}
private View CreateBounceEffect(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.Teal,
CornerRadius = 10,
Padding = 15,
Content = new Label
{
Text = "Bounce Effect",
TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center
}
};
// Apply bounce effect
TouchEffectPresets.Special.ApplyBounce(frame);
TouchEffect.SetCommand(frame, command);
return frame;
}
private View CreateNativeRippleEffect(ICommand command)
{
var frame = new Frame
{
BackgroundColor = Colors.Indigo,
CornerRadius = 10,
Padding = 15,
Content = new Label
{
Text = "Native Ripple Effect",
TextColor = Colors.White,
HorizontalOptions = LayoutOptions.Center
}
};
// Apply native ripple effect
TouchEffectPresets.Native.ApplyRipple(frame, Colors.White);
TouchEffect.SetCommand(frame, command);
return frame;
}
}

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="Maui.TouchEffect.Sample.Pages.TouchEffectPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:touch="clr-namespace:Maui.TouchEffect;assembly=MarketAlly.TouchEffect.Maui"
x:Name="Page">
<ContentPage.Resources>
<Style x:Key="GridRowContentStyle" TargetType="StackLayout">
<Setter Property="HorizontalOptions" Value="CenterAndExpand" />
<Setter Property="VerticalOptions" Value="CenterAndExpand" />
<Setter Property="Spacing" Value="10" />
</Style>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{Binding TouchCount, StringFormat='Touches: {0}', Source={x:Reference Page}}" />
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout Padding="{StaticResource ContentPadding}" Spacing="10">
<StackLayout Orientation="Horizontal">
<Label
FontSize="Title"
HorizontalOptions="FillAndExpand"
Text="NativeAnimationBorderless"
TextColor="Black" />
<Switch IsToggled="{Binding NativeAnimationBorderless, Source={x:Reference Page}}" />
</StackLayout>
<Label
FontSize="Title"
HorizontalOptions="CenterAndExpand"
Text="{Binding TouchCount, StringFormat='Touches: {0}', Source={x:Reference Page}}"
TextColor="Black" />
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Image | Toggle" />
<Image
touch:TouchEffect.Command="{Binding Command, Source={x:Reference Page}}"
touch:TouchEffect.IsToggled="False"
touch:TouchEffect.NormalBackgroundImageSource="button.png"
touch:TouchEffect.PressedBackgroundImageSource="button_pressed.png" />
</StackLayout>
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Scale | Fade | Animated" />
<StackLayout
touch:TouchEffect.AnimationDuration="250"
touch:TouchEffect.AnimationEasing="{x:Static Easing.CubicInOut}"
touch:TouchEffect.Command="{Binding Command, Source={x:Reference Page}}"
touch:TouchEffect.PressedOpacity="0.6"
touch:TouchEffect.PressedScale="0.8"
HorizontalOptions="CenterAndExpand"
Orientation="Horizontal">
<BoxView
HeightRequest="20"
WidthRequest="20"
Color="Gold" />
<Label Text="The entire layout receives touches" />
<BoxView
HeightRequest="20"
WidthRequest="20"
Color="Gold" />
</StackLayout>
</StackLayout>
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Native | Long Press | Hover" />
<Label
FontSize="Body"
HorizontalOptions="CenterAndExpand"
Text="{Binding LongPressCount, StringFormat='Long press count: {0}', Source={x:Reference Page}}"
TextColor="Black" />
<StackLayout
Padding="20"
touch:TouchEffect.Command="{Binding Command, Source={x:Reference Page}}"
touch:TouchEffect.HoveredScale="1.2"
touch:TouchEffect.LongPressCommand="{Binding LongPressCommand, Source={x:Reference Page}}"
touch:TouchEffect.NativeAnimation="True"
touch:TouchEffect.NativeAnimationBorderless="{Binding NativeAnimationBorderless, Source={x:Reference Page}}"
BackgroundColor="Black"
HorizontalOptions="CenterAndExpand"
Orientation="Horizontal">
<Label
FontSize="Large"
Text="TITLE"
TextColor="White" />
</StackLayout>
</StackLayout>
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Color | Rotation | Pulse | Animated" />
<StackLayout
Padding="20"
touch:TouchEffect.AnimationDuration="500"
touch:TouchEffect.Command="{Binding Command, Source={x:Reference Page}}"
touch:TouchEffect.NormalBackgroundColor="Gold"
touch:TouchEffect.PressedBackgroundColor="Orange"
touch:TouchEffect.PressedRotation="15"
touch:TouchEffect.PulseCount="2"
HorizontalOptions="CenterAndExpand"
Orientation="Horizontal">
<Label
FontSize="Large"
Text="TITLE"
TextColor="White" />
</StackLayout>
</StackLayout>
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Image | Native" />
<Image
touch:TouchEffect.Command="{Binding Command, Source={x:Reference Page}}"
touch:TouchEffect.NativeAnimation="True"
touch:TouchEffect.NativeAnimationBorderless="{Binding NativeAnimationBorderless, Source={x:Reference Page}}"
Source="button.png" />
</StackLayout>
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Button | ImageButton" />
<Button touch:TouchEffect.Command="{Binding Command, Mode=OneWay, Source={x:Reference Page}}" Text="Button" />
<ImageButton
touch:TouchEffect.Command="{Binding Command, Mode=OneWay, Source={x:Reference Page}}"
HeightRequest="150"
Source="xamarinstore.jpg" />
</StackLayout>
<StackLayout Style="{StaticResource GridRowContentStyle}">
<Label Text="Nested effect" />
<ContentView
Padding="50"
touch:TouchEffect.Command="{Binding ParentCommand, Source={x:Reference Page}}"
touch:TouchEffect.NativeAnimation="True"
touch:TouchEffect.NativeAnimationBorderless="{Binding NativeAnimationBorderless, Source={x:Reference Page}}"
BackgroundColor="Purple"
HorizontalOptions="CenterAndExpand">
<BoxView
touch:TouchEffect.Command="{Binding ChildCommand, Source={x:Reference Page}}"
touch:TouchEffect.NativeAnimation="True"
touch:TouchEffect.NativeAnimationBorderless="{Binding NativeAnimationBorderless, Source={x:Reference Page}}"
HeightRequest="100"
WidthRequest="100"
Color="Gold" />
</ContentView>
</StackLayout>
</StackLayout>
</ScrollView>
</ContentPage>

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Maui.TouchEffect.Sample.ObjectModel;
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
namespace Maui.TouchEffect.Sample.Pages;
public partial class TouchEffectPage : ContentPage
{
public TouchEffectPage()
{
On<iOS>().SetPrefersHomeIndicatorAutoHidden(true);
Command = CommandFactory.Create(() =>
{
TouchCount++;
OnPropertyChanged(nameof(TouchCount));
});
LongPressCommand = CommandFactory.Create(() =>
{
LongPressCount++;
OnPropertyChanged(nameof(LongPressCount));
});
ParentCommand = CommandFactory.Create(() => DisplayAlert("Parent clicked", null, "Ok"));
ChildCommand = CommandFactory.Create(() => DisplayAlert("Child clicked", null, "Ok"));
InitializeComponent();
}
public ICommand Command { get; }
public ICommand LongPressCommand { get; }
public ICommand ParentCommand { get; }
public ICommand ChildCommand { get; }
public int TouchCount { get; private set; }
public int LongPressCount { get; private set; }
bool nativeAnimationBorderless;
public bool NativeAnimationBorderless
{
get => nativeAnimationBorderless;
set
{
nativeAnimationBorderless = value;
OnPropertyChanged();
}
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,10 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace Maui.TouchEffect.Sample;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}

View File

@@ -0,0 +1,15 @@
using Android.App;
using Android.Runtime;
namespace Maui.TouchEffect.Sample;
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View File

@@ -0,0 +1,9 @@
using Foundation;
namespace Maui.TouchEffect.Sample;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Enable this value to use browser developer tools while debugging.-->
<!-- See https://aka.ms/blazor-hybrid-developer-tools -->
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace Maui.TouchEffect.Sample;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -0,0 +1,16 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
namespace Maui.TouchEffect.Sample;
class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="7" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="Maui.TouchEffect.Sample.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>

View File

@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="Maui.TouchEffect.Sample.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:Maui.TouchEffect.Sample.WinUI">
</maui:MauiWinUIApplication>

View File

@@ -0,0 +1,23 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Maui.TouchEffect.Sample.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="FBEF338E-1472-4432-B1A0-7AAC151B42A7" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Maui.TouchEffect.Sample.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,9 @@
using Foundation;
namespace Maui.TouchEffect.Sample;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace Maui.TouchEffect.Sample;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -0,0 +1,93 @@
<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M284.432 247.568L284.004 221.881C316.359 221.335 340.356 211.735 355.308 193.336C382.408 159.996 372.893 108.183 372.786 107.659L398.013 102.831C398.505 105.432 409.797 167.017 375.237 209.53C355.276 234.093 324.719 246.894 284.432 247.568Z" fill="#8A6FE8"/>
<path d="M331.954 109.36L361.826 134.245C367.145 138.676 375.055 137.959 379.497 132.639C383.928 127.32 383.211 119.41 377.891 114.969L348.019 90.0842C342.7 85.6531 334.79 86.3702 330.348 91.6896C325.917 97.0197 326.634 104.929 331.954 109.36Z" fill="#8A6FE8"/>
<path d="M407.175 118.062L417.92 94.2263C420.735 87.858 417.856 80.4087 411.488 77.5831C405.12 74.7682 397.67 77.6473 394.845 84.0156L383.831 108.461L407.175 118.062Z" fill="#8A6FE8"/>
<path d="M401.363 105.175L401.234 69.117C401.181 62.1493 395.498 56.541 388.53 56.5945C381.562 56.648 375.954 62.3313 376.007 69.2989L376.018 96.11L401.363 105.175Z" fill="#8A6FE8"/>
<path d="M386.453 109.071L378.137 73.9548C376.543 67.169 369.757 62.9628 362.971 64.5575C356.185 66.1523 351.979 72.938 353.574 79.7237L362.04 115.482L386.453 109.071Z" fill="#8A6FE8"/>
<path d="M381.776 142.261C396.359 142.261 408.181 130.44 408.181 115.857C408.181 101.274 396.359 89.4527 381.776 89.4527C367.194 89.4527 355.372 101.274 355.372 115.857C355.372 130.44 367.194 142.261 381.776 142.261Z" fill="url(#paint0_radial)"/>
<path d="M248.267 406.979C248.513 384.727 245.345 339.561 222.376 301.736L199.922 315.372C220.76 349.675 222.323 389.715 221.841 407.182C221.798 408.627 235.263 409.933 248.267 406.979Z" fill="url(#paint1_linear)"/>
<path d="M221.841 406.936L242.637 406.84L262.052 518.065L220.311 518.258C217.132 518.269 214.724 515.711 214.938 512.532L221.841 406.936Z" fill="#522CD5"/>
<path d="M306.566 488.814C310.173 491.661 310.109 495.782 309.831 500.127L308.964 513.452C308.803 515.839 306.727 517.798 304.34 517.809L260.832 518.012C258.125 518.023 256.08 515.839 256.262 513.142L256.551 499.335C256.883 494.315 255.192 492.474 251.307 487.744C244.649 479.663 224.967 435.62 226.84 406.925L248.256 406.829C249.691 423.858 272.167 461.682 306.566 488.814Z" fill="url(#paint2_linear)"/>
<path d="M309.82 500.127C310.023 497.088 310.077 494.176 308.889 491.715L254.635 491.961C256.134 494.166 256.765 496.092 256.562 499.314L256.273 513.121C256.091 515.828 258.146 518.012 260.843 517.99L304.34 517.798C306.727 517.787 308.803 515.828 308.964 513.442L309.82 500.127Z" fill="url(#paint3_radial)"/>
<path d="M133.552 407.471C133.103 385.22 135.864 340.021 158.49 301.993L181.073 315.425C160.545 349.921 159.346 389.972 159.989 407.428C160.042 408.884 146.578 410.318 133.552 407.471Z" fill="url(#paint4_linear)"/>
<path d="M110.798 497.152C110.765 494.187 111.204 491.575 112.457 487.23C131.882 434.132 133.52 407.364 133.52 407.364L159.999 407.246C159.999 407.246 161.819 433.512 181.716 486.427C183.289 490.195 183.471 493.641 183.674 496.831L183.792 513.816C183.803 516.374 181.716 518.483 179.158 518.494L177.873 518.504L116.781 518.782L115.496 518.793C112.927 518.804 110.83 516.728 110.819 514.159L110.798 497.152Z" fill="url(#paint5_linear)"/>
<path d="M110.798 497.152C110.798 496.67 110.808 496.199 110.83 495.739C110.969 494.262 111.643 492.603 114.875 492.582L180.207 492.282C182.561 492.367 183.343 494.176 183.589 495.311C183.621 495.814 183.664 496.328 183.696 496.82L183.813 513.806C183.824 515.411 183.011 516.824 181.769 517.669C181.031 518.172 180.132 518.472 179.179 518.483L177.895 518.494L116.802 518.772L115.528 518.782C114.244 518.793 113.077 518.269 112.232 517.434C111.386 516.599 110.862 515.432 110.851 514.148L110.798 497.152Z" fill="url(#paint6_radial)"/>
<path d="M314.979 246.348C324.162 210.407 318.008 181.777 318.008 181.777L326.452 181.734L326.656 181.574C314.262 115.75 256.326 66.0987 186.949 66.4198C108.796 66.773 45.7233 130.424 46.0765 208.577C46.4297 286.731 110.08 349.803 188.234 349.45C249.905 349.172 302.178 309.474 321.304 254.343C321.872 251.999 321.797 247.804 314.979 246.348Z" fill="url(#paint7_radial)"/>
<path d="M310.237 279.035L65.877 280.148C71.3998 289.428 77.95 298.012 85.3672 305.761L290.972 304.829C298.336 297.005 304.8 288.368 310.237 279.035Z" fill="#D8CFF7"/>
<path d="M235.062 312.794L280.924 312.585L280.74 272.021L234.877 272.23L235.062 312.794Z" fill="#512BD4"/>
<path d="M243.001 297.626C242.691 297.626 242.434 297.53 242.22 297.327C242.006 297.123 241.899 296.866 241.899 296.588C241.899 296.299 242.006 296.042 242.22 295.839C242.434 295.625 242.691 295.528 243.001 295.528C243.312 295.528 243.568 295.635 243.782 295.839C243.996 296.042 244.114 296.299 244.114 296.588C244.114 296.877 244.007 297.123 243.793 297.327C243.568 297.519 243.312 297.626 243.001 297.626Z" fill="white"/>
<path d="M255.192 297.434H253.212L247.967 289.203C247.839 289 247.721 288.775 247.636 288.55H247.593C247.636 288.786 247.657 289.299 247.657 290.091L247.668 297.444H245.912L245.891 286.228H247.999L253.062 294.265C253.276 294.597 253.415 294.833 253.479 294.95H253.511C253.458 294.651 253.437 294.148 253.437 293.441L253.426 286.217H255.17L255.192 297.434Z" fill="white"/>
<path d="M263.733 297.412L257.589 297.423L257.568 286.206L263.465 286.195V287.779L259.387 287.79L259.398 290.969L263.155 290.958V292.532L259.398 292.542L259.409 295.86L263.733 295.85V297.412Z" fill="white"/>
<path d="M272.445 287.758L269.298 287.769L269.32 297.401H267.5L267.479 287.769L264.343 287.779V286.195L272.434 286.174L272.445 287.758Z" fill="white"/>
<path d="M315.279 246.337C324.355 210.836 318.457 182.483 318.308 181.798L171.484 182.462C171.484 182.462 162.226 181.563 162.268 190.018C162.311 198.463 162.761 222.341 162.878 248.746C162.9 254.172 167.363 256.773 170.863 256.751C170.874 256.751 311.618 252.213 315.279 246.337Z" fill="url(#paint8_radial)"/>
<path d="M227.685 246.798C227.685 246.798 250.183 228.827 254.571 225.499C258.959 222.17 262.812 221.977 266.869 225.445C270.925 228.913 293.616 246.498 293.616 246.498L227.685 246.798Z" fill="#A08BE8"/>
<path d="M320.748 256.141C320.748 256.141 324.943 248.414 315.279 246.348C315.289 246.305 170.927 246.894 170.927 246.894C167.566 246.905 163.232 244.925 162.846 241.671C162.857 244.004 162.878 246.369 162.889 248.756C162.91 253.68 166.582 256.27 169.878 256.698C170.21 256.73 170.542 256.773 170.874 256.773L180.742 256.73L320.748 256.141Z" fill="#512BD4"/>
<path d="M206.4 233.214C212.511 233.095 217.302 224.667 217.102 214.39C216.901 204.112 211.785 195.878 205.674 195.997C199.563 196.116 194.772 204.544 194.973 214.821C195.173 225.099 200.289 233.333 206.4 233.214Z" fill="#512BD4"/>
<path d="M306.249 214.267C306.356 203.989 301.488 195.605 295.377 195.541C289.266 195.478 284.225 203.758 284.118 214.037C284.011 224.315 288.878 232.699 294.99 232.763C301.101 232.826 306.142 224.545 306.249 214.267Z" fill="#512BD4"/>
<path d="M205.905 205.291C208.152 203.022 211.192 202.016 214.157 202.262C215.912 205.495 217.014 209.733 217.111 214.389C217.164 217.3 216.811 220.04 216.158 222.513C212.669 223.519 208.752 222.662 205.979 219.922C201.912 215.909 201.88 209.348 205.905 205.291Z" fill="#8065E0"/>
<path d="M294.996 204.285C297.255 202.016 300.294 200.999 303.259 201.256C305.164 204.628 306.309 209.209 306.256 214.239C306.224 216.808 305.892 219.259 305.303 221.485C301.793 222.523 297.843 221.678 295.061 218.916C291.004 214.892 290.972 208.342 294.996 204.285Z" fill="#8065E0"/>
<path d="M11.6342 357.017C10.9171 354.716 -5.72611 300.141 21.3204 258.903C36.9468 235.078 63.3083 221.035 99.6664 217.15L102.449 243.276C74.3431 246.273 54.4676 256.345 43.3579 273.202C23.0971 303.941 36.5722 348.733 36.7113 349.183L11.6342 357.017Z" fill="url(#paint9_linear)"/>
<path d="M95.1498 252.802C109.502 252.802 121.137 241.167 121.137 226.815C121.137 212.463 109.502 200.828 95.1498 200.828C80.7976 200.828 69.1628 212.463 69.1628 226.815C69.1628 241.167 80.7976 252.802 95.1498 252.802Z" fill="url(#paint10_radial)"/>
<path d="M72.0098 334.434L33.4683 329.307C26.597 328.397 20.2929 333.214 19.3725 340.085C18.4627 346.956 23.279 353.26 30.1504 354.181L68.6919 359.308C75.5632 360.217 81.8673 355.401 82.7878 348.53C83.6975 341.658 78.8705 335.344 72.0098 334.434Z" fill="#8A6FE8"/>
<path d="M3.73535 367.185L7.35297 393.076C8.36975 399.968 14.7702 404.731 21.6629 403.725C28.5556 402.708 33.3185 396.308 32.3124 389.415L28.5984 362.861L3.73535 367.185Z" fill="#8A6FE8"/>
<path d="M15.5194 374.988L34.849 405.427C38.6058 411.292 46.4082 413.005 52.2735 409.248C58.1387 405.491 59.8512 397.689 56.0945 391.823L41.7953 369.144L15.5194 374.988Z" fill="#8A6FE8"/>
<path d="M26.0511 363.739L51.8026 389.019C56.7688 393.911 64.7532 393.846 69.6445 388.88C74.5358 383.914 74.4715 375.929 69.516 371.038L43.2937 345.297L26.0511 363.739Z" fill="#8A6FE8"/>
<path d="M26.4043 381.912C40.987 381.912 52.8086 370.091 52.8086 355.508C52.8086 340.925 40.987 329.104 26.4043 329.104C11.8216 329.104 0 340.925 0 355.508C0 370.091 11.8216 381.912 26.4043 381.912Z" fill="url(#paint11_radial)"/>
<path d="M184.73 63.6308L157.819 66.5892L158.561 38.5412L177.888 36.4178L184.73 63.6308Z" fill="#8A6FE8"/>
<path d="M170.018 41.647C180.455 39.521 187.193 29.3363 185.067 18.8988C182.941 8.46126 172.757 1.72345 162.319 3.84944C151.882 5.97543 145.144 16.1601 147.27 26.5976C149.396 37.0351 159.58 43.773 170.018 41.647Z" fill="#D8CFF7"/>
<path d="M196.885 79.385C198.102 79.2464 198.948 78.091 198.684 76.8997C195.851 64.2818 183.923 55.5375 170.773 56.9926C157.622 58.4371 147.886 69.5735 147.865 82.4995C147.863 83.7232 148.949 84.6597 150.168 84.5316L196.885 79.385Z" fill="url(#paint12_radial)"/>
<defs>
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(382.004 103.457) scale(26.4058)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
<linearGradient id="paint1_linear" x1="214.439" y1="303.482" x2="236.702" y2="409.505" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="231.673" y1="404.144" x2="297.805" y2="522.048" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(280.957 469.555) rotate(-0.260742) scale(45.8326)">
<stop offset="0.034" stop-color="#522CD5"/>
<stop offset="0.9955" stop-color="#8A6FE8"/>
</radialGradient>
<linearGradient id="paint4_linear" x1="166.061" y1="303.491" x2="144.763" y2="409.709" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<linearGradient id="paint5_linear" x1="146.739" y1="407.302" x2="147.246" y2="518.627" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(148.63 470.023) rotate(179.739) scale(50.2476)">
<stop offset="0.034" stop-color="#522CD5"/>
<stop offset="0.9955" stop-color="#8A6FE8"/>
</radialGradient>
<radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(219.219 153.929) rotate(179.739) scale(140.935)">
<stop offset="0.4744" stop-color="#A08BE8"/>
<stop offset="0.8618" stop-color="#8065E0"/>
</radialGradient>
<radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(314.861 158.738) rotate(179.739) scale(146.053)">
<stop offset="0.0933" stop-color="#E1DFDD"/>
<stop offset="0.6573" stop-color="white"/>
</radialGradient>
<linearGradient id="paint9_linear" x1="54.1846" y1="217.159" x2="54.1846" y2="357.022" gradientUnits="userSpaceOnUse">
<stop offset="0.3344" stop-color="#9780E6"/>
<stop offset="0.8488" stop-color="#8A6FE8"/>
</linearGradient>
<radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(90.3494 218.071) rotate(-0.260742) scale(25.9924)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
<radialGradient id="paint11_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.805 345.043) scale(26.4106)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
<radialGradient id="paint12_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(169.113 67.3662) rotate(-32.2025) scale(21.0773)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with you package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}" />
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}" />
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}" />
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}" />
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}" />
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}" />
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}" />
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}" />
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}" />
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}" />
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}" />
<Color x:Key="Yellow100Accent">#F7B548</Color>
<Color x:Key="Yellow200Accent">#FFD590</Color>
<Color x:Key="Yellow300Accent">#FFE5B9</Color>
<Color x:Key="Cyan100Accent">#28C2D1</Color>
<Color x:Key="Cyan200Accent">#7BDDEF</Color>
<Color x:Key="Cyan300Accent">#C3F2F4</Color>
<Color x:Key="Blue100Accent">#3E8EED</Color>
<Color x:Key="Blue200Accent">#72ACF1</Color>
<Color x:Key="Blue300Accent">#A7CBF6</Color>
<!-- Colors -->
<Color x:Key="AppBackgroundColor">#FAFAFA</Color>
<Color x:Key="PrimaryColor">#FAFAFA</Color>
<Color x:Key="NavigationBarTextColor">#5D5D5D</Color>
<Color x:Key="NormalButtonBackgroundColor">#1976D2</Color>
<Color x:Key="NormalButtonTextColor">#FFFFFF</Color>
<Color x:Key="HeadingLabelTextColor">#5D5D5D</Color>
<Color x:Key="NormalLabelTextColor">#888888</Color>
<Color x:Key="DarkLabelTextColor">#000000</Color>
<Color x:Key="DarkLabelPlaceholderColor">#333d47</Color>
<Color x:Key="SoftFrameBackgroundColor">#ecf0f9</Color>
<Color x:Key="StatusbarColor">#1976D2</Color>
<Color x:Key="NavBarColor">#1976D2</Color>
</ResourceDictionary>

View File

@@ -0,0 +1,417 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls">
<Thickness x:Key="ContentPadding">20, 0</Thickness>
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}" />
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Primary}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="14,10" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent" />
<Setter Property="BorderWidth" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44" />
<Setter Property="MinimumWidthRequest" Value="44" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style ApplyToDerivedTypes="True" TargetType="Page">
<Setter Property="Padding" Value="0" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style ApplyToDerivedTypes="True" TargetType="Shell">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.ForegroundColor" Value="{OnPlatform WinUI={StaticResource Primary}, Default={StaticResource White}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style ApplyToDerivedTypes="True" TargetType="ContentPage">
<Setter Property="BackgroundColor" Value="{StaticResource AppBackgroundColor}" />
<Setter Property="ios:Page.UseSafeArea" Value="False" />
<Setter Property="NavigationPage.BackButtonTitle" Value="" />
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,7 @@
namespace Maui.TouchEffect.Enums;
public enum HoverState
{
Normal,
Hovered
}

View File

@@ -0,0 +1,7 @@
namespace Maui.TouchEffect.Enums;
public enum HoverStatus
{
Entered,
Exited,
}

View File

@@ -0,0 +1,7 @@
namespace Maui.TouchEffect.Enums;
public enum TouchInteractionStatus
{
Started,
Completed,
}

View File

@@ -0,0 +1,7 @@
namespace Maui.TouchEffect.Enums;
public enum TouchState
{
Normal,
Pressed,
}

View File

@@ -0,0 +1,8 @@
namespace Maui.TouchEffect.Enums;
public enum TouchStatus
{
Started,
Completed,
Canceled,
}

View File

@@ -0,0 +1,12 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
public class HoverStateChangedEventArgs : EventArgs
{
internal HoverStateChangedEventArgs(HoverState state)
{
State = state;
}
public HoverState State { get; }
}

View File

@@ -0,0 +1,12 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
public class HoverStatusChangedEventArgs : EventArgs
{
internal HoverStatusChangedEventArgs(HoverStatus status)
{
Status = status;
}
public HoverStatus Status { get; }
}

View File

@@ -0,0 +1,11 @@
namespace Maui.TouchEffect;
public class LongPressCompletedEventArgs : EventArgs
{
internal LongPressCompletedEventArgs(object? parameter)
{
Parameter = parameter;
}
public object? Parameter { get; }
}

View File

@@ -0,0 +1,11 @@
namespace Maui.TouchEffect;
public class TouchCompletedEventArgs : EventArgs
{
internal TouchCompletedEventArgs(object? parameter)
{
Parameter = parameter;
}
public object? Parameter { get; }
}

View File

@@ -0,0 +1,12 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
public class TouchInteractionStatusChangedEventArgs : EventArgs
{
internal TouchInteractionStatusChangedEventArgs(TouchInteractionStatus touchInteractionStatus)
{
TouchInteractionStatus = touchInteractionStatus;
}
public TouchInteractionStatus TouchInteractionStatus { get; }
}

View File

@@ -0,0 +1,12 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
public class TouchStateChangedEventArgs : EventArgs
{
internal TouchStateChangedEventArgs(TouchState state)
{
State = state;
}
public TouchState State { get; }
}

View File

@@ -0,0 +1,12 @@
using Maui.TouchEffect.Enums;
namespace Maui.TouchEffect;
public class TouchStatusChangedEventArgs : EventArgs
{
internal TouchStatusChangedEventArgs(TouchStatus status)
{
Status = status;
}
public TouchStatus Status { get; }
}

View File

@@ -0,0 +1,123 @@
namespace MauiTouchEffect.Extensions;
/// <summary>
/// Extension methods for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask
/// </summary>
internal static class SafeFireAndForgetExtension
{
/// <summary>
/// Safely execute the ValueTask without waiting for it to complete before moving to the next line of
/// code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async
/// Void": https://johnthiriet.com/removing-async-void/.
/// </summary>
/// <param name="task">ValueTask.</param>
/// <param name="onException">
/// If an exception is thrown in the ValueTask, <c>onException</c> will
/// execute. If onException is null, the exception will be re-thrown
/// </param>
/// <param name="continueOnCapturedContext">
/// If set to <c>true</c>, continue on captured context; this
/// will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c>,
/// continue on a different context; this will allow the Synchronization Context to continue on a
/// different thread
/// </param>
internal static void SafeFireAndForget(this ValueTask task, in Action<Exception>? onException = null, in bool continueOnCapturedContext = false)
{
HandleSafeFireAndForget(task, continueOnCapturedContext, onException);
}
/// <summary>
/// Safely execute the ValueTask without waiting for it to complete before moving to the next line of
/// code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async
/// Void": https://johnthiriet.com/removing-async-void/.
/// </summary>
/// <param name="task">ValueTask.</param>
/// <param name="onException">
/// If an exception is thrown in the Task, <c>onException</c> will execute.
/// If onException is null, the exception will be re-thrown
/// </param>
/// <param name="continueOnCapturedContext">
/// If set to <c>true</c>, continue on captured context; this
/// will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c>,
/// continue on a different context; this will allow the Synchronization Context to continue on a
/// different thread
/// </param>
/// <typeparam name="TException">
/// Exception type. If an exception is thrown of a different type, it will
/// not be handled
/// </typeparam>
internal static void SafeFireAndForget<TException>(this ValueTask task, in Action<TException>? onException = null, in bool continueOnCapturedContext = false) where TException : Exception
{
HandleSafeFireAndForget(task, continueOnCapturedContext, onException);
}
/// <summary>
/// Safely execute the Task without waiting for it to complete before moving to the next line of code;
/// commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void":
/// https://johnthiriet.com/removing-async-void/.
/// </summary>
/// <param name="task">Task.</param>
/// <param name="onException">
/// If an exception is thrown in the Task, <c>onException</c> will execute.
/// If onException is null, the exception will be re-thrown
/// </param>
/// <param name="continueOnCapturedContext">
/// If set to <c>true</c>, continue on captured context; this
/// will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c>,
/// continue on a different context; this will allow the Synchronization Context to continue on a
/// different thread
/// </param>
internal static void SafeFireAndForget(this Task task, in Action<Exception>? onException = null, in bool continueOnCapturedContext = false)
{
HandleSafeFireAndForget(task, continueOnCapturedContext, onException);
}
/// <summary>
/// Safely execute the Task without waiting for it to complete before moving to the next line of code;
/// commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void":
/// https://johnthiriet.com/removing-async-void/.
/// </summary>
/// <param name="task">Task.</param>
/// <param name="onException">
/// If an exception is thrown in the Task, <c>onException</c> will execute.
/// If onException is null, the exception will be re-thrown
/// </param>
/// <param name="continueOnCapturedContext">
/// If set to <c>true</c>, continue on captured context; this
/// will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c>,
/// continue on a different context; this will allow the Synchronization Context to continue on a
/// different thread
/// </param>
/// <typeparam name="TException">
/// Exception type. If an exception is thrown of a different type, it will
/// not be handled
/// </typeparam>
internal static void SafeFireAndForget<TException>(this Task task, in Action<TException>? onException = null, in bool continueOnCapturedContext = false) where TException : Exception
{
HandleSafeFireAndForget(task, continueOnCapturedContext, onException);
}
private static async void HandleSafeFireAndForget<TException>(ValueTask valueTask, bool continueOnCapturedContext, Action<TException>? onException) where TException : Exception
{
try
{
await valueTask.ConfigureAwait(continueOnCapturedContext);
}
catch (TException ex) when (onException != null)
{
onException(ex);
}
}
private static async void HandleSafeFireAndForget<TException>(Task task, bool continueOnCapturedContext, Action<TException>? onException) where TException : Exception
{
try
{
await task.ConfigureAwait(continueOnCapturedContext);
}
catch (TException ex) when (onException != null)
{
onException(ex);
}
}
}

View File

@@ -0,0 +1,80 @@
namespace Maui.TouchEffect.Extensions;
/// <summary>
/// Extension methods for <see cref="VisualElement" />.
/// </summary>
public static class VisualElementExtension
{
public static Task<bool> ColorTo(this VisualElement element, Color? color, uint length = 250u, Easing? easing = null)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
var animationCompletionSource = new TaskCompletionSource<bool>();
if (color is null)
{
return Task.FromResult(false);
}
if (element.BackgroundColor is null)
{
element.BackgroundColor = color;
return Task.FromResult(false);
}
new Animation
{
{ 0, 1, new Animation(v => element.BackgroundColor = new Color((float)v, element.BackgroundColor.Green, element.BackgroundColor.Blue, element.BackgroundColor.Alpha), element.BackgroundColor.Red, color.Red) },
{ 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.Red, (float)v, element.BackgroundColor.Blue, element.BackgroundColor.Alpha), element.BackgroundColor.Green, color.Green) },
{ 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.Red, element.BackgroundColor.Green, (float)v, element.BackgroundColor.Alpha), element.BackgroundColor.Blue, color.Blue) },
{ 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.Red, element.BackgroundColor.Green, element.BackgroundColor.Blue, (float)v), element.BackgroundColor.Alpha, color.Alpha) },
}.Commit(element, nameof(ColorTo), 16, length, easing, (d, b) => animationCompletionSource.SetResult(true));
return animationCompletionSource.Task;
}
public static void AbortAnimations(this VisualElement element, params string[] otherAnimationNames)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.CancelAnimations();
_ = element.AbortAnimation(nameof(ColorTo));
if (otherAnimationNames == null)
{
return;
}
foreach (var name in otherAnimationNames)
{
_ = element.AbortAnimation(name);
}
}
internal static bool TryFindParentElementWithParentOfType<T>(this VisualElement element, out VisualElement result, out T parent) where T : VisualElement
{
result = null;
parent = null;
while (element?.Parent != null)
{
if (element.Parent is not T parentElement)
{
element = element.Parent as VisualElement;
continue;
}
result = element;
parent = parentElement;
return true;
}
return false;
}
internal static bool TryFindParentOfType<T>(this VisualElement element, out T parent) where T : VisualElement
{
return element.TryFindParentElementWithParentOfType(out _, out parent);
}
}

View File

@@ -0,0 +1,772 @@
using Maui.TouchEffect.Enums;
using Maui.TouchEffect.Extensions;
using static System.Math;
namespace Maui.TouchEffect;
internal sealed class GestureManager
{
private const int _animationProgressDelay = 10;
private Color? _defaultBackgroundColor;
private CancellationTokenSource? _longPressTokenSource;
private CancellationTokenSource? _animationTokenSource;
private Func<TouchEffect, TouchState, HoverState, int, Easing, CancellationToken, Task>? _animationTaskFactory;
private double? _durationMultiplier;
private double _animationProgress;
private TouchState? _animationState;
internal void HandleTouch(TouchEffect sender, TouchStatus status)
{
if (sender.IsDisabled)
{
return;
}
var canExecuteAction = sender.CanExecute;
if (status != TouchStatus.Started || canExecuteAction)
{
if (!canExecuteAction)
{
status = TouchStatus.Canceled;
}
var state = status == TouchStatus.Started
? TouchState.Pressed
: TouchState.Normal;
if (status == TouchStatus.Started)
{
_animationProgress = 0;
_animationState = state;
}
var isToggled = sender.IsToggled;
if (isToggled.HasValue)
{
if (status != TouchStatus.Started)
{
_durationMultiplier = _animationState == TouchState.Pressed && !isToggled.Value ||
_animationState == TouchState.Normal && isToggled.Value
? 1 - _animationProgress
: _animationProgress;
UpdateStatusAndState(sender, status, state);
if (status == TouchStatus.Canceled)
{
sender.ForceUpdateState(false);
return;
}
OnTapped(sender);
sender.IsToggled = !isToggled;
return;
}
state = isToggled.Value
? TouchState.Normal
: TouchState.Pressed;
}
UpdateStatusAndState(sender, status, state);
}
if (status == TouchStatus.Completed)
{
OnTapped(sender);
}
}
internal static void HandleUserInteraction(TouchEffect sender, TouchInteractionStatus interactionStatus)
{
if (sender.InteractionStatus != interactionStatus)
{
sender.InteractionStatus = interactionStatus;
sender.RaiseInteractionStatusChanged();
}
}
internal static void HandleHover(TouchEffect sender, HoverStatus status)
{
if (!sender.Element?.IsEnabled ?? true)
{
return;
}
var hoverState = status == HoverStatus.Entered
? HoverState.Hovered
: HoverState.Normal;
if (sender.HoverState != hoverState)
{
sender.HoverState = hoverState;
sender.RaiseHoverStateChanged();
}
if (sender.HoverStatus != status)
{
sender.HoverStatus = status;
sender.RaiseHoverStatusChanged();
}
}
internal async Task ChangeStateAsync(TouchEffect sender, bool animated)
{
var status = sender.Status;
var state = sender.State;
var hoverState = sender.HoverState;
AbortAnimations(sender);
_animationTokenSource = new CancellationTokenSource();
var token = _animationTokenSource.Token;
var isToggled = sender.IsToggled;
if (sender.Element != null)
{
UpdateVisualState(sender.Element, state, hoverState);
}
if (!animated)
{
if (isToggled.HasValue)
{
state = isToggled.Value
? TouchState.Pressed
: TouchState.Normal;
}
var durationMultiplier = _durationMultiplier;
_durationMultiplier = null;
await RunAnimationTask(sender, state, hoverState, _animationTokenSource.Token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false);
return;
}
var pulseCount = sender.PulseCount;
if (pulseCount == 0 || state == TouchState.Normal && !isToggled.HasValue)
{
if (isToggled.HasValue)
{
state =
status == TouchStatus.Started && isToggled.Value ||
status != TouchStatus.Started && !isToggled.Value
? TouchState.Normal
: TouchState.Pressed;
}
await RunAnimationTask(sender, state, hoverState, _animationTokenSource.Token).ConfigureAwait(false);
return;
}
do
{
var rippleState = isToggled.HasValue && isToggled.Value
? TouchState.Normal
: TouchState.Pressed;
await RunAnimationTask(sender, rippleState, hoverState, _animationTokenSource.Token);
if (token.IsCancellationRequested)
{
return;
}
rippleState = isToggled.HasValue && isToggled.Value
? TouchState.Pressed
: TouchState.Normal;
await RunAnimationTask(sender, rippleState, hoverState, _animationTokenSource.Token);
if (token.IsCancellationRequested)
{
return;
}
} while (--pulseCount != 0);
}
internal void HandleLongPress(TouchEffect sender)
{
if (sender.State == TouchState.Normal)
{
_longPressTokenSource?.Cancel();
_longPressTokenSource?.Dispose();
_longPressTokenSource = null;
return;
}
if (sender.LongPressCommand == null || sender.InteractionStatus == TouchInteractionStatus.Completed)
{
return;
}
_longPressTokenSource = new CancellationTokenSource();
_ = Task.Delay(sender.LongPressDuration, _longPressTokenSource.Token).ContinueWith(t =>
{
if (t.IsFaulted && t.Exception != null)
{
throw t.Exception;
}
if (t.IsCanceled)
{
return;
}
var longPressAction = new Action(() =>
{
sender.HandleUserInteraction(TouchInteractionStatus.Completed);
sender.RaiseLongPressCompleted();
});
if (MainThread.IsMainThread)
{
MainThread.BeginInvokeOnMainThread(longPressAction);
}
else
{
longPressAction.Invoke();
}
});
}
internal void SetCustomAnimationTask(Func<TouchEffect, TouchState, HoverState, int, Easing, CancellationToken, Task>? animationTaskFactory)
{
_animationTaskFactory = animationTaskFactory;
}
internal void Reset()
{
SetCustomAnimationTask(null);
_defaultBackgroundColor = default;
}
internal void OnTapped(TouchEffect sender)
{
if (!sender.CanExecute || sender.LongPressCommand != null && sender.InteractionStatus == TouchInteractionStatus.Completed)
{
return;
}
if (DeviceInfo.Current.Platform == DevicePlatform.Android)
{
HandleCollectionViewSelection(sender);
}
if (sender.Element is IButtonController button)
{
button.SendClicked();
}
sender.RaiseCompleted();
}
private static void HandleCollectionViewSelection(TouchEffect sender)
{
if (!sender.Element.TryFindParentElementWithParentOfType(out var result, out CollectionView parent))
{
return;
}
var collectionView = parent ?? throw new NullReferenceException();
var item = result?.BindingContext ?? result ?? throw new NullReferenceException();
switch (collectionView.SelectionMode)
{
case SelectionMode.Single:
collectionView.SelectedItem = item;
break;
case SelectionMode.Multiple:
var selectedItems = collectionView.SelectedItems?.ToList() ?? new List<object>();
if (selectedItems.Contains(item))
{
_ = selectedItems.Remove(item);
}
else
{
selectedItems.Add(item);
}
collectionView.UpdateSelectedItems(selectedItems);
break;
}
}
internal void AbortAnimations(TouchEffect sender)
{
_animationTokenSource?.Cancel();
_animationTokenSource?.Dispose();
_animationTokenSource = null;
var element = sender.Element;
if (element == null)
{
return;
}
element.AbortAnimations();
}
private static void UpdateStatusAndState(TouchEffect sender, TouchStatus status, TouchState state)
{
sender.Status = status;
sender.RaiseStatusChanged();
if (sender.State != state || status != TouchStatus.Canceled)
{
sender.State = state;
sender.RaiseStateChanged();
}
}
private static void UpdateVisualState(VisualElement visualElement, TouchState touchState, HoverState hoverState)
{
var state = touchState == TouchState.Pressed
? TouchEffect.PressedVisualState
: hoverState == HoverState.Hovered
? TouchEffect.HoveredVisualState
: TouchEffect.UnpressedVisualState;
_ = VisualStateManager.GoToState(visualElement, state);
}
private static async Task SetBackgroundImageAsync(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, CancellationToken token)
{
var normalBackgroundImageSource = sender.NormalBackgroundImageSource;
var pressedBackgroundImageSource = sender.PressedBackgroundImageSource;
var hoveredBackgroundImageSource = sender.HoveredBackgroundImageSource;
if (normalBackgroundImageSource == null &&
pressedBackgroundImageSource == null &&
hoveredBackgroundImageSource == null)
{
return;
}
var aspect = sender.BackgroundImageAspect;
var source = normalBackgroundImageSource;
if (touchState == TouchState.Pressed)
{
if (sender.Element?.IsSet(TouchEffect.PressedBackgroundImageAspectProperty) ?? false)
{
aspect = sender.PressedBackgroundImageAspect;
}
source = pressedBackgroundImageSource;
}
else if (hoverState == HoverState.Hovered)
{
if (sender.Element?.IsSet(TouchEffect.HoveredBackgroundImageAspectProperty) ?? false)
{
aspect = sender.HoveredBackgroundImageAspect;
}
if (sender.Element?.IsSet(TouchEffect.HoveredBackgroundImageSourceProperty) ?? false)
{
source = hoveredBackgroundImageSource;
}
}
else
{
if (sender.Element?.IsSet(TouchEffect.NormalBackgroundImageAspectProperty) ?? false)
{
aspect = sender.NormalBackgroundImageAspect;
}
}
try
{
if (sender.ShouldSetImageOnAnimationEnd && duration > 0)
{
await Task.Delay(duration, token);
}
}
catch (TaskCanceledException)
{
return;
}
if (sender.Element is Image image)
{
using (image.Batch())
{
image.Aspect = aspect;
image.Source = source;
}
}
}
private Task SetBackgroundColor(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
{
var normalBackgroundColor = sender.NormalBackgroundColor;
var pressedBackgroundColor = sender.PressedBackgroundColor;
var hoveredBackgroundColor = sender.HoveredBackgroundColor;
if (sender.Element == null
|| normalBackgroundColor is null
&& pressedBackgroundColor is null
&& hoveredBackgroundColor is null)
{
return Task.FromResult(false);
}
var element = sender.Element;
if (_defaultBackgroundColor == default)
{
_defaultBackgroundColor = element.BackgroundColor ?? normalBackgroundColor;
}
var color = GetBackgroundColor(normalBackgroundColor);
if (touchState == TouchState.Pressed)
{
color = GetBackgroundColor(pressedBackgroundColor);
}
else if (hoverState == HoverState.Hovered && sender.Element.IsSet(TouchEffect.HoveredBackgroundColorProperty))
{
color = GetBackgroundColor(hoveredBackgroundColor);
}
if (duration <= 0)
{
element.AbortAnimations();
element.BackgroundColor = color;
return Task.FromResult(true);
}
return element.ColorTo(color, (uint)duration, easing);
}
private static Task? SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
{
var normalOpacity = sender.NormalOpacity;
var pressedOpacity = sender.PressedOpacity;
var hoveredOpacity = sender.HoveredOpacity;
if (Abs(normalOpacity - 1) <= double.Epsilon &&
Abs(pressedOpacity - 1) <= double.Epsilon &&
Abs(hoveredOpacity - 1) <= double.Epsilon)
{
return Task.FromResult(false);
}
var opacity = normalOpacity;
if (touchState == TouchState.Pressed)
{
opacity = pressedOpacity;
}
else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredOpacityProperty) ?? false))
{
opacity = hoveredOpacity;
}
var element = sender.Element;
if (duration <= 0 && element != null)
{
element.AbortAnimations();
element.Opacity = opacity;
return Task.FromResult(true);
}
return element?.FadeTo(opacity, (uint)Abs(duration), easing);
}
private Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
{
var normalScale = sender.NormalScale;
var pressedScale = sender.PressedScale;
var hoveredScale = sender.HoveredScale;
if (Abs(normalScale - 1) <= double.Epsilon &&
Abs(pressedScale - 1) <= double.Epsilon &&
Abs(hoveredScale - 1) <= double.Epsilon)
{
return Task.FromResult(false);
}
var scale = normalScale;
if (touchState == TouchState.Pressed)
{
scale = pressedScale;
}
else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredScaleProperty) ?? false))
{
scale = hoveredScale;
}
var element = sender.Element;
if (element == null)
{
return Task.FromResult(false);
}
if (duration <= 0)
{
element.AbortAnimations(nameof(SetScale));
element.Scale = scale;
return Task.FromResult(true);
}
var animationCompletionSource = new TaskCompletionSource<bool>();
element.Animate(nameof(SetScale), v =>
{
if (double.IsNaN(v))
{
return;
}
element.Scale = v;
}, element.Scale, scale, 16, (uint)Abs(duration), easing, (v, b) => animationCompletionSource.SetResult(b));
return animationCompletionSource.Task;
}
private static Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing)
{
var normalTranslationX = sender.NormalTranslationX;
var pressedTranslationX = sender.PressedTranslationX;
var hoveredTranslationX = sender.HoveredTranslationX;
var normalTranslationY = sender.NormalTranslationY;
var pressedTranslationY = sender.PressedTranslationY;
var hoveredTranslationY = sender.HoveredTranslationY;
if (Abs(normalTranslationX) <= double.Epsilon
&& Abs(pressedTranslationX) <= double.Epsilon
&& Abs(hoveredTranslationX) <= double.Epsilon
&& Abs(normalTranslationY) <= double.Epsilon
&& Abs(pressedTranslationY) <= double.Epsilon
&& Abs(hoveredTranslationY) <= double.Epsilon)
{
return Task.FromResult(false);
}
var translationX = normalTranslationX;
var translationY = normalTranslationY;
if (touchState == TouchState.Pressed)
{
translationX = pressedTranslationX;
translationY = pressedTranslationY;
}
else if (hoverState == HoverState.Hovered)
{
if (sender.Element?.IsSet(TouchEffect.HoveredTranslationXProperty) ?? false)
{
translationX = hoveredTranslationX;
}
if (sender.Element?.IsSet(TouchEffect.HoveredTranslationYProperty) ?? false)
{
translationY = hoveredTranslationY;
}
}
var element = sender.Element;
if (duration <= 0 && element != null)
{
element.AbortAnimations();
element.TranslationX = translationX;
element.TranslationY = translationY;
return Task.FromResult(true);
}
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)
{
var normalRotation = sender.NormalRotation;
var pressedRotation = sender.PressedRotation;
var hoveredRotation = sender.HoveredRotation;
if (Abs(normalRotation) <= double.Epsilon
&& Abs(pressedRotation) <= double.Epsilon
&& Abs(hoveredRotation) <= double.Epsilon)
{
return Task.FromResult(false);
}
var rotation = normalRotation;
if (touchState == TouchState.Pressed)
{
rotation = pressedRotation;
}
else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredRotationProperty) ?? false))
{
rotation = hoveredRotation;
}
var element = sender.Element;
if (duration <= 0 && element != null)
{
element.AbortAnimations();
element.Rotation = rotation;
return Task.FromResult(true);
}
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)
{
var normalRotationX = sender.NormalRotationX;
var pressedRotationX = sender.PressedRotationX;
var hoveredRotationX = sender.HoveredRotationX;
if (Abs(normalRotationX) <= double.Epsilon &&
Abs(pressedRotationX) <= double.Epsilon &&
Abs(hoveredRotationX) <= double.Epsilon)
{
return Task.FromResult(false);
}
var rotationX = normalRotationX;
if (touchState == TouchState.Pressed)
{
rotationX = pressedRotationX;
}
else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredRotationXProperty) ?? false))
{
rotationX = hoveredRotationX;
}
var element = sender.Element;
if (duration <= 0 && element != null)
{
element.AbortAnimations();
element.RotationX = rotationX;
return Task.FromResult(true);
}
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)
{
var normalRotationY = sender.NormalRotationY;
var pressedRotationY = sender.PressedRotationY;
var hoveredRotationY = sender.HoveredRotationY;
if (Abs(normalRotationY) <= double.Epsilon &&
Abs(pressedRotationY) <= double.Epsilon &&
Abs(hoveredRotationY) <= double.Epsilon)
{
return Task.FromResult(false);
}
var rotationY = normalRotationY;
if (touchState == TouchState.Pressed)
{
rotationY = pressedRotationY;
}
else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredRotationYProperty) ?? false))
{
rotationY = hoveredRotationY;
}
var element = sender.Element;
if (duration <= 0 && element != null)
{
element.AbortAnimations();
element.RotationY = rotationY;
return Task.FromResult(true);
}
return element?.RotateYTo(rotationY, (uint)Abs(duration), easing) ?? Task.FromResult(false);
}
private Color? GetBackgroundColor(Color? color)
{
return color != KnownColor.Default
? color
: _defaultBackgroundColor;
}
private Task RunAnimationTask(TouchEffect sender, TouchState touchState, HoverState hoverState, CancellationToken token, double? durationMultiplier = null)
{
if (sender.Element == null)
{
return Task.FromResult(false);
}
var duration = sender.AnimationDuration;
var easing = sender.AnimationEasing;
if (touchState == TouchState.Pressed)
{
if (sender.Element.IsSet(TouchEffect.PressedAnimationDurationProperty))
{
duration = sender.PressedAnimationDuration;
}
if (sender.Element.IsSet(TouchEffect.PressedAnimationEasingProperty))
{
easing = sender.PressedAnimationEasing;
}
}
else if (hoverState == HoverState.Hovered)
{
if (sender.Element.IsSet(TouchEffect.HoveredAnimationDurationProperty))
{
duration = sender.HoveredAnimationDuration;
}
if (sender.Element.IsSet(TouchEffect.HoveredAnimationEasingProperty))
{
easing = sender.HoveredAnimationEasing;
}
}
else
{
if (sender.Element.IsSet(TouchEffect.NormalAnimationDurationProperty))
{
duration = sender.NormalAnimationDuration;
}
if (sender.Element.IsSet(TouchEffect.NormalAnimationEasingProperty))
{
easing = sender.NormalAnimationEasing;
}
}
if (durationMultiplier.HasValue)
{
duration = (int)durationMultiplier.Value * duration;
}
duration = Max(duration, 0);
return Task.WhenAll(
_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),
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 () =>
{
_animationProgress = 0;
_animationState = touchState;
for (var progress = _animationProgressDelay; progress < duration; progress += _animationProgressDelay)
{
await Task.Delay(_animationProgressDelay).ConfigureAwait(false);
if (token.IsCancellationRequested)
{
return;
}
_animationProgress = (double)progress / duration;
}
_animationProgress = 1;
}));
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Hosting;
namespace Maui.TouchEffect.Hosting;
public static class AppBuilderExtensions
{
public static MauiAppBuilder UseMauiTouchEffect(this MauiAppBuilder builder)
{
builder.ConfigureEffects(effects =>
{
// Register TouchEffect for all supported platforms
#if IOS
effects.Add<TouchEffect, PlatformTouchEffect>();
#endif
#if ANDROID
effects.Add<TouchEffect, PlatformTouchEffect>();
#endif
#if WINDOWS
effects.Add<TouchEffect, PlatformTouchEffect>();
#endif
});
return builder;
}
}

View File

@@ -0,0 +1,150 @@
using System;
namespace Maui.TouchEffect.Interfaces;
/// <summary>
/// Interface for logging TouchEffect events and errors.
/// </summary>
public interface ITouchEffectLogger
{
/// <summary>
/// Logs an error with exception details.
/// </summary>
/// <param name="exception">The exception that occurred.</param>
/// <param name="context">Context information about where the error occurred.</param>
/// <param name="additionalInfo">Optional additional information.</param>
void LogError(Exception exception, string context, string? additionalInfo = null);
/// <summary>
/// Logs a warning message.
/// </summary>
/// <param name="message">The warning message.</param>
/// <param name="context">Context information about the warning.</param>
void LogWarning(string message, string context);
/// <summary>
/// Logs an informational message.
/// </summary>
/// <param name="message">The information message.</param>
/// <param name="context">Context information.</param>
void LogInformation(string message, string context);
/// <summary>
/// Logs debug information (only in debug builds).
/// </summary>
/// <param name="message">The debug message.</param>
/// <param name="context">Context information.</param>
void LogDebug(string message, string context);
/// <summary>
/// Logs performance metrics.
/// </summary>
/// <param name="operationName">Name of the operation being measured.</param>
/// <param name="elapsedMilliseconds">Time taken in milliseconds.</param>
/// <param name="additionalMetrics">Optional additional metrics.</param>
void LogPerformance(string operationName, double elapsedMilliseconds, Dictionary<string, object>? additionalMetrics = null);
}
/// <summary>
/// Default implementation of ITouchEffectLogger that uses System.Diagnostics.
/// </summary>
public class DefaultTouchEffectLogger : ITouchEffectLogger
{
private readonly bool _enableDebugLogging;
private readonly bool _enablePerformanceLogging;
/// <summary>
/// Creates a new instance of the default logger.
/// </summary>
/// <param name="enableDebugLogging">Whether to enable debug logging.</param>
/// <param name="enablePerformanceLogging">Whether to enable performance logging.</param>
public DefaultTouchEffectLogger(bool enableDebugLogging = false, bool enablePerformanceLogging = false)
{
_enableDebugLogging = enableDebugLogging;
_enablePerformanceLogging = enablePerformanceLogging;
}
/// <inheritdoc/>
public void LogError(Exception exception, string context, string? additionalInfo = null)
{
var message = $"[TouchEffect ERROR] {context}: {exception.Message}";
if (!string.IsNullOrEmpty(additionalInfo))
{
message += $" | Additional Info: {additionalInfo}";
}
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/>
public void LogWarning(string message, string context)
{
System.Diagnostics.Debug.WriteLine($"[TouchEffect WARNING] {context}: {message}");
}
/// <inheritdoc/>
public void LogInformation(string message, string context)
{
System.Diagnostics.Debug.WriteLine($"[TouchEffect INFO] {context}: {message}");
}
/// <inheritdoc/>
public void LogDebug(string message, string context)
{
#if DEBUG
if (_enableDebugLogging)
{
System.Diagnostics.Debug.WriteLine($"[TouchEffect DEBUG] {context}: {message}");
}
#endif
}
/// <inheritdoc/>
public void LogPerformance(string operationName, double elapsedMilliseconds, Dictionary<string, object>? additionalMetrics = null)
{
if (!_enablePerformanceLogging)
return;
var message = $"[TouchEffect PERF] {operationName}: {elapsedMilliseconds:F2}ms";
if (additionalMetrics != null)
{
var metrics = string.Join(", ", additionalMetrics.Select(kvp => $"{kvp.Key}={kvp.Value}"));
message += $" | Metrics: {metrics}";
}
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
}
}
/// <summary>
/// Null logger implementation for when logging is disabled.
/// </summary>
public class NullTouchEffectLogger : ITouchEffectLogger
{
/// <summary>
/// Gets the singleton instance of the null logger.
/// </summary>
public static NullTouchEffectLogger Instance { get; } = new NullTouchEffectLogger();
private NullTouchEffectLogger() { }
public void LogError(Exception exception, string context, string? additionalInfo = null) { }
public void LogWarning(string message, string context) { }
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

@@ -0,0 +1,16 @@
namespace Maui.TouchEffect;
internal static class JavaObjectExtensions
{
public static bool IsDisposed(this Java.Lang.Object obj)
=> obj.Handle == IntPtr.Zero;
public static bool IsAlive(this Java.Lang.Object obj)
=> obj != null && !obj.IsDisposed();
public static bool IsDisposed(this global::Android.Runtime.IJavaObject obj)
=> obj.Handle == IntPtr.Zero;
public static bool IsAlive(this global::Android.Runtime.IJavaObject obj)
=> obj != null && !obj.IsDisposed();
}

View File

@@ -0,0 +1,420 @@
using Android.Content;
using Android.Content.Res;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Views;
using Android.Views.Accessibility;
using Android.Widget;
using Microsoft.Maui.Platform;
using System.ComponentModel;
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;
public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffect
{
private static readonly Mcolor defaultNativeAnimationColor = new(128, 128, 128, 64);
AccessibilityManager? accessibilityManager;
AccessibilityListener? accessibilityListener;
TouchEffect? effect;
bool isHoverSupported;
RippleDrawable? ripple;
AView? rippleView;
float startX;
float startY;
Mcolor? rippleColor;
int rippleRadius = -1;
AView view => Control ?? Container;
ViewGroup? group => (Container ?? Control) as ViewGroup;
internal bool IsCanceled { get; set; }
bool IsAccessibilityMode => accessibilityManager != null
&& accessibilityManager.IsEnabled
&& accessibilityManager.IsTouchExplorationEnabled;
bool IsForegroundRippleWithTapGestureRecognizer
=> ripple != null &&
ripple.IsAlive() &&
view.IsAlive() &&
view.Foreground == ripple &&
Element is Mview mauiView &&
mauiView.GestureRecognizers.Any(gesture => gesture is TapGestureRecognizer);
protected override void OnAttached()
{
if (view == null)
return;
effect = TouchEffect.PickFrom(Element);
if (effect?.IsDisabled ?? true)
return;
effect.Element = (VisualElement)Element;
view.Touch += OnTouch;
UpdateClickHandler();
accessibilityManager = view.Context?.GetSystemService(Context.AccessibilityService) as AccessibilityManager;
if (accessibilityManager != null)
{
accessibilityListener = new AccessibilityListener(this);
accessibilityManager.AddAccessibilityStateChangeListener(accessibilityListener);
accessibilityManager.AddTouchExplorationStateChangeListener(accessibilityListener);
}
if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop || !effect.NativeAnimation)
return;
view.Clickable = true;
view.LongClickable = true;
CreateRipple();
if (group == null)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
view.Foreground = ripple;
return;
}
rippleView = new FrameLayout(group.Context ?? throw new NullReferenceException())
{
LayoutParameters = new ViewGroup.LayoutParams(-1, -1),
Clickable = false,
Focusable = false,
Enabled = false,
};
view.LayoutChange += OnLayoutChange;
rippleView.Background = ripple;
group.AddView(rippleView);
rippleView.BringToFront();
}
protected override void OnDetached()
{
if (effect?.Element == null)
return;
try
{
if (accessibilityManager != null && accessibilityListener != null)
{
accessibilityManager.RemoveAccessibilityStateChangeListener(accessibilityListener);
accessibilityManager.RemoveTouchExplorationStateChangeListener(accessibilityListener);
accessibilityListener.Dispose();
accessibilityManager = null;
accessibilityListener = null;
}
if (view != null)
{
view.LayoutChange -= OnLayoutChange;
view.Touch -= OnTouch;
view.Click -= OnClick;
if (Build.VERSION.SdkInt >= BuildVersionCodes.M && view.Foreground == ripple)
view.Foreground = null;
}
effect.Element = null;
effect = null;
if (rippleView != null)
{
rippleView.Pressed = false;
rippleView.Background = null;
group?.RemoveView(rippleView);
rippleView.Dispose();
rippleView = null;
}
ripple?.Dispose();
ripple = null;
}
catch (ObjectDisposedException)
{
// Suppress exception
}
isHoverSupported = false;
}
protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(args);
if (args.PropertyName == TouchEffect.IsAvailableProperty.PropertyName ||
args.PropertyName == VisualElement.IsEnabledProperty.PropertyName)
{
UpdateClickHandler();
}
}
void UpdateClickHandler()
{
view.Click -= OnClick;
if (IsAccessibilityMode || (effect?.IsAvailable ?? false) && (effect?.Element?.IsEnabled ?? false))
{
view.Click += OnClick;
return;
}
}
void OnTouch(object sender, AView.TouchEventArgs e)
{
e.Handled = false;
if (effect?.IsDisabled ?? true)
return;
if (IsAccessibilityMode)
return;
switch (e.Event?.ActionMasked)
{
case MotionEventActions.Down:
OnTouchDown(e);
break;
case MotionEventActions.Up:
OnTouchUp();
break;
case MotionEventActions.Cancel:
OnTouchCancel();
break;
case MotionEventActions.Move:
OnTouchMove(sender, e);
break;
case MotionEventActions.HoverEnter:
OnHoverEnter();
break;
case MotionEventActions.HoverExit:
OnHoverExit();
break;
}
}
void OnTouchDown(AView.TouchEventArgs e)
{
_ = e.Event ?? throw new NullReferenceException();
IsCanceled = false;
startX = e.Event.GetX();
startY = e.Event.GetY();
effect?.HandleUserInteraction(TouchInteractionStatus.Started);
effect?.HandleTouch(TouchStatus.Started);
StartRipple(e.Event.GetX(), e.Event.GetY());
if (effect?.DisallowTouchThreshold > 0)
group?.Parent?.RequestDisallowInterceptTouchEvent(true);
}
void OnTouchUp()
=> HandleEnd(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled);
void OnTouchCancel()
=> HandleEnd(TouchStatus.Canceled);
void OnTouchMove(object sender, AView.TouchEventArgs e)
{
if (IsCanceled || e.Event == null)
return;
var diffX = Math.Abs(e.Event.GetX() - startX) / view.Context?.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException();
var diffY = Math.Abs(e.Event.GetY() - startY) / view.Context?.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException();
var maxDiff = Math.Max(diffX, diffY);
var disallowTouchThreshold = effect?.DisallowTouchThreshold;
if (disallowTouchThreshold > 0 && maxDiff > disallowTouchThreshold)
{
HandleEnd(TouchStatus.Canceled);
return;
}
if (sender is not AView touchView)
return;
var screenPointerCoords = new Point(view.Left + e.Event.GetX(), view.Top + e.Event.GetY());
var viewRect = new Rect(view.Left, view.Top, view.Right - view.Left, view.Bottom - view.Top);
var status = viewRect.Contains(screenPointerCoords) ? TouchStatus.Started : TouchStatus.Canceled;
if (isHoverSupported && (status == TouchStatus.Canceled && effect?.HoverStatus == HoverStatus.Entered
|| status == TouchStatus.Started && effect?.HoverStatus == HoverStatus.Exited))
effect?.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited);
if (effect?.Status != status)
{
effect?.HandleTouch(status);
if (status == TouchStatus.Started)
StartRipple(e.Event.GetX(), e.Event.GetY());
if (status == TouchStatus.Canceled)
EndRipple();
}
}
void OnHoverEnter()
{
isHoverSupported = true;
effect?.HandleHover(HoverStatus.Entered);
}
void OnHoverExit()
{
isHoverSupported = true;
effect?.HandleHover(HoverStatus.Exited);
}
void OnClick(object sender, System.EventArgs args)
{
if (effect?.IsDisabled ?? true)
return;
if (!IsAccessibilityMode)
return;
IsCanceled = false;
HandleEnd(TouchStatus.Completed);
}
void HandleEnd(TouchStatus status)
{
if (IsCanceled)
return;
IsCanceled = true;
if (effect?.DisallowTouchThreshold > 0)
group?.Parent?.RequestDisallowInterceptTouchEvent(false);
effect?.HandleTouch(status);
effect?.HandleUserInteraction(TouchInteractionStatus.Completed);
EndRipple();
}
void StartRipple(float x, float y)
{
if (effect?.IsDisabled ?? true)
return;
if (effect.CanExecute && effect.NativeAnimation)
{
UpdateRipple();
if (rippleView != null)
{
rippleView.Enabled = true;
rippleView.BringToFront();
ripple?.SetHotspot(x, y);
rippleView.Pressed = true;
}
else if (IsForegroundRippleWithTapGestureRecognizer)
{
ripple?.SetHotspot(x, y);
view.Pressed = true;
}
}
}
void EndRipple()
{
if (effect?.IsDisabled ?? true)
return;
if (rippleView != null)
{
if (rippleView.Pressed)
{
rippleView.Pressed = false;
rippleView.Enabled = false;
}
}
else if (IsForegroundRippleWithTapGestureRecognizer)
{
if (view.Pressed)
view.Pressed = false;
}
}
void CreateRipple()
{
var drawable = Build.VERSION.SdkInt >= BuildVersionCodes.M && group == null
? view?.Foreground
: view?.Background;
var isEmptyDrawable = Element is Layout || drawable == null;
if (drawable is RippleDrawable rippleDrawable && rippleDrawable.GetConstantState() is Drawable.ConstantState constantState)
ripple = (RippleDrawable)constantState.NewDrawable();
else
ripple = new RippleDrawable(GetColorStateList(), isEmptyDrawable ? null : drawable, isEmptyDrawable ? new ColorDrawable(Color.White) : null);
UpdateRipple();
}
void UpdateRipple()
{
if (effect?.IsDisabled ?? true)
return;
if (effect.NativeAnimationColor == rippleColor && effect.NativeAnimationRadius == rippleRadius)
return;
rippleColor = effect.NativeAnimationColor;
rippleRadius = effect.NativeAnimationRadius;
ripple?.SetColor(GetColorStateList());
if (Build.VERSION.SdkInt >= BuildVersionCodes.M && ripple != null)
ripple.Radius = (int)(view.Context?.Resources?.DisplayMetrics?.Density * effect?.NativeAnimationRadius ?? throw new NullReferenceException());
}
ColorStateList GetColorStateList()
{
var nativeAnimationColor = effect?.NativeAnimationColor;
nativeAnimationColor ??= defaultNativeAnimationColor;
return new ColorStateList(
new[] { new int[] { } },
new[] { (int)nativeAnimationColor.ToPlatform() });
}
void OnLayoutChange(object sender, AView.LayoutChangeEventArgs e)
{
if (sender is not AView layoutView || group == null || rippleView == null)
return;
rippleView.Right = layoutView.Width;
rippleView.Bottom = layoutView.Height;
}
sealed class AccessibilityListener : Java.Lang.Object,
AccessibilityManager.IAccessibilityStateChangeListener,
AccessibilityManager.ITouchExplorationStateChangeListener
{
PlatformTouchEffect platformTouchEffect;
internal AccessibilityListener(PlatformTouchEffect platformTouchEffect)
=> this.platformTouchEffect = platformTouchEffect;
public void OnAccessibilityStateChanged(bool enabled)
=> platformTouchEffect?.UpdateClickHandler();
public void OnTouchExplorationStateChanged(bool enabled)
=> platformTouchEffect?.UpdateClickHandler();
protected override void Dispose(bool disposing)
{
if (disposing)
platformTouchEffect = null;
base.Dispose(disposing);
}
}
}

View File

@@ -0,0 +1,690 @@
using Maui.TouchEffect.Enums;
using MauiTouchEffect.Extensions;
using Microsoft.Maui.Controls.Platform;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using System.Numerics;
using Windows.Foundation;
using Microsoft.UI.Input;
using WinUI = Microsoft.UI.Xaml.Controls;
using WinBrush = Microsoft.UI.Xaml.Media.Brush;
using WinSolidColorBrush = Microsoft.UI.Xaml.Media.SolidColorBrush;
using WinPoint = Windows.Foundation.Point;
namespace Maui.TouchEffect;
public class PlatformTouchEffect : Microsoft.Maui.Controls.Platform.PlatformEffect
{
private TouchEffect? _effect;
private FrameworkElement? _view;
private bool _isPressed;
private bool _isHovered;
private WinPoint _startPoint;
private CompositeTransform? _compositeTransform;
private Storyboard? _pressStoryboard;
private Storyboard? _releaseStoryboard;
private Storyboard? _hoverStoryboard;
private WinBrush? _originalBrush;
private double _originalOpacity;
// Pointer capture for proper touch handling
private uint? _capturedPointerId;
private PointerPoint? _startPointerPoint;
protected override void OnAttached()
{
_effect = TouchEffect.PickFrom(Element);
if (_effect?.IsDisabled ?? true)
{
return;
}
_effect.Element = (VisualElement)Element;
// Try to get the native view - Control first, then Container
var view = Control as FrameworkElement ?? Container as FrameworkElement;
if (view == null)
{
return;
}
_view = view;
// Store original values
_originalOpacity = _view.Opacity;
if (_view is WinUI.Control control)
{
_originalBrush = control.Background;
}
// Set up transform for animations
SetupTransform();
// Attach event handlers
AttachEventHandlers();
// Enable hit testing
_view.IsHitTestVisible = true;
}
protected override void OnDetached()
{
if (_effect?.Element == null || _view == null)
return;
try
{
// Clean up event handlers
DetachEventHandlers();
// Clean up animations
CleanupAnimations();
// Restore original values
if (_view != null)
{
_view.Opacity = _originalOpacity;
if (_view is WinUI.Control control && _originalBrush != null)
{
control.Background = _originalBrush;
}
_view.RenderTransform = null;
}
_effect.Element = null;
_effect = null;
_view = null;
}
catch (Exception)
{
// Suppress exceptions during cleanup
}
}
private void SetupTransform()
{
if (_view == null)
return;
_compositeTransform = new CompositeTransform
{
CenterX = _view.ActualWidth / 2,
CenterY = _view.ActualHeight / 2
};
_view.RenderTransform = _compositeTransform;
// Update transform center when size changes
_view.SizeChanged += OnViewSizeChanged;
}
private void OnViewSizeChanged(object sender, SizeChangedEventArgs e)
{
if (_compositeTransform != null)
{
_compositeTransform.CenterX = e.NewSize.Width / 2;
_compositeTransform.CenterY = e.NewSize.Height / 2;
}
}
private void AttachEventHandlers()
{
if (_view == null)
{
return;
}
// Pointer events for touch, mouse, and pen
_view.PointerPressed += OnPointerPressed;
_view.PointerReleased += OnPointerReleased;
_view.PointerCanceled += OnPointerCanceled;
_view.PointerCaptureLost += OnPointerCaptureLost;
_view.PointerMoved += OnPointerMoved;
// Hover events
_view.PointerEntered += OnPointerEntered;
_view.PointerExited += OnPointerExited;
// Keyboard support for accessibility
_view.KeyDown += OnKeyDown;
_view.KeyUp += OnKeyUp;
// Focus events
_view.GotFocus += OnGotFocus;
_view.LostFocus += OnLostFocus;
}
private void DetachEventHandlers()
{
if (_view == null)
return;
_view.PointerPressed -= OnPointerPressed;
_view.PointerReleased -= OnPointerReleased;
_view.PointerCanceled -= OnPointerCanceled;
_view.PointerCaptureLost -= OnPointerCaptureLost;
_view.PointerMoved -= OnPointerMoved;
_view.PointerEntered -= OnPointerEntered;
_view.PointerExited -= OnPointerExited;
_view.KeyDown -= OnKeyDown;
_view.KeyUp -= OnKeyUp;
_view.GotFocus -= OnGotFocus;
_view.LostFocus -= OnLostFocus;
_view.SizeChanged -= OnViewSizeChanged;
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled || _view == null)
{
return;
}
var pointer = e.GetCurrentPoint(_view);
_startPointerPoint = pointer;
_startPoint = pointer.Position;
_isPressed = true;
// Capture the pointer for this interaction
_capturedPointerId = e.Pointer.PointerId;
try
{
_view.CapturePointer(e.Pointer);
}
catch
{
// Some controls may not support pointer capture
}
// Handle the touch interaction
HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started);
// Start long press detection
StartLongPressDetection();
e.Handled = true;
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled)
return;
var pointer = e.GetCurrentPoint(_view);
// Check if this is the pointer we're tracking (or if we're pressed but lost the ID somehow)
if (_isPressed && (_capturedPointerId == null || _capturedPointerId == e.Pointer.PointerId))
{
_isPressed = false;
_capturedPointerId = null;
try
{
_view?.ReleasePointerCapture(e.Pointer);
}
catch
{
// Ignore capture release errors
}
// Determine if this is a completed tap or canceled
var distance = CalculateDistance(_startPoint, pointer.Position);
var threshold = _effect.DisallowTouchThreshold > 0 ? _effect.DisallowTouchThreshold : 20;
if (distance <= threshold)
{
HandleTouch(TouchStatus.Completed, TouchInteractionStatus.Completed);
}
else
{
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed);
}
CancelLongPressDetection();
}
e.Handled = true;
}
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
HandleCancellation();
e.Handled = true;
}
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
HandleCancellation();
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled || !_isPressed || _view == null)
return;
var pointer = e.GetCurrentPoint(_view);
var distance = CalculateDistance(_startPoint, pointer.Position);
var threshold = _effect.DisallowTouchThreshold > 0 ? _effect.DisallowTouchThreshold : 20;
// Cancel if moved too far
if (distance > threshold)
{
HandleCancellation();
}
}
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled)
return;
_isHovered = true;
_effect.HandleHover(HoverStatus.Entered);
UpdateVisualState();
}
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled)
return;
_isHovered = false;
_effect.HandleHover(HoverStatus.Exited);
UpdateVisualState();
}
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled)
return;
// Handle Space and Enter for accessibility
if (e.Key == Windows.System.VirtualKey.Space || e.Key == Windows.System.VirtualKey.Enter)
{
if (!_isPressed)
{
_isPressed = true;
HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started);
e.Handled = true;
}
}
}
private void OnKeyUp(object sender, KeyRoutedEventArgs e)
{
if (_effect == null || _effect.IsDisabled)
return;
if (e.Key == Windows.System.VirtualKey.Space || e.Key == Windows.System.VirtualKey.Enter)
{
if (_isPressed)
{
_isPressed = false;
HandleTouch(TouchStatus.Completed, TouchInteractionStatus.Completed);
e.Handled = true;
}
}
}
private void OnGotFocus(object sender, RoutedEventArgs e)
{
// Could add focus visual state here if needed
}
private void OnLostFocus(object sender, RoutedEventArgs e)
{
if (_isPressed)
{
HandleCancellation();
}
}
private void HandleCancellation()
{
if (_effect == null || !_isPressed)
return;
_isPressed = false;
_capturedPointerId = null;
_view?.ReleasePointerCaptures();
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed);
CancelLongPressDetection();
}
private void HandleTouch(TouchStatus status, TouchInteractionStatus interactionStatus)
{
_effect?.HandleTouch(status);
_effect?.HandleUserInteraction(interactionStatus);
UpdateVisualState();
}
private void UpdateVisualState()
{
if (_effect == null || _view == null)
return;
// Determine target values based on state
double targetOpacity = _originalOpacity;
double targetScale = 1.0;
double targetTranslationX = 0;
double targetTranslationY = 0;
double targetRotation = 0;
WinBrush? targetBrush = _originalBrush;
if (_isPressed && _effect.State == TouchState.Pressed)
{
targetOpacity = _effect.PressedOpacity;
targetScale = _effect.PressedScale;
targetTranslationX = _effect.PressedTranslationX;
targetTranslationY = _effect.PressedTranslationY;
targetRotation = _effect.PressedRotation;
var pressedColor = _effect.PressedBackgroundColor;
if (pressedColor != null && pressedColor != Microsoft.Maui.Graphics.Colors.Transparent)
{
targetBrush = new WinSolidColorBrush(Windows.UI.Color.FromArgb(
(byte)(pressedColor.Alpha * 255),
(byte)(pressedColor.Red * 255),
(byte)(pressedColor.Green * 255),
(byte)(pressedColor.Blue * 255)));
}
}
else if (_isHovered && _effect.HoverState == HoverState.Hovered)
{
targetOpacity = _effect.HoveredOpacity;
targetScale = _effect.HoveredScale;
targetTranslationX = _effect.HoveredTranslationX;
targetTranslationY = _effect.HoveredTranslationY;
targetRotation = _effect.HoveredRotation;
var hoveredColor = _effect.HoveredBackgroundColor;
if (hoveredColor != null && hoveredColor != Microsoft.Maui.Graphics.Colors.Transparent)
{
targetBrush = new WinSolidColorBrush(Windows.UI.Color.FromArgb(
(byte)(hoveredColor.Alpha * 255),
(byte)(hoveredColor.Red * 255),
(byte)(hoveredColor.Green * 255),
(byte)(hoveredColor.Blue * 255)));
}
}
else
{
targetOpacity = _effect.NormalOpacity;
targetScale = _effect.NormalScale;
targetTranslationX = _effect.NormalTranslationX;
targetTranslationY = _effect.NormalTranslationY;
targetRotation = _effect.NormalRotation;
var normalColor = _effect.NormalBackgroundColor;
if (normalColor != null && normalColor != Microsoft.Maui.Graphics.Colors.Transparent)
{
targetBrush = new WinSolidColorBrush(Windows.UI.Color.FromArgb(
(byte)(normalColor.Alpha * 255),
(byte)(normalColor.Red * 255),
(byte)(normalColor.Green * 255),
(byte)(normalColor.Blue * 255)));
}
}
// Apply animations
var duration = GetAnimationDuration();
if (duration > 0)
{
AnimateToState(targetOpacity, targetScale, targetTranslationX, targetTranslationY,
targetRotation, targetBrush, duration);
}
else
{
// Apply immediately without animation
_view.Opacity = targetOpacity;
if (_compositeTransform != null)
{
_compositeTransform.ScaleX = targetScale;
_compositeTransform.ScaleY = targetScale;
_compositeTransform.TranslateX = targetTranslationX;
_compositeTransform.TranslateY = targetTranslationY;
_compositeTransform.Rotation = targetRotation;
}
if (_view is WinUI.Control control && targetBrush != null)
{
control.Background = targetBrush;
}
}
}
private void AnimateToState(double opacity, double scale, double translateX, double translateY,
double rotation, WinBrush? brush, int durationMs)
{
if (_view == null || _compositeTransform == null)
return;
var storyboard = new Storyboard();
var duration = new Duration(TimeSpan.FromMilliseconds(durationMs));
var easing = GetEasingFunction();
// Opacity animation
var opacityAnimation = new DoubleAnimation
{
To = opacity,
Duration = duration,
EasingFunction = easing
};
Storyboard.SetTarget(opacityAnimation, _view);
Storyboard.SetTargetProperty(opacityAnimation, "Opacity");
storyboard.Children.Add(opacityAnimation);
// Scale X animation
var scaleXAnimation = new DoubleAnimation
{
To = scale,
Duration = duration,
EasingFunction = easing
};
Storyboard.SetTarget(scaleXAnimation, _compositeTransform);
Storyboard.SetTargetProperty(scaleXAnimation, "ScaleX");
storyboard.Children.Add(scaleXAnimation);
// Scale Y animation
var scaleYAnimation = new DoubleAnimation
{
To = scale,
Duration = duration,
EasingFunction = easing
};
Storyboard.SetTarget(scaleYAnimation, _compositeTransform);
Storyboard.SetTargetProperty(scaleYAnimation, "ScaleY");
storyboard.Children.Add(scaleYAnimation);
// TranslateX animation
var translateXAnimation = new DoubleAnimation
{
To = translateX,
Duration = duration,
EasingFunction = easing
};
Storyboard.SetTarget(translateXAnimation, _compositeTransform);
Storyboard.SetTargetProperty(translateXAnimation, "TranslateX");
storyboard.Children.Add(translateXAnimation);
// TranslateY animation
var translateYAnimation = new DoubleAnimation
{
To = translateY,
Duration = duration,
EasingFunction = easing
};
Storyboard.SetTarget(translateYAnimation, _compositeTransform);
Storyboard.SetTargetProperty(translateYAnimation, "TranslateY");
storyboard.Children.Add(translateYAnimation);
// Rotation animation
var rotationAnimation = new DoubleAnimation
{
To = rotation,
Duration = duration,
EasingFunction = easing
};
Storyboard.SetTarget(rotationAnimation, _compositeTransform);
Storyboard.SetTargetProperty(rotationAnimation, "Rotation");
storyboard.Children.Add(rotationAnimation);
// Background color animation if control supports it
if (_view is WinUI.Control control && brush is WinSolidColorBrush solidBrush)
{
control.Background = brush;
}
// Handle pulse/ripple count
if (_effect?.PulseCount != 0)
{
var pulseCount = _effect?.PulseCount ?? 0;
if (pulseCount < 0)
{
storyboard.RepeatBehavior = RepeatBehavior.Forever;
}
else if (pulseCount > 1)
{
storyboard.RepeatBehavior = new RepeatBehavior(pulseCount);
storyboard.AutoReverse = true;
}
}
storyboard.Begin();
}
private int GetAnimationDuration()
{
if (_effect == null)
return 0;
// Determine which animation duration to use
if (_isPressed)
{
return _effect.PressedAnimationDuration > 0
? _effect.PressedAnimationDuration
: _effect.AnimationDuration;
}
else if (_isHovered)
{
return _effect.HoveredAnimationDuration > 0
? _effect.HoveredAnimationDuration
: _effect.AnimationDuration;
}
else
{
return _effect.NormalAnimationDuration > 0
? _effect.NormalAnimationDuration
: _effect.AnimationDuration;
}
}
private EasingFunctionBase? GetEasingFunction()
{
if (_effect == null)
return null;
// Get the appropriate easing
Microsoft.Maui.Easing? mauiEasing = null;
if (_isPressed)
{
mauiEasing = _effect.PressedAnimationEasing ?? _effect.AnimationEasing;
}
else if (_isHovered)
{
mauiEasing = _effect.HoveredAnimationEasing ?? _effect.AnimationEasing;
}
else
{
mauiEasing = _effect.NormalAnimationEasing ?? _effect.AnimationEasing;
}
// Convert MAUI easing to WinUI easing
if (mauiEasing == null)
return null;
// Map common easings
if (mauiEasing == Microsoft.Maui.Easing.Linear)
return null; // Linear is default, no easing function needed
if (mauiEasing == Microsoft.Maui.Easing.CubicIn)
return new CubicEase { EasingMode = EasingMode.EaseIn };
if (mauiEasing == Microsoft.Maui.Easing.CubicOut)
return new CubicEase { EasingMode = EasingMode.EaseOut };
if (mauiEasing == Microsoft.Maui.Easing.CubicInOut)
return new CubicEase { EasingMode = EasingMode.EaseInOut };
if (mauiEasing == Microsoft.Maui.Easing.BounceIn)
return new BounceEase { EasingMode = EasingMode.EaseIn };
if (mauiEasing == Microsoft.Maui.Easing.BounceOut)
return new BounceEase { EasingMode = EasingMode.EaseOut };
if (mauiEasing == Microsoft.Maui.Easing.SinIn)
return new SineEase { EasingMode = EasingMode.EaseIn };
if (mauiEasing == Microsoft.Maui.Easing.SinOut)
return new SineEase { EasingMode = EasingMode.EaseOut };
if (mauiEasing == Microsoft.Maui.Easing.SinInOut)
return new SineEase { EasingMode = EasingMode.EaseInOut };
// Default to cubic for unrecognized easings
return new CubicEase { EasingMode = EasingMode.EaseInOut };
}
private double CalculateDistance(WinPoint p1, WinPoint p2)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
private void CleanupAnimations()
{
_pressStoryboard?.Stop();
_releaseStoryboard?.Stop();
_hoverStoryboard?.Stop();
_pressStoryboard = null;
_releaseStoryboard = null;
_hoverStoryboard = null;
}
#region Long Press Support
private System.Threading.CancellationTokenSource? _longPressCts;
private async void StartLongPressDetection()
{
if (_effect == null || _effect.LongPressCommand == null)
return;
_longPressCts?.Cancel();
_longPressCts = new System.Threading.CancellationTokenSource();
try
{
var duration = _effect.LongPressDuration > 0 ? _effect.LongPressDuration : 500;
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
}
}
private void CancelLongPressDetection()
{
_longPressCts?.Cancel();
_longPressCts?.Dispose();
_longPressCts = null;
}
#endregion
}

View File

@@ -0,0 +1,311 @@
using CoreGraphics;
using Foundation;
using Maui.TouchEffect.Enums;
using MauiTouchEffect.Extensions;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Platform;
using UIKit;
namespace Maui.TouchEffect;
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);
if (effect?.IsDisabled ?? true)
return;
effect.Element = (VisualElement)Element;
if (View == null)
return;
touchGesture = new TouchUITapGestureRecognizer(effect);
if (View is UIButton button)
{
button.AllTouchEvents += PreventButtonHighlight;
((TouchUITapGestureRecognizer)touchGesture).IsButton = true;
}
View.AddGestureRecognizer(touchGesture);
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
hoverGesture = new UIHoverGestureRecognizer(OnHover);
View.AddGestureRecognizer(hoverGesture);
}
View.UserInteractionEnabled = true;
}
protected override void OnDetached()
{
if (View == null || (effect != null && effect.IsDisabled))
return;
if (touchGesture != null)
{
View.RemoveGestureRecognizer(touchGesture);
if (View is UIButton button)
{
button.AllTouchEvents -= PreventButtonHighlight;
}
touchGesture.Dispose();
touchGesture = null;
}
if (hoverGesture != null)
{
View.RemoveGestureRecognizer(hoverGesture);
hoverGesture.Dispose();
hoverGesture = null;
}
effect = null;
}
private void OnHover()
{
if (effect == null || effect.IsDisabled)
return;
switch (hoverGesture?.State)
{
case UIGestureRecognizerState.Began:
case UIGestureRecognizerState.Changed:
effect.HandleHover(HoverStatus.Entered);
break;
case UIGestureRecognizerState.Ended:
effect.HandleHover(HoverStatus.Exited);
break;
}
}
private void PreventButtonHighlight(object? sender, EventArgs args)
{
if (sender is not UIButton button)
{
throw new ArgumentException($"{nameof(sender)} must be Type {nameof(UIButton)}", nameof(sender));
}
button.Highlighted = false;
}
}
internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer
{
private TouchEffect touchEffect;
private float? defaultRadius;
private float? defaultShadowRadius;
private float? defaultShadowOpacity;
private CGPoint? startPoint;
public TouchUITapGestureRecognizer(TouchEffect touchEffect)
{
this.touchEffect = touchEffect;
CancelsTouchesInView = false;
Delegate = new TouchUITapGestureRecognizerDelegate();
}
public bool IsCanceled { get; set; } = true;
public bool IsButton { get; set; }
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
if (touchEffect?.IsDisabled ?? true)
{
return;
}
IsCanceled = false;
startPoint = GetTouchPoint(touches);
HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started).SafeFireAndForget();
base.TouchesBegan(touches, evt);
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
if (touchEffect?.IsDisabled ?? true)
{
return;
}
HandleTouch(touchEffect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
IsCanceled = true;
base.TouchesEnded(touches, evt);
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
if (touchEffect?.IsDisabled ?? true)
{
return;
}
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
IsCanceled = true;
base.TouchesCancelled(touches, evt);
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
if (touchEffect?.IsDisabled ?? true)
{
return;
}
var disallowTouchThreshold = touchEffect.DisallowTouchThreshold;
var point = GetTouchPoint(touches);
if (point != null && startPoint != null && disallowTouchThreshold > 0)
{
var diffX = Math.Abs(point.Value.X - startPoint.Value.X);
var diffY = Math.Abs(point.Value.Y - startPoint.Value.Y);
var maxDiff = Math.Max(diffX, diffY);
if (maxDiff > disallowTouchThreshold)
{
HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
IsCanceled = true;
base.TouchesMoved(touches, evt);
return;
}
}
var status = point != null && View?.Bounds.Contains(point.Value) is true
? TouchStatus.Started
: TouchStatus.Canceled;
if (touchEffect?.Status != status)
{
HandleTouch(status).SafeFireAndForget();
}
if (status == TouchStatus.Canceled)
{
IsCanceled = true;
}
base.TouchesMoved(touches, evt);
}
public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? interactionStatus = null)
{
if (IsCanceled || touchEffect == null)
{
return;
}
if (touchEffect?.IsDisabled ?? true)
{
return;
}
var canExecuteAction = touchEffect.CanExecute;
if (interactionStatus == TouchInteractionStatus.Started)
{
touchEffect?.HandleUserInteraction(TouchInteractionStatus.Started);
interactionStatus = null;
}
touchEffect?.HandleTouch(status);
if (interactionStatus.HasValue)
{
touchEffect?.HandleUserInteraction(interactionStatus.Value);
}
if (touchEffect == null || touchEffect.Element is null || (!touchEffect.NativeAnimation && !IsButton) || (!canExecuteAction && status == TouchStatus.Started))
{
return;
}
var color = touchEffect.NativeAnimationColor;
var radius = touchEffect.NativeAnimationRadius;
var shadowRadius = touchEffect.NativeAnimationShadowRadius;
var isStarted = status == TouchStatus.Started;
defaultRadius = (float?) (defaultRadius ?? View.Layer.CornerRadius);
defaultShadowRadius = (float?) (defaultShadowRadius ?? View.Layer.ShadowRadius);
defaultShadowOpacity ??= View.Layer.ShadowOpacity;
var tcs = new TaskCompletionSource<UIViewAnimatingPosition>();
UIViewPropertyAnimator.CreateRunningPropertyAnimator(.2, 0, UIViewAnimationOptions.AllowUserInteraction,
() =>
{
if (color is null)
{
View.Layer.Opacity = isStarted ? 0.5f : (float) touchEffect.Element.Opacity;
}
else
{
var backgroundColor = touchEffect.Element.BackgroundColor ?? Colors.Transparent;
View.Layer.BackgroundColor = (isStarted ? color : backgroundColor).ToCGColor();
}
View.Layer.CornerRadius = isStarted ? radius : defaultRadius.GetValueOrDefault();
if (shadowRadius >= 0)
{
View.Layer.ShadowRadius = isStarted ? shadowRadius : defaultShadowRadius.GetValueOrDefault();
View.Layer.ShadowOpacity = isStarted ? 0.7f : defaultShadowOpacity.GetValueOrDefault();
}
}, endPos => tcs.SetResult(endPos));
await tcs.Task;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Delegate.Dispose();
}
base.Dispose(disposing);
}
private CGPoint? GetTouchPoint(NSSet touches)
{
return (touches?.AnyObject as UITouch)?.LocationInView(View);
}
private class TouchUITapGestureRecognizerDelegate : UIGestureRecognizerDelegate
{
public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
{
if (gestureRecognizer is TouchUITapGestureRecognizer touchGesture && otherGestureRecognizer is UIPanGestureRecognizer &&
otherGestureRecognizer.State == UIGestureRecognizerState.Began)
{
touchGesture.HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget();
touchGesture.IsCanceled = true;
}
return true;
}
public override bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch)
{
if (recognizer.View.IsDescendantOfView(touch.View))
{
return true;
}
return recognizer.View.Subviews.Any(view => view == touch.View);
}
}
}

View File

@@ -0,0 +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")]

View File

@@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.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>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageOutputPath>C:\Users\logik\Dropbox\Nugets</PackageOutputPath>
<Authors>David H Friedel Jr</Authors>
<Owners>MarketAlly</Owners>
<Company>MarketAlly</Company>
<NeutralLanguage>en</NeutralLanguage>
<RepositoryUrl>https://github.com/MarketAlly/TouchEffect</RepositoryUrl>
<PackageProjectUrl>https://github.com/MarketAlly/TouchEffect</PackageProjectUrl>
<AssemblyName>MarketAlly.TouchEffect.Maui</AssemblyName>
<RootNamespace>MarketAlly.TouchEffect.Maui</RootNamespace>
<PackageId>MarketAlly.TouchEffect.Maui</PackageId>
<Summary>Advanced touch effects for .NET MAUI with fluent API, presets, and Windows support</Summary>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageTags>maui,touch,effect,toucheffect,animation,gesture,interaction,fluent,builder,presets,windows,android,ios</PackageTags>
<Title>MarketAlly.TouchEffect.Maui</Title>
<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>
<PackageIcon>icon.png</PackageIcon>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">13.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
<None Include="..\..\LICENSE" Pack="true" PackagePath="\"/>
<None Include="icon.png">
<Pack>true</Pack>
<PackagePath>\</PackagePath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>true</Visible>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.82" />
</ItemGroup>
</Project>

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,428 @@
using System.Windows.Input;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Maui.TouchEffect;
namespace Maui.TouchEffect;
/// <summary>
/// Fluent builder for configuring TouchEffect with a clean API.
/// </summary>
public class TouchEffectBuilder
{
private readonly VisualElement element;
private ICommand? command;
private object? commandParameter;
private ICommand? longPressCommand;
private object? longPressCommandParameter;
private int longPressDuration = 500; // Default long press duration
// Visual properties
private double pressedOpacity = 1.0;
private double pressedScale = 1.0;
private Color? pressedBackgroundColor;
private double hoveredOpacity = 1.0;
private double hoveredScale = 1.0;
private Color? hoveredBackgroundColor;
private double normalOpacity = 1.0;
private double normalScale = 1.0;
private Color? normalBackgroundColor;
// Animation properties
private int animationDuration = 150;
private Easing? animationEasing;
private int pressedAnimationDuration;
private Easing? pressedAnimationEasing;
private int hoveredAnimationDuration;
private Easing? hoveredAnimationEasing;
private int pulseCount = 1;
// Native animation properties
private bool nativeAnimation;
private Color? nativeAnimationColor;
private int nativeAnimationRadius = -1;
// Other properties
private bool? isToggled;
private int disallowTouchThreshold = 10;
private bool isAvailable = true;
/// <summary>
/// Creates a new TouchEffectBuilder for the specified element.
/// </summary>
/// <param name="element">The visual element to apply the effect to.</param>
public TouchEffectBuilder(VisualElement element)
{
this.element = element ?? throw new ArgumentNullException(nameof(element));
}
/// <summary>
/// Creates a builder for the specified element.
/// </summary>
public static TouchEffectBuilder For(VisualElement element) => new(element);
#region Command Configuration
/// <summary>
/// Sets the command to execute on tap.
/// </summary>
public TouchEffectBuilder WithCommand(ICommand command, object? parameter = null)
{
this.command = command;
this.commandParameter = parameter;
return this;
}
/// <summary>
/// Sets the command to execute on long press.
/// </summary>
public TouchEffectBuilder WithLongPressCommand(ICommand command, object? parameter = null)
{
this.longPressCommand = command;
this.longPressCommandParameter = parameter;
return this;
}
/// <summary>
/// Sets the duration for long press detection.
/// </summary>
public TouchEffectBuilder WithLongPressDuration(int milliseconds)
{
this.longPressDuration = milliseconds;
return this;
}
#endregion
#region Visual Configuration
/// <summary>
/// Configures the pressed state appearance.
/// </summary>
public TouchEffectBuilder WithPressedState(double? opacity = null, double? scale = null, Color? backgroundColor = null)
{
if (opacity.HasValue) pressedOpacity = opacity.Value;
if (scale.HasValue) pressedScale = scale.Value;
if (backgroundColor != null) pressedBackgroundColor = backgroundColor;
return this;
}
/// <summary>
/// Configures the hovered state appearance.
/// </summary>
public TouchEffectBuilder WithHoveredState(double? opacity = null, double? scale = null, Color? backgroundColor = null)
{
if (opacity.HasValue) hoveredOpacity = opacity.Value;
if (scale.HasValue) hoveredScale = scale.Value;
if (backgroundColor != null) hoveredBackgroundColor = backgroundColor;
return this;
}
/// <summary>
/// Configures the normal state appearance.
/// </summary>
public TouchEffectBuilder WithNormalState(double? opacity = null, double? scale = null, Color? backgroundColor = null)
{
if (opacity.HasValue) normalOpacity = opacity.Value;
if (scale.HasValue) normalScale = scale.Value;
if (backgroundColor != null) normalBackgroundColor = backgroundColor;
return this;
}
/// <summary>
/// Sets only the pressed opacity.
/// </summary>
public TouchEffectBuilder WithPressedOpacity(double opacity)
{
this.pressedOpacity = opacity;
return this;
}
/// <summary>
/// Sets only the pressed scale.
/// </summary>
public TouchEffectBuilder WithPressedScale(double scale)
{
this.pressedScale = scale;
return this;
}
/// <summary>
/// Sets only the pressed background color.
/// </summary>
public TouchEffectBuilder WithPressedBackgroundColor(Color color)
{
this.pressedBackgroundColor = color;
return this;
}
/// <summary>
/// Sets only the hovered scale.
/// </summary>
public TouchEffectBuilder WithHoveredScale(double scale)
{
this.hoveredScale = scale;
return this;
}
#endregion
#region Animation Configuration
/// <summary>
/// Sets the animation duration and optional easing.
/// </summary>
public TouchEffectBuilder WithAnimation(int duration, Easing? easing = null)
{
this.animationDuration = duration;
this.animationEasing = easing;
return this;
}
/// <summary>
/// Sets animation for pressed state.
/// </summary>
public TouchEffectBuilder WithPressedAnimation(int duration, Easing? easing = null)
{
this.pressedAnimationDuration = duration;
this.pressedAnimationEasing = easing;
return this;
}
/// <summary>
/// Sets animation for hovered state.
/// </summary>
public TouchEffectBuilder WithHoveredAnimation(int duration, Easing? easing = null)
{
this.hoveredAnimationDuration = duration;
this.hoveredAnimationEasing = easing;
return this;
}
/// <summary>
/// Sets the pulse/ripple count.
/// </summary>
public TouchEffectBuilder WithPulse(int count)
{
this.pulseCount = count;
return this;
}
/// <summary>
/// Enables infinite pulse.
/// </summary>
public TouchEffectBuilder WithInfinitePulse()
{
this.pulseCount = -1;
return this;
}
#endregion
#region Native Animation Configuration
/// <summary>
/// Enables native platform animations.
/// </summary>
public TouchEffectBuilder WithNativeAnimation(Color? color = null, int radius = -1)
{
this.nativeAnimation = true;
this.nativeAnimationColor = color;
this.nativeAnimationRadius = radius;
return this;
}
#endregion
#region Toggle Configuration
/// <summary>
/// Enables toggle behavior.
/// </summary>
public TouchEffectBuilder AsToggle(bool initialState = false)
{
this.isToggled = initialState;
return this;
}
#endregion
#region Other Configuration
/// <summary>
/// Sets the movement threshold for touch cancellation.
/// </summary>
public TouchEffectBuilder WithDisallowThreshold(int pixels)
{
this.disallowTouchThreshold = pixels;
return this;
}
/// <summary>
/// Disables the effect.
/// </summary>
public TouchEffectBuilder Disable()
{
this.isAvailable = false;
return this;
}
#endregion
#region Preset Methods
/// <summary>
/// Applies a button preset with standard press effect.
/// </summary>
public TouchEffectBuilder AsButton()
{
return WithPressedOpacity(0.7)
.WithAnimation(100, Easing.CubicOut);
}
/// <summary>
/// Applies a card preset with subtle scale effect.
/// </summary>
public TouchEffectBuilder AsCard()
{
return WithPressedScale(0.97)
.WithAnimation(150, Easing.CubicInOut)
.WithHoveredScale(1.02);
}
/// <summary>
/// Applies a list item preset with background color change.
/// </summary>
public TouchEffectBuilder AsListItem()
{
return WithPressedBackgroundColor(Colors.LightGray.WithAlpha(0.3f))
.WithAnimation(50);
}
/// <summary>
/// Applies a floating action button preset.
/// </summary>
public TouchEffectBuilder AsFloatingActionButton()
{
return WithPressedScale(0.9)
.WithPressedOpacity(0.8)
.WithAnimation(100, Easing.SpringOut)
.WithNativeAnimation();
}
#endregion
/// <summary>
/// Builds and applies the TouchEffect to the element.
/// </summary>
public VisualElement Build()
{
// Set all the properties
TouchEffect.SetIsAvailable(this.element, this.isAvailable);
if (this.command != null)
TouchEffect.SetCommand(this.element, this.command);
if (this.commandParameter != null)
TouchEffect.SetCommandParameter(this.element, this.commandParameter);
if (this.longPressCommand != null)
TouchEffect.SetLongPressCommand(this.element, this.longPressCommand);
if (this.longPressCommandParameter != null)
TouchEffect.SetLongPressCommandParameter(this.element, this.longPressCommandParameter);
TouchEffect.SetLongPressDuration(this.element, this.longPressDuration);
// Visual properties
TouchEffect.SetPressedOpacity(this.element, this.pressedOpacity);
TouchEffect.SetPressedScale(this.element, this.pressedScale);
if (this.pressedBackgroundColor != null)
TouchEffect.SetPressedBackgroundColor(this.element, this.pressedBackgroundColor);
TouchEffect.SetHoveredOpacity(this.element, this.hoveredOpacity);
TouchEffect.SetHoveredScale(this.element, this.hoveredScale);
if (this.hoveredBackgroundColor != null)
TouchEffect.SetHoveredBackgroundColor(this.element, this.hoveredBackgroundColor);
TouchEffect.SetNormalOpacity(this.element, this.normalOpacity);
TouchEffect.SetNormalScale(this.element, this.normalScale);
if (this.normalBackgroundColor != null)
TouchEffect.SetNormalBackgroundColor(this.element, this.normalBackgroundColor);
// Animation properties
TouchEffect.SetAnimationDuration(this.element, this.animationDuration);
if (this.animationEasing != null)
TouchEffect.SetAnimationEasing(this.element, this.animationEasing);
if (this.pressedAnimationDuration > 0)
TouchEffect.SetPressedAnimationDuration(this.element, this.pressedAnimationDuration);
if (this.pressedAnimationEasing != null)
TouchEffect.SetPressedAnimationEasing(this.element, this.pressedAnimationEasing);
if (this.hoveredAnimationDuration > 0)
TouchEffect.SetHoveredAnimationDuration(this.element, this.hoveredAnimationDuration);
if (this.hoveredAnimationEasing != null)
TouchEffect.SetHoveredAnimationEasing(this.element, this.hoveredAnimationEasing);
TouchEffect.SetPulseCount(this.element, this.pulseCount);
// Native animation
TouchEffect.SetNativeAnimation(this.element, this.nativeAnimation);
if (this.nativeAnimationColor != null)
TouchEffect.SetNativeAnimationColor(this.element, this.nativeAnimationColor);
TouchEffect.SetNativeAnimationRadius(this.element, this.nativeAnimationRadius);
// Other properties
if (this.isToggled.HasValue)
TouchEffect.SetIsToggled(this.element, this.isToggled);
TouchEffect.SetDisallowTouchThreshold(this.element, this.disallowTouchThreshold);
return this.element;
}
/// <summary>
/// Builds and applies the effect, then returns the builder for further configuration.
/// </summary>
public TouchEffectBuilder Apply()
{
Build();
return this;
}
}
/// <summary>
/// Extension methods for applying TouchEffectBuilder to elements.
/// </summary>
public static class TouchEffectBuilderExtensions
{
/// <summary>
/// Creates a TouchEffectBuilder for this element.
/// </summary>
public static TouchEffectBuilder ConfigureTouchEffect(this VisualElement element)
{
return new TouchEffectBuilder(element);
}
/// <summary>
/// Applies a button touch effect preset.
/// </summary>
public static VisualElement WithButtonEffect(this VisualElement element, ICommand? command = null)
{
var builder = new TouchEffectBuilder(element).AsButton();
if (command != null)
builder.WithCommand(command);
return builder.Build();
}
/// <summary>
/// Applies a card touch effect preset.
/// </summary>
public static VisualElement WithCardEffect(this VisualElement element, ICommand? command = null)
{
var builder = new TouchEffectBuilder(element).AsCard();
if (command != null)
builder.WithCommand(command);
return builder.Build();
}
}

View File

@@ -0,0 +1,215 @@
namespace Maui.TouchEffect;
/// <summary>
/// Constants used throughout the TouchEffect library.
/// </summary>
public static class TouchEffectConstants
{
/// <summary>
/// Animation constants
/// </summary>
public static class Animation
{
/// <summary>
/// Default animation progress delay in milliseconds for smooth transitions.
/// </summary>
public const int DefaultProgressDelay = 10;
/// <summary>
/// Default animation duration when not specified (instant).
/// </summary>
public const int DefaultDuration = 0;
/// <summary>
/// Maximum recommended animation duration for responsive feel.
/// </summary>
public const int MaxRecommendedDuration = 300;
/// <summary>
/// Frame rate target for animations (60fps).
/// </summary>
public const int TargetFrameRate = 60;
/// <summary>
/// Throttle interval for property changes in milliseconds.
/// </summary>
public const int ThrottleInterval = 16; // ~60fps
}
/// <summary>
/// Default visual property values
/// </summary>
public static class Defaults
{
/// <summary>
/// Default opacity for all states.
/// </summary>
public const double Opacity = 1.0;
/// <summary>
/// Default scale for all states.
/// </summary>
public const double Scale = 1.0;
/// <summary>
/// Default translation X offset.
/// </summary>
public const double TranslationX = 0.0;
/// <summary>
/// Default translation Y offset.
/// </summary>
public const double TranslationY = 0.0;
/// <summary>
/// Default rotation angle in degrees.
/// </summary>
public const double Rotation = 0.0;
/// <summary>
/// Default long press duration in milliseconds.
/// </summary>
public const int LongPressDuration = 500;
/// <summary>
/// Default touch movement threshold in pixels.
/// </summary>
public const int DisallowTouchThreshold = 0;
/// <summary>
/// Default native animation radius (-1 means use platform default).
/// </summary>
public const int NativeAnimationRadius = -1;
/// <summary>
/// Default native animation shadow radius.
/// </summary>
public const int NativeAnimationShadowRadius = -1;
/// <summary>
/// Default pulse count (0 means no pulse).
/// </summary>
public const int PulseCount = 0;
}
/// <summary>
/// Platform-specific constants
/// </summary>
public static class Platform
{
/// <summary>
/// Android specific constants
/// </summary>
public static class Android
{
/// <summary>
/// Minimum API level for ripple effect support.
/// </summary>
public const int MinRippleApiLevel = 21; // Lollipop
/// <summary>
/// Default ripple alpha value.
/// </summary>
public const byte DefaultRippleAlpha = 64;
/// <summary>
/// Default ripple color RGB values.
/// </summary>
public const byte DefaultRippleColor = 128;
}
/// <summary>
/// iOS specific constants
/// </summary>
public static class iOS
{
/// <summary>
/// Minimum iOS version for hover gesture support.
/// </summary>
public const int MinHoverGestureVersion = 13;
}
/// <summary>
/// Windows specific constants
/// </summary>
public static class Windows
{
/// <summary>
/// Default pointer movement threshold in pixels.
/// </summary>
public const double DefaultMovementThreshold = 20.0;
/// <summary>
/// Maximum animation timeout in milliseconds.
/// </summary>
public const int MaxAnimationTimeout = 10000;
}
}
/// <summary>
/// Visual state names for use with VisualStateManager
/// </summary>
public static class VisualStates
{
/// <summary>
/// Visual state for unpressed/normal state.
/// </summary>
public const string Unpressed = "Unpressed";
/// <summary>
/// Visual state for pressed state.
/// </summary>
public const string Pressed = "Pressed";
/// <summary>
/// Visual state for hovered state.
/// </summary>
public const string Hovered = "Hovered";
/// <summary>
/// Visual state for disabled state.
/// </summary>
public const string Disabled = "Disabled";
/// <summary>
/// Visual state for focused state.
/// </summary>
public const string Focused = "Focused";
}
/// <summary>
/// Preset animation durations for common scenarios
/// </summary>
public static class PresetDurations
{
/// <summary>
/// Instant feedback with no animation.
/// </summary>
public const int Instant = 0;
/// <summary>
/// Very fast animation for responsive buttons.
/// </summary>
public const int VeryFast = 50;
/// <summary>
/// Fast animation for most interactive elements.
/// </summary>
public const int Fast = 100;
/// <summary>
/// Normal animation speed for standard interactions.
/// </summary>
public const int Normal = 150;
/// <summary>
/// Slow animation for emphasis or special effects.
/// </summary>
public const int Slow = 250;
/// <summary>
/// Very slow animation for dramatic effects.
/// </summary>
public const int VerySlow = 500;
}
}

View File

@@ -0,0 +1,363 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace Maui.TouchEffect;
/// <summary>
/// Predefined TouchEffect configurations for common UI patterns.
/// </summary>
public static class TouchEffectPresets
{
/// <summary>
/// Standard button effect with opacity change.
/// </summary>
public static class Button
{
/// <summary>
/// Standard button with opacity feedback.
/// </summary>
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
/// <summary>
/// Primary button with scale and opacity feedback.
/// </summary>
public static void ApplyPrimary(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
/// <summary>
/// Secondary button with subtle feedback.
/// </summary>
public static void ApplySecondary(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.6);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
}
/// <summary>
/// Text button with minimal feedback.
/// </summary>
public static void ApplyText(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.5);
TouchEffect.SetAnimationDuration(element, 25); // Instant
}
}
/// <summary>
/// Card effect with scale animation.
/// </summary>
public static class Card
{
/// <summary>
/// Standard card with subtle scale effect.
/// </summary>
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.97);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
/// <summary>
/// Elevated card with shadow-like effect.
/// </summary>
public static void ApplyElevated(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetPressedOpacity(element, 0.9);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
TouchEffect.SetHoveredScale(element, 1.02);
TouchEffect.SetHoveredAnimationDuration(element, 200); // Slow
}
/// <summary>
/// Interactive card with hover support.
/// </summary>
public static void ApplyInteractive(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.98);
TouchEffect.SetHoveredScale(element, 1.01);
TouchEffect.SetHoveredBackgroundColor(element, Colors.Gray.WithAlpha(0.1f));
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
}
/// <summary>
/// List item effects.
/// </summary>
public static class ListItem
{
/// <summary>
/// Standard list item with background highlight.
/// </summary>
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedBackgroundColor(element, Colors.Gray.WithAlpha(0.2f));
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
}
/// <summary>
/// Selectable list item with toggle behavior.
/// </summary>
public static void ApplySelectable(VisualElement element)
{
TouchEffect.SetIsToggled(element, false);
TouchEffect.SetPressedBackgroundColor(element, Colors.Blue.WithAlpha(0.3f));
TouchEffect.SetAnimationDuration(element, 100); // Fast
}
/// <summary>
/// Swipeable list item with scale feedback.
/// </summary>
public static void ApplySwipeable(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.98);
TouchEffect.SetPressedBackgroundColor(element, Colors.Gray.WithAlpha(0.1f));
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
}
}
/// <summary>
/// Icon button effects.
/// </summary>
public static class IconButton
{
/// <summary>
/// Standard icon button with scale effect.
/// </summary>
public static void Apply(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.85);
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.SpringOut);
}
/// <summary>
/// Floating action button effect.
/// </summary>
public static void ApplyFloatingAction(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.9);
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.SpringOut);
TouchEffect.SetNativeAnimation(element, true);
}
/// <summary>
/// Toolbar icon effect.
/// </summary>
public static void ApplyToolbar(VisualElement element)
{
TouchEffect.SetPressedOpacity(element, 0.5);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
}
}
/// <summary>
/// Switch/Toggle effects.
/// </summary>
public static class Toggle
{
/// <summary>
/// Standard toggle with color change.
/// </summary>
public static void Apply(VisualElement element)
{
TouchEffect.SetIsToggled(element, false);
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
/// <summary>
/// Checkbox-style toggle.
/// </summary>
public static void ApplyCheckbox(VisualElement element)
{
TouchEffect.SetIsToggled(element, false);
TouchEffect.SetPressedScale(element, 0.9);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.BounceOut);
}
}
/// <summary>
/// Image effects.
/// </summary>
public static class Image
{
/// <summary>
/// Thumbnail image with scale effect.
/// </summary>
public static void ApplyThumbnail(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.95);
TouchEffect.SetHoveredScale(element, 1.05);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
/// <summary>
/// Gallery image with zoom effect.
/// </summary>
public static void ApplyGallery(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.98);
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetHoveredScale(element, 1.1);
TouchEffect.SetAnimationDuration(element, 200); // Slow
TouchEffect.SetAnimationEasing(element, Easing.CubicInOut);
}
/// <summary>
/// Avatar image with subtle feedback.
/// </summary>
public static void ApplyAvatar(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.92);
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetAnimationDuration(element, 100); // Fast
TouchEffect.SetAnimationEasing(element, Easing.CubicOut);
}
}
/// <summary>
/// Native platform effects.
/// </summary>
public static class Native
{
/// <summary>
/// Android ripple effect.
/// </summary>
public static void ApplyRipple(VisualElement element, Color? color = null)
{
TouchEffect.SetNativeAnimation(element, true);
if (color != null)
TouchEffect.SetNativeAnimationColor(element, color);
}
/// <summary>
/// iOS haptic feedback effect.
/// </summary>
public static void ApplyHaptic(VisualElement element)
{
TouchEffect.SetNativeAnimation(element, true);
TouchEffect.SetPressedOpacity(element, 0.8);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
}
}
/// <summary>
/// Special effects.
/// </summary>
public static class Special
{
/// <summary>
/// Pulse effect with repeating animation.
/// </summary>
public static void ApplyPulse(VisualElement element, int count = 3)
{
TouchEffect.SetPressedScale(element, 1.1);
TouchEffect.SetPressedOpacity(element, 0.7);
TouchEffect.SetPulseCount(element, count);
TouchEffect.SetAnimationDuration(element, 150); // Normal
TouchEffect.SetAnimationEasing(element, Easing.SinInOut);
}
/// <summary>
/// Bounce effect with spring animation.
/// </summary>
public static void ApplyBounce(VisualElement element)
{
TouchEffect.SetPressedScale(element, 0.8);
TouchEffect.SetAnimationDuration(element, 200); // Slow
TouchEffect.SetAnimationEasing(element, Easing.SpringOut);
}
/// <summary>
/// Shake effect with rotation.
/// </summary>
public static void ApplyShake(VisualElement element)
{
TouchEffect.SetPressedRotation(element, 5);
TouchEffect.SetPulseCount(element, 2);
TouchEffect.SetAnimationDuration(element, 50); // Very Fast
TouchEffect.SetAnimationEasing(element, Easing.BounceOut);
}
/// <summary>
/// Disabled state with no interaction.
/// </summary>
public static void ApplyDisabled(VisualElement element)
{
TouchEffect.SetIsAvailable(element, false);
element.Opacity = 0.5;
element.InputTransparent = true;
}
}
}
/// <summary>
/// Extension methods for applying presets.
/// </summary>
public static class TouchEffectPresetExtensions
{
/// <summary>
/// Applies the standard button preset.
/// </summary>
public static T WithButtonPreset<T>(this T element) where T : VisualElement
{
TouchEffectPresets.Button.Apply(element);
return element;
}
/// <summary>
/// Applies the standard card preset.
/// </summary>
public static T WithCardPreset<T>(this T element) where T : VisualElement
{
TouchEffectPresets.Card.Apply(element);
return element;
}
/// <summary>
/// Applies the standard list item preset.
/// </summary>
public static T WithListItemPreset<T>(this T element) where T : VisualElement
{
TouchEffectPresets.ListItem.Apply(element);
return element;
}
/// <summary>
/// Applies the icon button preset.
/// </summary>
public static T WithIconButtonPreset<T>(this T element) where T : VisualElement
{
TouchEffectPresets.IconButton.Apply(element);
return element;
}
/// <summary>
/// Applies native platform animation.
/// </summary>
public static T WithNativeEffect<T>(this T element, Color? color = null) where T : VisualElement
{
TouchEffectPresets.Native.ApplyRipple(element, color);
return element;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB