Initial
375
.editorconfig
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,440 @@
|
||||
# MarketAlly.TouchEffect.Maui
|
||||
|
||||
[](https://www.nuget.org/packages/MarketAlly.TouchEffect.Maui)
|
||||
[](https://www.nuget.org/packages/MarketAlly.TouchEffect.Maui)
|
||||
[](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*
|
||||
@@ -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>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Maui.TouchEffect.Sample;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
MainPage = new AppShell();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Maui.TouchEffect.Sample;
|
||||
|
||||
public partial class AppShell : Shell
|
||||
{
|
||||
public AppShell()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Maui.TouchEffect.Sample;
|
||||
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Maui.TouchEffect.Sample;
|
||||
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 69 KiB |
@@ -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 |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -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();
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
7
src/Maui.TouchEffect/Enums/HoverState.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Maui.TouchEffect.Enums;
|
||||
|
||||
public enum HoverState
|
||||
{
|
||||
Normal,
|
||||
Hovered
|
||||
}
|
||||
7
src/Maui.TouchEffect/Enums/HoverStatus.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Maui.TouchEffect.Enums;
|
||||
|
||||
public enum HoverStatus
|
||||
{
|
||||
Entered,
|
||||
Exited,
|
||||
}
|
||||
7
src/Maui.TouchEffect/Enums/TouchInteractionStatus.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Maui.TouchEffect.Enums;
|
||||
|
||||
public enum TouchInteractionStatus
|
||||
{
|
||||
Started,
|
||||
Completed,
|
||||
}
|
||||
7
src/Maui.TouchEffect/Enums/TouchState.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Maui.TouchEffect.Enums;
|
||||
|
||||
public enum TouchState
|
||||
{
|
||||
Normal,
|
||||
Pressed,
|
||||
}
|
||||
8
src/Maui.TouchEffect/Enums/TouchStatus.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Maui.TouchEffect.Enums;
|
||||
|
||||
public enum TouchStatus
|
||||
{
|
||||
Started,
|
||||
Completed,
|
||||
Canceled,
|
||||
}
|
||||
12
src/Maui.TouchEffect/EventArgs/HoverStateChangedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Maui.TouchEffect;
|
||||
|
||||
public class LongPressCompletedEventArgs : EventArgs
|
||||
{
|
||||
internal LongPressCompletedEventArgs(object? parameter)
|
||||
{
|
||||
Parameter = parameter;
|
||||
}
|
||||
|
||||
public object? Parameter { get; }
|
||||
}
|
||||
11
src/Maui.TouchEffect/EventArgs/TouchCompletedEventArgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Maui.TouchEffect;
|
||||
|
||||
public class TouchCompletedEventArgs : EventArgs
|
||||
{
|
||||
internal TouchCompletedEventArgs(object? parameter)
|
||||
{
|
||||
Parameter = parameter;
|
||||
}
|
||||
|
||||
public object? Parameter { get; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
12
src/Maui.TouchEffect/EventArgs/TouchStateChangedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
123
src/Maui.TouchEffect/Extensions/SafeFireAndForgetExtension.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/Maui.TouchEffect/Extensions/VisualElementExtension.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
772
src/Maui.TouchEffect/GestureManager.cs
Normal 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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
25
src/Maui.TouchEffect/Hosting/AppBuilderExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
150
src/Maui.TouchEffect/Interfaces/ITouchEffectLogger.cs
Normal 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) { }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
420
src/Maui.TouchEffect/Platforms/Android/PlatformTouchEffect.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
690
src/Maui.TouchEffect/Platforms/Windows/PlatformTouchEffect.cs
Normal 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
|
||||
}
|
||||
311
src/Maui.TouchEffect/Platforms/iOS/PlatformTouchEffect.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/Maui.TouchEffect/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
56
src/Maui.TouchEffect/TouchEffect.Maui.csproj
Normal 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>
|
||||
1535
src/Maui.TouchEffect/TouchEffect.cs
Normal file
428
src/Maui.TouchEffect/TouchEffectBuilder.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
215
src/Maui.TouchEffect/TouchEffectConstants.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
363
src/Maui.TouchEffect/TouchEffectPresets.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
src/Maui.TouchEffect/icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |