├── .editorconfig ├── .gitignore ├── ComposeLevitation ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── gigamole │ └── composelevitation │ ├── common │ ├── LevitationState.kt │ └── LevitationUtils.kt │ ├── levitation │ ├── LevitationConfig.kt │ ├── LevitationContainer.kt │ ├── LevitationDefaults.kt │ └── LevitationOrientation.kt │ ├── press │ ├── PressConfig.kt │ ├── PressDefaults.kt │ └── PressType.kt │ └── shadow │ ├── ShadowConfig.kt │ ├── ShadowDefaults.kt │ └── ShadowType.kt ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ └── com │ │ └── gigamole │ │ └── composelevitation │ │ └── sample │ │ ├── MainActivity.kt │ │ ├── MainApplication.kt │ │ ├── MainScreenDemoContent.kt │ │ ├── MainScreenHolographicContent.kt │ │ └── MainTheme.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── android.png │ ├── logo.png │ ├── noise.png │ └── scene.jpg │ ├── font │ ├── days_one_regular.ttf │ ├── opensans_regular.ttf │ └── space_grotesk_bold.ttf │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ └── values │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── media ├── credits.png ├── demo-img.png ├── demo.gif ├── footer.png ├── header-new.png ├── header.png ├── image-levitation.gif ├── image-press.gif ├── image-shadow.gif ├── shape-levitation.gif ├── shape-press.gif ├── shape-shadow.gif ├── steam-1.gif ├── steam-2.gif └── steam-3.gif ├── plugins ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── java │ ├── CommonExtension.kt │ ├── ProjectConfig.kt │ ├── composelevitation.application.gradle.kts │ └── composelevitation.library.gradle.kts └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = crlf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = false 7 | max_line_length = 170 8 | tab_width = 4 9 | ij_continuation_indent_size = 8 10 | ij_formatter_off_tag = @formatter:off 11 | ij_formatter_on_tag = @formatter:on 12 | ij_formatter_tags_enabled = false 13 | ij_smart_tabs = false 14 | ij_visual_guides = none 15 | ij_wrap_on_typing = false 16 | 17 | # noinspection LongLine 18 | [*.java] 19 | ij_java_align_consecutive_assignments = false 20 | ij_java_align_consecutive_variable_declarations = false 21 | ij_java_align_group_field_declarations = false 22 | ij_java_align_multiline_annotation_parameters = false 23 | ij_java_align_multiline_array_initializer_expression = false 24 | ij_java_align_multiline_assignment = false 25 | ij_java_align_multiline_binary_operation = false 26 | ij_java_align_multiline_chained_methods = false 27 | ij_java_align_multiline_extends_list = false 28 | ij_java_align_multiline_for = true 29 | ij_java_align_multiline_method_parentheses = false 30 | ij_java_align_multiline_parameters = true 31 | ij_java_align_multiline_parameters_in_calls = false 32 | ij_java_align_multiline_parenthesized_expression = false 33 | ij_java_align_multiline_records = true 34 | ij_java_align_multiline_resources = true 35 | ij_java_align_multiline_ternary_operation = false 36 | ij_java_align_multiline_text_blocks = false 37 | ij_java_align_multiline_throws_list = false 38 | ij_java_align_subsequent_simple_methods = false 39 | ij_java_align_throws_keyword = false 40 | ij_java_align_types_in_multi_catch = true 41 | ij_java_annotation_parameter_wrap = off 42 | ij_java_array_initializer_new_line_after_left_brace = false 43 | ij_java_array_initializer_right_brace_on_new_line = false 44 | ij_java_array_initializer_wrap = off 45 | ij_java_assert_statement_colon_on_next_line = false 46 | ij_java_assert_statement_wrap = off 47 | ij_java_assignment_wrap = off 48 | ij_java_binary_operation_sign_on_next_line = false 49 | ij_java_binary_operation_wrap = off 50 | ij_java_blank_lines_after_anonymous_class_header = 0 51 | ij_java_blank_lines_after_class_header = 0 52 | ij_java_blank_lines_after_imports = 1 53 | ij_java_blank_lines_after_package = 1 54 | ij_java_blank_lines_around_class = 1 55 | ij_java_blank_lines_around_field = 0 56 | ij_java_blank_lines_around_field_in_interface = 0 57 | ij_java_blank_lines_around_initializer = 1 58 | ij_java_blank_lines_around_method = 1 59 | ij_java_blank_lines_around_method_in_interface = 1 60 | ij_java_blank_lines_before_class_end = 0 61 | ij_java_blank_lines_before_imports = 1 62 | ij_java_blank_lines_before_method_body = 0 63 | ij_java_blank_lines_before_package = 0 64 | ij_java_block_brace_style = end_of_line 65 | ij_java_block_comment_add_space = false 66 | ij_java_block_comment_at_first_column = true 67 | ij_java_builder_methods = none 68 | ij_java_call_parameters_new_line_after_left_paren = false 69 | ij_java_call_parameters_right_paren_on_new_line = false 70 | ij_java_call_parameters_wrap = off 71 | ij_java_case_statement_on_separate_line = true 72 | ij_java_catch_on_new_line = false 73 | ij_java_class_annotation_wrap = split_into_lines 74 | ij_java_class_brace_style = end_of_line 75 | ij_java_class_count_to_use_import_on_demand = 99 76 | ij_java_class_names_in_javadoc = 1 77 | ij_java_do_not_indent_top_level_class_members = false 78 | ij_java_do_not_wrap_after_single_annotation = false 79 | ij_java_do_not_wrap_after_single_annotation_in_parameter = false 80 | ij_java_do_while_brace_force = never 81 | ij_java_doc_add_blank_line_after_description = true 82 | ij_java_doc_add_blank_line_after_param_comments = false 83 | ij_java_doc_add_blank_line_after_return = false 84 | ij_java_doc_add_p_tag_on_empty_lines = true 85 | ij_java_doc_align_exception_comments = true 86 | ij_java_doc_align_param_comments = true 87 | ij_java_doc_do_not_wrap_if_one_line = false 88 | ij_java_doc_enable_formatting = true 89 | ij_java_doc_enable_leading_asterisks = true 90 | ij_java_doc_indent_on_continuation = false 91 | ij_java_doc_keep_empty_lines = true 92 | ij_java_doc_keep_empty_parameter_tag = true 93 | ij_java_doc_keep_empty_return_tag = true 94 | ij_java_doc_keep_empty_throws_tag = true 95 | ij_java_doc_keep_invalid_tags = true 96 | ij_java_doc_param_description_on_new_line = false 97 | ij_java_doc_preserve_line_breaks = false 98 | ij_java_doc_use_throws_not_exception_tag = true 99 | ij_java_else_on_new_line = false 100 | ij_java_enum_constants_wrap = off 101 | ij_java_extends_keyword_wrap = off 102 | ij_java_extends_list_wrap = off 103 | ij_java_field_annotation_wrap = split_into_lines 104 | ij_java_finally_on_new_line = false 105 | ij_java_for_brace_force = never 106 | ij_java_for_statement_new_line_after_left_paren = false 107 | ij_java_for_statement_right_paren_on_new_line = false 108 | ij_java_for_statement_wrap = off 109 | ij_java_generate_final_locals = false 110 | ij_java_generate_final_parameters = false 111 | ij_java_if_brace_force = never 112 | ij_java_imports_layout = $android.**, $androidx.**, $com.**, $junit.**, $net.**, $org.**, $java.**, $javax.**, $*, |, android.**, |, androidx.**, |, com.**, |, junit.**, |, net.**, |, org.**, |, java.**, |, javax.**, |, *, | 113 | ij_java_indent_case_from_switch = true 114 | ij_java_insert_inner_class_imports = false 115 | ij_java_insert_override_annotation = true 116 | ij_java_keep_blank_lines_before_right_brace = 2 117 | ij_java_keep_blank_lines_between_package_declaration_and_header = 2 118 | ij_java_keep_blank_lines_in_code = 2 119 | ij_java_keep_blank_lines_in_declarations = 2 120 | ij_java_keep_builder_methods_indents = false 121 | ij_java_keep_control_statement_in_one_line = true 122 | ij_java_keep_first_column_comment = true 123 | ij_java_keep_indents_on_empty_lines = false 124 | ij_java_keep_line_breaks = true 125 | ij_java_keep_multiple_expressions_in_one_line = false 126 | ij_java_keep_simple_blocks_in_one_line = false 127 | ij_java_keep_simple_classes_in_one_line = false 128 | ij_java_keep_simple_lambdas_in_one_line = false 129 | ij_java_keep_simple_methods_in_one_line = false 130 | ij_java_label_indent_absolute = false 131 | ij_java_label_indent_size = 0 132 | ij_java_lambda_brace_style = end_of_line 133 | ij_java_layout_static_imports_separately = true 134 | ij_java_line_comment_add_space = false 135 | ij_java_line_comment_add_space_on_reformat = false 136 | ij_java_line_comment_at_first_column = true 137 | ij_java_method_annotation_wrap = split_into_lines 138 | ij_java_method_brace_style = end_of_line 139 | ij_java_method_call_chain_wrap = off 140 | ij_java_method_parameters_new_line_after_left_paren = false 141 | ij_java_method_parameters_right_paren_on_new_line = false 142 | ij_java_method_parameters_wrap = off 143 | ij_java_modifier_list_wrap = false 144 | ij_java_multi_catch_types_wrap = normal 145 | ij_java_names_count_to_use_import_on_demand = 99 146 | ij_java_new_line_after_lparen_in_annotation = false 147 | ij_java_new_line_after_lparen_in_record_header = false 148 | ij_java_parameter_annotation_wrap = off 149 | ij_java_parentheses_expression_new_line_after_left_paren = false 150 | ij_java_parentheses_expression_right_paren_on_new_line = false 151 | ij_java_place_assignment_sign_on_next_line = false 152 | ij_java_prefer_longer_names = true 153 | ij_java_prefer_parameters_wrap = false 154 | ij_java_record_components_wrap = normal 155 | ij_java_repeat_synchronized = true 156 | ij_java_replace_instanceof_and_cast = false 157 | ij_java_replace_null_check = true 158 | ij_java_replace_sum_lambda_with_method_ref = true 159 | ij_java_resource_list_new_line_after_left_paren = false 160 | ij_java_resource_list_right_paren_on_new_line = false 161 | ij_java_resource_list_wrap = off 162 | ij_java_rparen_on_new_line_in_annotation = false 163 | ij_java_rparen_on_new_line_in_record_header = false 164 | ij_java_space_after_closing_angle_bracket_in_type_argument = false 165 | ij_java_space_after_colon = true 166 | ij_java_space_after_comma = true 167 | ij_java_space_after_comma_in_type_arguments = true 168 | ij_java_space_after_for_semicolon = true 169 | ij_java_space_after_quest = true 170 | ij_java_space_after_type_cast = true 171 | ij_java_space_before_annotation_array_initializer_left_brace = false 172 | ij_java_space_before_annotation_parameter_list = false 173 | ij_java_space_before_array_initializer_left_brace = false 174 | ij_java_space_before_catch_keyword = true 175 | ij_java_space_before_catch_left_brace = true 176 | ij_java_space_before_catch_parentheses = true 177 | ij_java_space_before_class_left_brace = true 178 | ij_java_space_before_colon = true 179 | ij_java_space_before_colon_in_foreach = true 180 | ij_java_space_before_comma = false 181 | ij_java_space_before_do_left_brace = true 182 | ij_java_space_before_else_keyword = true 183 | ij_java_space_before_else_left_brace = true 184 | ij_java_space_before_finally_keyword = true 185 | ij_java_space_before_finally_left_brace = true 186 | ij_java_space_before_for_left_brace = true 187 | ij_java_space_before_for_parentheses = true 188 | ij_java_space_before_for_semicolon = false 189 | ij_java_space_before_if_left_brace = true 190 | ij_java_space_before_if_parentheses = true 191 | ij_java_space_before_method_call_parentheses = false 192 | ij_java_space_before_method_left_brace = true 193 | ij_java_space_before_method_parentheses = false 194 | ij_java_space_before_opening_angle_bracket_in_type_parameter = false 195 | ij_java_space_before_quest = true 196 | ij_java_space_before_switch_left_brace = true 197 | ij_java_space_before_switch_parentheses = true 198 | ij_java_space_before_synchronized_left_brace = true 199 | ij_java_space_before_synchronized_parentheses = true 200 | ij_java_space_before_try_left_brace = true 201 | ij_java_space_before_try_parentheses = true 202 | ij_java_space_before_type_parameter_list = false 203 | ij_java_space_before_while_keyword = true 204 | ij_java_space_before_while_left_brace = true 205 | ij_java_space_before_while_parentheses = true 206 | ij_java_space_inside_one_line_enum_braces = false 207 | ij_java_space_within_empty_array_initializer_braces = false 208 | ij_java_space_within_empty_method_call_parentheses = false 209 | ij_java_space_within_empty_method_parentheses = false 210 | ij_java_spaces_around_additive_operators = true 211 | ij_java_spaces_around_annotation_eq = true 212 | ij_java_spaces_around_assignment_operators = true 213 | ij_java_spaces_around_bitwise_operators = true 214 | ij_java_spaces_around_equality_operators = true 215 | ij_java_spaces_around_lambda_arrow = true 216 | ij_java_spaces_around_logical_operators = true 217 | ij_java_spaces_around_method_ref_dbl_colon = false 218 | ij_java_spaces_around_multiplicative_operators = true 219 | ij_java_spaces_around_relational_operators = true 220 | ij_java_spaces_around_shift_operators = true 221 | ij_java_spaces_around_type_bounds_in_type_parameters = true 222 | ij_java_spaces_around_unary_operator = false 223 | ij_java_spaces_within_angle_brackets = false 224 | ij_java_spaces_within_annotation_parentheses = false 225 | ij_java_spaces_within_array_initializer_braces = false 226 | ij_java_spaces_within_braces = false 227 | ij_java_spaces_within_brackets = false 228 | ij_java_spaces_within_cast_parentheses = false 229 | ij_java_spaces_within_catch_parentheses = false 230 | ij_java_spaces_within_for_parentheses = false 231 | ij_java_spaces_within_if_parentheses = false 232 | ij_java_spaces_within_method_call_parentheses = false 233 | ij_java_spaces_within_method_parentheses = false 234 | ij_java_spaces_within_parentheses = false 235 | ij_java_spaces_within_record_header = false 236 | ij_java_spaces_within_switch_parentheses = false 237 | ij_java_spaces_within_synchronized_parentheses = false 238 | ij_java_spaces_within_try_parentheses = false 239 | ij_java_spaces_within_while_parentheses = false 240 | ij_java_special_else_if_treatment = true 241 | ij_java_subclass_name_suffix = Impl 242 | ij_java_ternary_operation_signs_on_next_line = false 243 | ij_java_ternary_operation_wrap = off 244 | ij_java_test_name_suffix = Test 245 | ij_java_throws_keyword_wrap = off 246 | ij_java_throws_list_wrap = off 247 | ij_java_use_external_annotations = false 248 | ij_java_use_fq_class_names = false 249 | ij_java_use_relative_indents = false 250 | ij_java_use_single_class_imports = true 251 | ij_java_variable_annotation_wrap = off 252 | ij_java_visibility = public 253 | ij_java_while_brace_force = never 254 | ij_java_while_on_new_line = false 255 | ij_java_wrap_comments = false 256 | ij_java_wrap_first_method_in_call_chain = false 257 | ij_java_wrap_long_lines = false 258 | 259 | [*.properties] 260 | ij_properties_align_group_field_declarations = false 261 | ij_properties_keep_blank_lines = false 262 | ij_properties_key_value_delimiter = equals 263 | ij_properties_spaces_around_key_value_delimiter = false 264 | 265 | [.editorconfig] 266 | ij_editorconfig_align_group_field_declarations = false 267 | ij_editorconfig_space_after_colon = false 268 | ij_editorconfig_space_after_comma = true 269 | ij_editorconfig_space_before_colon = false 270 | ij_editorconfig_space_before_comma = false 271 | ij_editorconfig_spaces_around_assignment_operators = true 272 | 273 | [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] 274 | ij_continuation_indent_size = 4 275 | ij_xml_align_attributes = false 276 | ij_xml_align_text = false 277 | ij_xml_attribute_wrap = normal 278 | ij_xml_block_comment_add_space = false 279 | ij_xml_block_comment_at_first_column = true 280 | ij_xml_keep_blank_lines = 2 281 | ij_xml_keep_indents_on_empty_lines = false 282 | ij_xml_keep_line_breaks = false 283 | ij_xml_keep_line_breaks_in_text = true 284 | ij_xml_keep_whitespaces = false 285 | ij_xml_keep_whitespaces_around_cdata = preserve 286 | ij_xml_keep_whitespaces_inside_cdata = false 287 | ij_xml_line_comment_at_first_column = true 288 | ij_xml_space_after_tag_name = false 289 | ij_xml_space_around_equals_in_attribute = false 290 | ij_xml_space_inside_empty_tag = true 291 | ij_xml_text_wrap = normal 292 | ij_xml_use_custom_settings = true 293 | 294 | [{*.kt,*.kts}] 295 | ij_kotlin_align_in_columns_case_branch = false 296 | ij_kotlin_align_multiline_binary_operation = false 297 | ij_kotlin_align_multiline_extends_list = false 298 | ij_kotlin_align_multiline_method_parentheses = false 299 | ij_kotlin_align_multiline_parameters = true 300 | ij_kotlin_align_multiline_parameters_in_calls = false 301 | ij_kotlin_allow_trailing_comma = true 302 | ij_kotlin_allow_trailing_comma_on_call_site = false 303 | ij_kotlin_assignment_wrap = normal 304 | ij_kotlin_blank_lines_after_class_header = 0 305 | ij_kotlin_blank_lines_around_block_when_branches = 0 306 | ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 307 | ij_kotlin_block_comment_add_space = false 308 | ij_kotlin_block_comment_at_first_column = true 309 | ij_kotlin_call_parameters_new_line_after_left_paren = true 310 | ij_kotlin_call_parameters_right_paren_on_new_line = true 311 | ij_kotlin_call_parameters_wrap = on_every_item 312 | ij_kotlin_catch_on_new_line = false 313 | ij_kotlin_class_annotation_wrap = split_into_lines 314 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 315 | ij_kotlin_continuation_indent_for_chained_calls = false 316 | ij_kotlin_continuation_indent_for_expression_bodies = false 317 | ij_kotlin_continuation_indent_in_argument_lists = false 318 | ij_kotlin_continuation_indent_in_elvis = false 319 | ij_kotlin_continuation_indent_in_if_conditions = false 320 | ij_kotlin_continuation_indent_in_parameter_lists = false 321 | ij_kotlin_continuation_indent_in_supertype_lists = false 322 | ij_kotlin_else_on_new_line = false 323 | ij_kotlin_enum_constants_wrap = off 324 | ij_kotlin_extends_list_wrap = normal 325 | ij_kotlin_field_annotation_wrap = split_into_lines 326 | ij_kotlin_finally_on_new_line = false 327 | ij_kotlin_if_rparen_on_new_line = true 328 | ij_kotlin_import_nested_classes = false 329 | ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ 330 | ij_kotlin_insert_whitespaces_in_simple_one_line_method = true 331 | ij_kotlin_keep_blank_lines_before_right_brace = 2 332 | ij_kotlin_keep_blank_lines_in_code = 2 333 | ij_kotlin_keep_blank_lines_in_declarations = 2 334 | ij_kotlin_keep_first_column_comment = true 335 | ij_kotlin_keep_indents_on_empty_lines = false 336 | ij_kotlin_keep_line_breaks = true 337 | ij_kotlin_lbrace_on_next_line = false 338 | ij_kotlin_line_comment_add_space = false 339 | ij_kotlin_line_comment_add_space_on_reformat = false 340 | ij_kotlin_line_comment_at_first_column = true 341 | ij_kotlin_method_annotation_wrap = split_into_lines 342 | ij_kotlin_method_call_chain_wrap = normal 343 | ij_kotlin_method_parameters_new_line_after_left_paren = true 344 | ij_kotlin_method_parameters_right_paren_on_new_line = true 345 | ij_kotlin_method_parameters_wrap = on_every_item 346 | ij_kotlin_name_count_to_use_star_import = 2147483647 347 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 348 | ij_kotlin_parameter_annotation_wrap = off 349 | ij_kotlin_space_after_comma = true 350 | ij_kotlin_space_after_extend_colon = true 351 | ij_kotlin_space_after_type_colon = true 352 | ij_kotlin_space_before_catch_parentheses = true 353 | ij_kotlin_space_before_comma = false 354 | ij_kotlin_space_before_extend_colon = true 355 | ij_kotlin_space_before_for_parentheses = true 356 | ij_kotlin_space_before_if_parentheses = true 357 | ij_kotlin_space_before_lambda_arrow = true 358 | ij_kotlin_space_before_type_colon = false 359 | ij_kotlin_space_before_when_parentheses = true 360 | ij_kotlin_space_before_while_parentheses = true 361 | ij_kotlin_spaces_around_additive_operators = true 362 | ij_kotlin_spaces_around_assignment_operators = true 363 | ij_kotlin_spaces_around_equality_operators = true 364 | ij_kotlin_spaces_around_function_type_arrow = true 365 | ij_kotlin_spaces_around_logical_operators = true 366 | ij_kotlin_spaces_around_multiplicative_operators = true 367 | ij_kotlin_spaces_around_range = false 368 | ij_kotlin_spaces_around_relational_operators = true 369 | ij_kotlin_line_break_after_multiline_when_entry = false 370 | ij_kotlin_spaces_around_unary_operator = false 371 | ij_kotlin_spaces_around_when_arrow = true 372 | ij_kotlin_use_custom_formatting_for_modifiers = true 373 | ij_kotlin_variable_annotation_wrap = off 374 | ij_kotlin_while_on_new_line = false 375 | ij_kotlin_wrap_elvis_expressions = 1 376 | ij_kotlin_wrap_expression_body_functions = 1 377 | ij_kotlin_wrap_first_method_in_call_chain = false 378 | 379 | [{*.har,*.json}] 380 | indent_size = 2 381 | ij_json_keep_blank_lines_in_code = 0 382 | ij_json_keep_indents_on_empty_lines = false 383 | ij_json_keep_line_breaks = true 384 | ij_json_space_after_colon = true 385 | ij_json_space_after_comma = true 386 | ij_json_space_before_colon = true 387 | ij_json_space_before_comma = false 388 | ij_json_spaces_within_braces = false 389 | ij_json_spaces_within_brackets = false 390 | ij_json_wrap_long_lines = false 391 | 392 | [{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] 393 | ij_toml_keep_indents_on_empty_lines = false 394 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | 25 | # JetBrains IDE 26 | .idea/ 27 | 28 | # Unit test reports 29 | TEST*.xml 30 | 31 | # Generated by MacOS 32 | .DS_Store 33 | 34 | # Generated by Windows 35 | Thumbs.db 36 | 37 | # Applications 38 | *.app 39 | *.exe 40 | *.war 41 | 42 | # Large media files 43 | *.mp4 44 | *.tiff 45 | *.avi 46 | *.flv 47 | *.mov 48 | *.wmv 49 | 50 | 51 | *.iml 52 | .gradle 53 | /local.properties 54 | /.idea/caches 55 | /.idea/libraries 56 | /.idea/modules.xml 57 | /.idea/workspace.xml 58 | /.idea/navEditor.xml 59 | /.idea/assetWizardSettings.xml 60 | build 61 | /build 62 | /captures 63 | .externalNativeBuild 64 | .cxx 65 | local.properties 66 | buildSrc/build/classes/kotlin/main/gradle/kotlin/dsl/ 67 | buildSrc/build/ 68 | presentation/envTest/release/ 69 | presentation/prod/release/ 70 | presentation/envTest/ 71 | plugins/build/ 72 | gradle/libs.versions.updates.toml -------------------------------------------------------------------------------- /ComposeLevitation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ComposeLevitation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | @Suppress( 4 | "DSL_SCOPE_VIOLATION", 5 | "MISSING_DEPENDENCY_CLASS", 6 | "UNRESOLVED_REFERENCE_WRONG_RECEIVER", 7 | "FUNCTION_CALL_EXPECTED" 8 | ) 9 | 10 | plugins { 11 | id("composelevitation.library") 12 | `maven-publish` 13 | } 14 | 15 | group = ProjectConfig.group 16 | version = ProjectConfig.versionName 17 | 18 | publishing { 19 | publications { 20 | register(ProjectConfig.publication) { 21 | groupId = ProjectConfig.group 22 | artifactId = ProjectConfig.artifact 23 | version = ProjectConfig.versionName 24 | 25 | afterEvaluate { 26 | from(components[ProjectConfig.publication]) 27 | } 28 | } 29 | } 30 | } 31 | 32 | android { 33 | namespace = ProjectConfig.namespace 34 | 35 | composeOptions { 36 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.version.get() 37 | } 38 | 39 | publishing { 40 | singleVariant(ProjectConfig.publication) { 41 | withSourcesJar() 42 | withJavadocJar() 43 | } 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation(libs.androidx.ktx) 49 | 50 | implementation(platform(libs.compose.bom)) 51 | implementation(libs.bundles.compose) 52 | 53 | api(libs.compose.shadows.plus) 54 | 55 | debugImplementation(libs.bundles.debug.compose) 56 | } -------------------------------------------------------------------------------- /ComposeLevitation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /ComposeLevitation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/common/LevitationState.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused", "MemberVisibilityCanBePrivate") 2 | 3 | package com.gigamole.composelevitation.common 4 | 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.animateOffsetAsState 7 | import androidx.compose.animation.core.spring 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.derivedStateOf 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableFloatStateOf 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.geometry.Offset 18 | import androidx.compose.ui.geometry.Size 19 | import androidx.compose.ui.input.pointer.PointerInputChange 20 | import androidx.compose.ui.input.pointer.isOutOfBounds 21 | import androidx.compose.ui.platform.LocalDensity 22 | import androidx.compose.ui.unit.DpOffset 23 | import androidx.compose.ui.unit.IntSize 24 | import androidx.compose.ui.unit.toSize 25 | import com.gigamole.composelevitation.levitation.LevitationConfig 26 | import com.gigamole.composelevitation.levitation.LevitationContainer 27 | import com.gigamole.composelevitation.levitation.LevitationOrientation 28 | import com.gigamole.composelevitation.levitation.levitation 29 | import com.gigamole.composelevitation.press.PressConfig 30 | import com.gigamole.composelevitation.press.PressType 31 | import com.gigamole.composelevitation.shadow.ShadowConfig 32 | import com.gigamole.composelevitation.shadow.ShadowType 33 | import kotlin.math.abs 34 | import kotlin.math.absoluteValue 35 | import kotlin.math.max 36 | 37 | /** 38 | * A utility function to create a [Composable] [LevitationState]. 39 | * 40 | * @param levitationConfig The required [LevitationConfig]. 41 | * @param pressConfig The required [PressConfig]. 42 | * @param shadowConfig An optional [ShadowConfig]. 43 | * @return The [Composable] prepared [LevitationState]. 44 | * @see LevitationContainer 45 | * @author GIGAMOLE 46 | */ 47 | @Composable 48 | fun rememberLevitationState( 49 | levitationConfig: LevitationConfig = LevitationConfig(), 50 | pressConfig: PressConfig = PressConfig(), 51 | shadowConfig: ShadowConfig? = ShadowConfig() 52 | ): LevitationState = remember( 53 | levitationConfig, 54 | pressConfig, 55 | shadowConfig 56 | ) { 57 | LevitationState( 58 | levitationConfig = levitationConfig, 59 | pressConfig = pressConfig, 60 | shadowConfig = shadowConfig 61 | ) 62 | } 63 | 64 | /** 65 | * A centralized source of state data for [LevitationContainer]. 66 | * 67 | * @param levitationConfig The required [LevitationConfig]. 68 | * @param pressConfig The required [PressConfig]. 69 | * @param shadowConfig An optional [ShadowConfig]. 70 | * @see LevitationContainer 71 | * @see Modifier.levitation 72 | * @author GIGAMOLE 73 | */ 74 | class LevitationState( 75 | val levitationConfig: LevitationConfig = LevitationConfig(), 76 | val pressConfig: PressConfig = PressConfig(), 77 | val shadowConfig: ShadowConfig? = ShadowConfig() 78 | ) { 79 | 80 | /** Raw content size state. */ 81 | internal var intSizeState by mutableStateOf(IntSize.Zero) 82 | 83 | /** Raw press offset state. Range from 0.0F to max [size]. */ 84 | internal var rawPressOffsetState by mutableStateOf(Offset.Zero) 85 | 86 | /** 87 | * Raw drag offset state. Range from 0.0F to max [size]. 88 | * 89 | * @see handleDrag 90 | */ 91 | internal var rawDragOffsetState by mutableStateOf(Offset.Zero) 92 | 93 | /** 94 | * Indicates whether levitation is in pressed state or not. 95 | * 96 | * @see AnimatePressFraction 97 | * @see PressConfig.isPivotedWhenReleased 98 | */ 99 | internal var isPressedState by mutableStateOf(false) 100 | 101 | /** 102 | * Indicates whether an ongoing press animation should awaits for full press to release after or not. 103 | * 104 | * @see AnimatePressFraction 105 | * @see cancelPendingAwaitPressedState 106 | * @see handleAwaitPressedState 107 | * @see PressConfig.isAwaitPressAnimation 108 | */ 109 | internal var isAwaitPressedState by mutableStateOf(false) 110 | 111 | /** 112 | * Indicates whether levitation is in dragged state or not. 113 | * 114 | * @see handleDrag 115 | */ 116 | internal var isDraggedState by mutableStateOf(false) 117 | 118 | /** Press fraction state. Range from 0.0F to 1.0F. */ 119 | private var pressFractionState by mutableFloatStateOf(0.0F) 120 | 121 | /** 122 | * Offset state that combines [rawPressOffsetState] and [rawDragOffsetState], depending on the current press gesture. Range from 0.0F to max [size]. 123 | * 124 | * @see AnimateLevitationOffset 125 | */ 126 | private var offsetState by mutableStateOf(Offset.Zero) 127 | 128 | /** 129 | * Converted from [DpOffset] to [Offset] shadow translation offset state. Can be positive or negative. 130 | * 131 | * @see ConvertShadowOffsets 132 | * @see shadowTranslation 133 | */ 134 | private var shadowTranslationOffsetState by mutableStateOf(Offset.Zero) 135 | 136 | /** 137 | * Converted from [DpOffset] to [Offset] shadow offset state. Can be positive or negative. 138 | * 139 | * @see ConvertShadowOffsets 140 | * @see shadowTranslation 141 | * @see ShadowConfig.isPivotedWhenPressed 142 | */ 143 | private var shadowOffsetState by mutableStateOf(Offset.Zero) 144 | 145 | /** 146 | * State which indicates whether the simulated press started or not. 147 | * 148 | * @see press 149 | * @see AnimatePressFraction 150 | */ 151 | private var isSimulatedPressStartedState by mutableStateOf(false) 152 | 153 | /** 154 | * State which indicates whether the [isAwaitPressedState] and press animation finished ([pressFractionState] == 1.0F) or not. 155 | * 156 | * @see AnimatePressFraction 157 | * @see PressConfig.isAwaitPressAnimation 158 | */ 159 | private var isAwaitPressedAnimationFinishedState by mutableStateOf(false) 160 | 161 | /** Pivot size based offset. Range from 0.0F to max [size]. */ 162 | private val pivotOffset by derivedStateOf { 163 | Offset( 164 | x = size.width * levitationConfig.pivot.x, 165 | y = size.height * levitationConfig.pivot.y 166 | ) 167 | } 168 | 169 | /** 170 | * Indicates whether the at least one drag event is outside the [intSizeState] bounds, so next drag events are skipped until release or not. 171 | * 172 | * @see handleDrag 173 | */ 174 | private val isDragBlockedUntilRelease by derivedStateOf { 175 | isDraggedState.not() 176 | } 177 | 178 | /** Indicates whether the levitation is pressed. */ 179 | val isPressed: Boolean 180 | get() = isPressedState 181 | 182 | /** Press fraction. Range from 0.0F to 1.0F. */ 183 | val pressFraction: Float 184 | get() = pressFractionState 185 | 186 | /** 187 | * Levitation content size. 188 | * 189 | * @see intSizeState 190 | */ 191 | val size by derivedStateOf { 192 | intSizeState.toSize() 193 | } 194 | 195 | /** 196 | * Levitation animated (between press, drag and release) raw offset. Range from 0.0F to max [size]. 197 | * 198 | * @see rawPressOffsetState 199 | * @see rawDragOffsetState 200 | */ 201 | val offset by derivedStateOf { 202 | offsetState 203 | } 204 | 205 | /** 206 | * Levitation offset progress. Range from 0.0F to 1.0F. 207 | * 208 | * @see offset 209 | * @see calculateOffsetAxisProgress 210 | */ 211 | val offsetProgress by derivedStateOf { 212 | Offset( 213 | x = when (levitationConfig.orientation) { 214 | LevitationOrientation.All, 215 | LevitationOrientation.Horizontal -> { 216 | calculateOffsetAxisProgress( 217 | offsetAxis = offset.x, 218 | defaultOffsetAxis = levitationConfig.pivot.x, 219 | sizeAxis = size.width 220 | ) 221 | } 222 | LevitationOrientation.Vertical -> { 223 | levitationConfig.pivot.x 224 | } 225 | }, 226 | y = when (levitationConfig.orientation) { 227 | LevitationOrientation.All, 228 | LevitationOrientation.Vertical -> { 229 | calculateOffsetAxisProgress( 230 | offsetAxis = offset.y, 231 | defaultOffsetAxis = levitationConfig.pivot.y, 232 | sizeAxis = size.height 233 | ) 234 | } 235 | LevitationOrientation.Horizontal -> { 236 | levitationConfig.pivot.y 237 | } 238 | } 239 | ) 240 | } 241 | 242 | /** 243 | * Levitation degree progress. Range from -1.0F to 1.0F. The (0.0F, 0.0F) degree progress is at the pivot. 244 | * 245 | * @see offsetProgress 246 | * @see calculateDegreeAxisProgress 247 | * @see LevitationConfig.pivot 248 | */ 249 | val degreeProgress by derivedStateOf { 250 | Offset( 251 | x = when (levitationConfig.orientation) { 252 | LevitationOrientation.All, 253 | LevitationOrientation.Horizontal -> { 254 | calculateDegreeAxisProgress( 255 | offsetProgressAxis = offsetProgress.x, 256 | pivotAxis = levitationConfig.pivot.x 257 | ) 258 | } 259 | LevitationOrientation.Vertical -> { 260 | 0.0F 261 | } 262 | }, 263 | y = when (levitationConfig.orientation) { 264 | LevitationOrientation.All, 265 | LevitationOrientation.Vertical -> { 266 | calculateDegreeAxisProgress( 267 | offsetProgressAxis = offsetProgress.y, 268 | pivotAxis = levitationConfig.pivot.y 269 | ) 270 | } 271 | LevitationOrientation.Horizontal -> { 272 | 0.0F 273 | } 274 | } 275 | ) 276 | } 277 | 278 | /** 279 | * Levitation content scale. Calculated specifically for [Modifier.levitation]. 280 | * 281 | * @see calculateScale 282 | */ 283 | val scale by derivedStateOf { 284 | calculateScale() 285 | } 286 | 287 | /** 288 | * Levitation content rotation. Calculated specifically for [Modifier.levitation]. 289 | * 290 | * @see degreeProgress 291 | * @see pressFraction 292 | * @see LevitationConfig.degree 293 | */ 294 | val rotation by derivedStateOf { 295 | Offset( 296 | x = degreeProgress.y * -pressFraction * levitationConfig.degree, 297 | y = degreeProgress.x * pressFraction * levitationConfig.degree 298 | ) 299 | } 300 | 301 | /** 302 | * Levitation shadow scale. Calculated specifically for [Modifier.levitation]. 303 | * 304 | * @see calculateScale 305 | * @see ShadowConfig.downscaleMultiplier 306 | */ 307 | val shadowScale by derivedStateOf { 308 | shadowConfig?.let { 309 | calculateScale(downscaleMultiplier = it.downscaleMultiplier) 310 | } ?: 0.0F 311 | } 312 | 313 | /** 314 | * Levitation shadow rotation. Calculated specifically for [Modifier.levitation]. 315 | * 316 | * @see rotation 317 | * @see ShadowConfig.degreeMultiplier 318 | */ 319 | val shadowRotation by derivedStateOf { 320 | shadowConfig?.let { 321 | rotation.times(operand = it.degreeMultiplier) 322 | } ?: rotation 323 | } 324 | 325 | /** 326 | * Levitation shadow translation. Calculated specifically for [Modifier.levitation]. 327 | * 328 | * @see degreeProgress 329 | * @see pressFraction 330 | * @see LevitationConfig.degree 331 | * @see ShadowConfig.translationOffset 332 | * @see ShadowConfig.isPivotedWhenPressed 333 | */ 334 | val shadowTranslation by derivedStateOf { 335 | shadowConfig?.let { 336 | // Combines shadows offset, shadow translation offset, and pivot centering if needed. 337 | val degreePressTranslationX = degreeProgress.x * pressFraction * -shadowTranslationOffsetState.x 338 | val degreePressCenteredShadowX = if (it.isPivotedWhenPressed) { 339 | ((1.0F - abs(degreeProgress.x)) * pressFraction * shadowOffsetState.x) 340 | } else { 341 | 0.0F 342 | } 343 | val degreePressTranslationY = degreeProgress.y * pressFraction * -shadowTranslationOffsetState.y 344 | val degreePressCenteredShadowY = if (it.isPivotedWhenPressed) { 345 | ((1.0F - abs(degreeProgress.y)) * pressFraction * shadowOffsetState.y) 346 | } else { 347 | 0.0F 348 | } 349 | 350 | Offset( 351 | x = degreePressTranslationX - degreePressCenteredShadowX, 352 | y = degreePressTranslationY - degreePressCenteredShadowY 353 | ) 354 | } ?: Offset.Zero 355 | } 356 | 357 | /** 358 | * Shadow camera distance. Calculated specifically for [Modifier.levitation]. 359 | * 360 | * @see LevitationConfig.cameraDistance 361 | * @see ShadowConfig.cameraDistanceMultiplier 362 | */ 363 | val shadowCameraDistance by derivedStateOf { 364 | shadowConfig?.let { 365 | levitationConfig.cameraDistance * it.cameraDistanceMultiplier 366 | } ?: levitationConfig.cameraDistance 367 | } 368 | 369 | /** 370 | * Performs levitation press at the provided [pivot] point. Levitation animates to full press, and then releases press. 371 | * 372 | * @param pivot The point at which perform press action. Range from 0.0F to 1.0F. 373 | * @see LevitationConfig.pivot 374 | * @see AnimatePressFraction 375 | * @see isSimulatedPressStartedState 376 | */ 377 | fun press(pivot: Offset = levitationConfig.pivot) { 378 | rawPressOffsetState = Offset( 379 | x = size.width * pivot.x, 380 | y = size.height * pivot.y, 381 | ) 382 | isSimulatedPressStartedState = true 383 | isPressedState = true 384 | 385 | // If levitation is already fully pressed, release it. 386 | if (pressFraction == 1.0F) { 387 | isSimulatedPressStartedState = false 388 | isPressedState = false 389 | } 390 | } 391 | 392 | /** 393 | * Animates [pressFraction]. Also, handles the [isAwaitPressedState] and [isSimulatedPressStartedState] behaviour. 394 | * 395 | * @see PressConfig.isAwaitPressAnimation 396 | * @see press 397 | * @see LevitationContainer 398 | */ 399 | @Composable 400 | internal fun AnimatePressFraction() { 401 | val pressFraction by animateFloatAsState( 402 | targetValue = if (isPressedState) 1.0F else 0.0F, 403 | animationSpec = pressConfig.pressAnimationSpec, 404 | label = "PressFraction" 405 | ) { value -> 406 | if (pressConfig.isAwaitPressAnimation) { 407 | isAwaitPressedAnimationFinishedState = value == 1.0F 408 | 409 | if (isAwaitPressedState) { 410 | isAwaitPressedState = false 411 | isPressedState = false 412 | } 413 | } 414 | 415 | if (isSimulatedPressStartedState) { 416 | if (value == 1.0F) { 417 | isSimulatedPressStartedState = false 418 | isPressedState = false 419 | } 420 | } 421 | } 422 | 423 | isAwaitPressedAnimationFinishedState = pressFraction == 1.0F 424 | pressFractionState = pressFraction 425 | } 426 | 427 | /** 428 | * Animates levitation [offsetState] between [rawPressOffsetState], [rawDragOffsetState] and [pivotOffset] when released if needed. 429 | * 430 | * @see isPressedState 431 | * @see isDraggedState 432 | * @see PressConfig.isPivotedWhenReleased 433 | * @see LevitationContainer 434 | */ 435 | @Composable 436 | internal fun AnimateLevitationOffset() { 437 | val offset by animateOffsetAsState( 438 | targetValue = when { 439 | // Animate to drag offset. 440 | isDraggedState && isPressedState -> { 441 | rawDragOffsetState 442 | } 443 | else -> { 444 | if (pressConfig.isPivotedWhenReleased) { 445 | if (isPressedState && isDraggedState.not()) { 446 | // Animate to press offset. 447 | rawPressOffsetState 448 | } else { 449 | // Animate to pivot center offset. 450 | pivotOffset 451 | } 452 | } else { 453 | // Animate to press offset. 454 | rawPressOffsetState 455 | } 456 | } 457 | }, 458 | animationSpec = if (pressFraction == 0.0F) { 459 | // Required to react to the first levitation press offset change without animation. 460 | tween(durationMillis = 0) 461 | } else { 462 | spring() 463 | }, 464 | label = "Offset" 465 | ) 466 | 467 | offsetState = offset 468 | } 469 | 470 | /** 471 | * Converts [shadowOffsetState] and [shadowTranslationOffsetState] from [DpOffset] to [Offset] if [ShadowConfig] provided. 472 | * 473 | * @see LevitationContainer 474 | */ 475 | @Composable 476 | internal fun ConvertShadowOffsets() { 477 | shadowConfig?.let { 478 | val density = LocalDensity.current 479 | 480 | @Composable 481 | fun rememberAndConvertOffsetToPx(offset: DpOffset): Offset { 482 | return remember(offset) { 483 | with(density) { 484 | Offset( 485 | x = offset.x.toPx(), 486 | y = offset.y.toPx() 487 | ) 488 | } 489 | } 490 | } 491 | 492 | shadowTranslationOffsetState = rememberAndConvertOffsetToPx(offset = it.translationOffset) 493 | 494 | if (it.isPivotedWhenPressed) { 495 | shadowOffsetState = when (it.type) { 496 | is ShadowType.Elevation -> { 497 | Offset.Zero 498 | } 499 | is ShadowType.ShadowsPlus.SoftLayer -> { 500 | rememberAndConvertOffsetToPx(offset = it.type.offset) 501 | } 502 | is ShadowType.ShadowsPlus.RSBlur -> { 503 | rememberAndConvertOffsetToPx(offset = it.type.offset) 504 | } 505 | } 506 | } 507 | } 508 | } 509 | 510 | /** 511 | * Handles levitation [rawDragOffsetState] and checks whether current [change] drag event is within levitation content bounds, and if no, then the 512 | * [rawDragOffsetState] calculation will be stopped (when [isDragBlockedUntilRelease] equals false, or basically released press). 513 | * 514 | * @param change Current [PointerInputChange] drag event data. 515 | * @see LevitationContainer 516 | */ 517 | internal fun handleDrag( 518 | change: PointerInputChange 519 | ) { 520 | // Skip drag calculation until release, if any drag event is outside content bounds. 521 | if (isDragBlockedUntilRelease) { 522 | return 523 | } 524 | 525 | val isOutOfBounds = change.isOutOfBounds( 526 | size = intSizeState, 527 | extendedTouchPadding = Size.Zero 528 | ) 529 | isPressedState = isOutOfBounds.not() 530 | isDraggedState = isPressedState 531 | 532 | if (isOutOfBounds) { 533 | rawPressOffsetState = rawDragOffsetState 534 | return 535 | } 536 | 537 | change.consume() 538 | 539 | rawDragOffsetState = change.position 540 | } 541 | 542 | /** 543 | * Cancels the pending [isAwaitPressedState] indicator. 544 | * 545 | * @see AnimatePressFraction 546 | * @see LevitationContainer 547 | */ 548 | internal fun cancelPendingAwaitPressedState() { 549 | if (pressConfig.isAwaitPressAnimation) { 550 | isAwaitPressedState = false 551 | } 552 | } 553 | 554 | /** 555 | * Handles [PressConfig.isAwaitPressAnimation] behaviour ([isAwaitPressedState] and [isAwaitPressedAnimationFinishedState]) on press release. 556 | * 557 | * @see AnimatePressFraction 558 | * @see LevitationContainer 559 | */ 560 | internal fun handleAwaitPressedState() { 561 | if (isAwaitPressedAnimationFinishedState) { 562 | isPressedState = false 563 | } else { 564 | isAwaitPressedState = true 565 | } 566 | } 567 | 568 | /** 569 | * Calculates levitation downscale based on provided [PressConfig.downscale], [PressConfig.type], and [downscaleMultiplier] 570 | * 571 | * @param downscaleMultiplier The downscale multiplier fraction. Used for [shadowScale]. 572 | * @return Calculated scale specifically for [Modifier.levitation]. 573 | * @see scale 574 | * @see shadowScale 575 | * @see pressFraction 576 | */ 577 | private fun calculateScale(downscaleMultiplier: Float = 1.0F): Float = with(pressConfig) { 578 | val downscaleFraction = pressFraction * downscale 579 | val fraction = downscaleFraction * downscaleMultiplier 580 | 581 | when (type) { 582 | PressType.Full -> { 583 | 1.0F - fraction 584 | } 585 | is PressType.Ranged -> { 586 | val radialDegreeOffset = max( 587 | degreeProgress.x.absoluteValue, 588 | degreeProgress.y.absoluteValue 589 | ).subLerp( 590 | start = type.start, 591 | stop = type.stop 592 | ) 593 | 594 | 1.0F - (1.0F - radialDegreeOffset) * fraction 595 | } 596 | PressType.None -> { 597 | 1.0F 598 | } 599 | } 600 | } 601 | 602 | /** 603 | * Calculates levitation offset progress by its axis in [offsetProgress]. Range from 0.0F to 1.0F. 604 | * 605 | * @param offsetAxis The levitation offset axis. 606 | * @param defaultOffsetAxis The levitation offset default axis, basically the [LevitationConfig.pivot] axis. 607 | * @param sizeAxis The levitation [size] axis. 608 | * @return Calculated single axis for [offsetProgress]. 609 | */ 610 | private fun calculateOffsetAxisProgress( 611 | offsetAxis: Float, 612 | defaultOffsetAxis: Float, 613 | sizeAxis: Float 614 | ): Float { 615 | if (sizeAxis <= 0) { 616 | return defaultOffsetAxis 617 | } 618 | 619 | return (offsetAxis / sizeAxis).coerceIn(0.0F, 1.0F) 620 | } 621 | 622 | /** 623 | * Calculates levitation degree progress by its axis in [degreeProgress]. Range from -1.0F to 1.0F. The (0.0F, 0.0F) degree progress is at the [pivotAxis]. 624 | * 625 | * @param offsetProgressAxis The levitation [offsetProgress] axis. 626 | * @param pivotAxis The [LevitationConfig.pivot] axis. 627 | * @return Calculated single axis for [degreeProgress]. 628 | */ 629 | private fun calculateDegreeAxisProgress( 630 | offsetProgressAxis: Float, 631 | pivotAxis: Float 632 | ): Float = if (offsetProgressAxis <= pivotAxis) { 633 | offsetProgressAxis.subLerp(0.0F, pivotAxis) - 1.0F 634 | } else { 635 | offsetProgressAxis.subLerp(pivotAxis, 1.0F) 636 | } 637 | } -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/common/LevitationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.common 2 | 3 | import androidx.compose.animation.core.Easing 4 | 5 | /** 6 | * Performs a linear interpolation (lerp) between two values ([start] and [stop]) using a sub-fraction of a given [Float]. 7 | * 8 | * Optionally, an [Easing] function can be provided to modify the interpolation curve. 9 | * 10 | * @param start The starting value of the interpolation. 11 | * @param stop The ending value of the interpolation. 12 | * @param easing An optional [Easing] function to modify the interpolation curve. 13 | * @return The interpolated value based on the sub-fraction of the current [Float]. 14 | * @see LevitationState 15 | * @author GIGAMOLE 16 | */ 17 | fun Float.subLerp(start: Float, stop: Float, easing: Easing? = null): Float { 18 | return when { 19 | start == stop -> start 20 | this < start -> 0.0F 21 | this > stop -> 1.0F 22 | else -> { 23 | val subFraction = (this - start) / (stop - start) 24 | 25 | easing?.transform(subFraction) ?: subFraction 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/levitation/LevitationConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.levitation 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.graphics.Shape 5 | import com.gigamole.composelevitation.common.LevitationState 6 | import com.gigamole.composelevitation.press.PressConfig 7 | import com.gigamole.composelevitation.shadow.ShadowConfig 8 | 9 | /** 10 | * A required main levitation configuration for [LevitationState]. 11 | * 12 | * @property orientation The [LevitationOrientation]. 13 | * @property degree The levitation degree. Can be positive or negative. 14 | * @property pivot The levitation pivot point. Range from 0.0F to 1.0F. Set to the center point with an offset of (0.5F, 0.5F). 15 | * @property cameraDistance The levitation camera distance. Range from 0.0F. 16 | * @property isBounded Indicates whether levitation is bounded to its size or not. 17 | * @property shape The shape of the levitation content and its shadow. 18 | * @property isClipped Indicates whether levitation gesture events are clipped (and the content) within the [shape] or not. 19 | * @see LevitationState 20 | * @see PressConfig 21 | * @see ShadowConfig 22 | * @author GIGAMOLE 23 | */ 24 | data class LevitationConfig( 25 | val orientation: LevitationOrientation = LevitationDefaults.Orientation, 26 | val degree: Float = LevitationDefaults.Degree, 27 | val pivot: Offset = LevitationDefaults.Pivot, 28 | val cameraDistance: Float = LevitationDefaults.CameraDistance, 29 | val isBounded: Boolean = LevitationDefaults.IsBounded, 30 | val shape: Shape = LevitationDefaults.Shape, 31 | val isClipped: Boolean = LevitationDefaults.IsClipped 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/levitation/LevitationContainer.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalComposeUiApi::class) 2 | 3 | package com.gigamole.composelevitation.levitation 4 | 5 | import android.view.MotionEvent 6 | import androidx.compose.foundation.gestures.awaitEachGesture 7 | import androidx.compose.foundation.gestures.detectDragGestures 8 | import androidx.compose.foundation.gestures.detectTapGestures 9 | import androidx.compose.foundation.gestures.waitForUpOrCancellation 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.IntrinsicSize 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.ExperimentalComposeUiApi 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.draw.shadow 21 | import androidx.compose.ui.geometry.Offset 22 | import androidx.compose.ui.graphics.GraphicsLayerScope 23 | import androidx.compose.ui.graphics.TransformOrigin 24 | import androidx.compose.ui.graphics.graphicsLayer 25 | import androidx.compose.ui.input.pointer.PointerEventType 26 | import androidx.compose.ui.input.pointer.pointerInput 27 | import androidx.compose.ui.input.pointer.pointerInteropFilter 28 | import androidx.compose.ui.layout.onPlaced 29 | import com.gigamole.composelevitation.common.LevitationState 30 | import com.gigamole.composelevitation.common.rememberLevitationState 31 | import com.gigamole.composelevitation.press.PressConfig 32 | import com.gigamole.composelevitation.shadow.ShadowConfig 33 | import com.gigamole.composelevitation.shadow.ShadowType 34 | import com.gigamole.composeshadowsplus.rsblur.rsBlurShadow 35 | import com.gigamole.composeshadowsplus.softlayer.softLayerShadow 36 | 37 | /** 38 | * A levitation effect [Composable]. 39 | * 40 | * This container applies levitation effect to its [content] based on the provided [state]. The parent container consists of two pieces: an optional shadow content and 41 | * the levitation [content]. Basically, the optional shadow content controlled by the [ShadowConfig]. Yet, levitation [content] is controlled by whole [state] and does 42 | * complex inter-manipulation between [PressConfig] and [LevitationConfig]. 43 | * 44 | * @param modifier The parent container [Modifier]. 45 | * @param state The [LevitationState]. 46 | * @param isEnabled Indicates whether the levitation effect is enabled or not. 47 | * @param content The content with levitation effect. 48 | * @see Modifier.levitation 49 | * @see LevitationConfig 50 | * @see PressConfig 51 | * @see ShadowConfig 52 | * @author GIGAMOLE 53 | */ 54 | @Composable 55 | fun LevitationContainer( 56 | modifier: Modifier = Modifier, 57 | state: LevitationState = rememberLevitationState(), 58 | isEnabled: Boolean = true, 59 | content: @Composable () -> Unit 60 | ) { 61 | // Handle Composable based values in state. 62 | with(state) { 63 | ConvertShadowOffsets() 64 | AnimatePressFraction() 65 | AnimateLevitationOffset() 66 | } 67 | 68 | // Parent container. IntrinsicSize.Min is required to limit max contents size. 69 | Box( 70 | modifier = modifier 71 | .height(intrinsicSize = IntrinsicSize.Min) 72 | .width(intrinsicSize = IntrinsicSize.Min), 73 | contentAlignment = Alignment.Center 74 | ) { 75 | // Optional shadow content. 76 | state.shadowConfig?.let { 77 | Box(contentAlignment = Alignment.Center) { 78 | Box( 79 | modifier = Modifier 80 | .fillMaxSize() 81 | .then( 82 | if (isEnabled) { 83 | Modifier.levitation( 84 | state = state, 85 | isShadow = true 86 | ) 87 | } else { 88 | Modifier 89 | } 90 | ) 91 | .then( 92 | when (it.type) { 93 | is ShadowType.Elevation -> { 94 | with(it.type) { 95 | Modifier.shadow( 96 | elevation = it.radius, 97 | shape = state.levitationConfig.shape, 98 | clip = isClipped, 99 | ambientColor = ambientColor, 100 | spotColor = spotColor, 101 | ) 102 | } 103 | } 104 | is ShadowType.ShadowsPlus.SoftLayer -> { 105 | Modifier.softLayerShadow( 106 | radius = it.radius, 107 | shape = state.levitationConfig.shape, 108 | color = it.type.color, 109 | spread = it.type.spread, 110 | offset = it.type.offset, 111 | ) 112 | } 113 | is ShadowType.ShadowsPlus.RSBlur -> { 114 | Modifier.rsBlurShadow( 115 | radius = it.radius, 116 | shape = state.levitationConfig.shape, 117 | color = it.type.color, 118 | spread = it.type.spread, 119 | offset = it.type.offset, 120 | ) 121 | } 122 | } 123 | ) 124 | ) 125 | } 126 | } 127 | 128 | Box( 129 | modifier = Modifier 130 | .fillMaxSize() 131 | .onPlaced { 132 | state.intSizeState = it.size 133 | } 134 | .then( 135 | if (isEnabled) { 136 | Modifier 137 | .then( 138 | // A little bit odd, but required to handle the gesture clipping properly. 139 | if (state.levitationConfig.isClipped) { 140 | Modifier 141 | .levitation( 142 | state = state, 143 | isShadow = false 144 | ) 145 | .clip(shape = state.levitationConfig.shape) 146 | .levitationPointer(state = state) 147 | } else { 148 | Modifier 149 | .levitationPointer(state = state) 150 | .levitation( 151 | state = state, 152 | isShadow = false 153 | ) 154 | } 155 | ) 156 | } else { 157 | Modifier 158 | } 159 | ) 160 | ) { 161 | content() 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * A [Modifier] to apply levitation effect. 168 | * 169 | * Basically, [LevitationState] built around [GraphicsLayerScope] to match its values. 170 | * 171 | * @param state The [LevitationState]. 172 | * @param isShadow Indicates whether levitation effect applied to shadow or not. 173 | * @return The modified [Modifier] with applied levitation effect. 174 | * @author GIGAMOLE 175 | */ 176 | fun Modifier.levitation( 177 | state: LevitationState, 178 | isShadow: Boolean 179 | ): Modifier { 180 | return then( 181 | Modifier.graphicsLayer { 182 | with(state) { 183 | val scale = if (isShadow) shadowScale else scale 184 | 185 | // Apply levitation downscale. 186 | scaleX = scale 187 | scaleY = scale 188 | 189 | // Apply levitation pivot point. 190 | transformOrigin = if (levitationConfig.isBounded) { 191 | TransformOrigin( 192 | pivotFractionX = 1.0F - offsetProgress.x, 193 | pivotFractionY = 1.0F - offsetProgress.y 194 | ) 195 | } else { 196 | TransformOrigin( 197 | pivotFractionX = levitationConfig.pivot.x, 198 | pivotFractionY = levitationConfig.pivot.y 199 | ) 200 | } 201 | 202 | if (isShadow) { 203 | // Apply levitation shadow translation offset. 204 | translationX = shadowTranslation.x 205 | translationY = shadowTranslation.y 206 | 207 | // Apply levitation shadow degree rotation. 208 | rotationX = shadowRotation.x 209 | rotationY = shadowRotation.y 210 | 211 | // Apply levitation shadow camera distance. 212 | cameraDistance = shadowCameraDistance 213 | } else { 214 | // Apply levitation content degree rotation. 215 | rotationX = rotation.x 216 | rotationY = rotation.y 217 | 218 | // Apply levitation content camera distance. 219 | cameraDistance = levitationConfig.cameraDistance 220 | } 221 | } 222 | } 223 | ) 224 | } 225 | 226 | /** 227 | * A [Modifier] to handle levitation gesture events: press, release, and drag. 228 | * 229 | * @param state The [LevitationState]. 230 | * @return The modified [Modifier] with levitation gestures handling. 231 | * @author GIGAMOLE 232 | */ 233 | private fun Modifier.levitationPointer( 234 | state: LevitationState 235 | ): Modifier = this.then( 236 | with(state) { 237 | Modifier 238 | .pointerInteropFilter( 239 | onTouchEvent = { 240 | // Intercept down events, specifically for clickable content. 241 | if (it.action == MotionEvent.ACTION_DOWN) { 242 | rawPressOffsetState = Offset( 243 | x = it.x, 244 | y = it.y 245 | ) 246 | isPressedState = true 247 | 248 | cancelPendingAwaitPressedState() 249 | } 250 | 251 | false 252 | } 253 | ) 254 | .pointerInput(this) { 255 | awaitEachGesture { 256 | // Handle release events, basically single clicks/taps. 257 | if (isPressedState) { 258 | if (currentEvent.type == PointerEventType.Release) { 259 | if (pressConfig.isAwaitPressAnimation) { 260 | handleAwaitPressedState() 261 | } else { 262 | isPressedState = false 263 | } 264 | } 265 | } 266 | 267 | waitForUpOrCancellation() 268 | } 269 | } 270 | .pointerInput(this) { 271 | detectTapGestures( 272 | // Handle tap events if upstream didn't. 273 | onPress = { 274 | rawPressOffsetState = it 275 | isPressedState = true 276 | 277 | awaitRelease() 278 | 279 | if (pressConfig.isAwaitPressAnimation) { 280 | isAwaitPressedState = true 281 | } else { 282 | isPressedState = false 283 | } 284 | } 285 | ) 286 | } 287 | .pointerInput(this) { 288 | // Handle drag events. 289 | detectDragGestures( 290 | onDragStart = { 291 | isPressedState = true 292 | isDraggedState = true 293 | 294 | cancelPendingAwaitPressedState() 295 | }, 296 | onDragEnd = { 297 | rawPressOffsetState = rawDragOffsetState 298 | isPressedState = false 299 | isDraggedState = false 300 | }, 301 | onDragCancel = { 302 | rawPressOffsetState = rawDragOffsetState 303 | isPressedState = false 304 | isDraggedState = false 305 | }, 306 | onDrag = { change, _ -> 307 | handleDrag(change = change) 308 | } 309 | ) 310 | } 311 | } 312 | ) -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/levitation/LevitationDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.levitation 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.graphics.DefaultCameraDistance 5 | import androidx.compose.ui.graphics.GraphicsLayerScope 6 | import androidx.compose.ui.graphics.RectangleShape 7 | import com.gigamole.composelevitation.shadow.ShadowConfig 8 | 9 | /** 10 | * Default values for [LevitationConfig]. 11 | * 12 | * @author GIGAMOLE 13 | */ 14 | object LevitationDefaults { 15 | 16 | /** The default [LevitationOrientation]. */ 17 | val Orientation = LevitationOrientation.All 18 | 19 | /** 20 | * The default levitation degree. Can be positive or negative. 21 | * 22 | * @see GraphicsLayerScope 23 | */ 24 | const val Degree = 4.0F 25 | 26 | /** 27 | * The default levitation pivot point. Range from 0.0F to 1.0F. 28 | * 29 | * Set to the center point with an offset of (0.5F, 0.5F). 30 | * 31 | * @see GraphicsLayerScope 32 | */ 33 | val Pivot = Offset( 34 | x = 0.5F, 35 | y = 0.5F 36 | ) 37 | 38 | /** 39 | * The default levitation camera distance. Range from 0.0F. 40 | * 41 | * @see GraphicsLayerScope 42 | */ 43 | const val CameraDistance = DefaultCameraDistance 44 | 45 | /** Indicates whether levitation is bounded to its size or not. */ 46 | const val IsBounded = false 47 | 48 | /** 49 | * The default shape of the levitation content and its shadow. 50 | * 51 | * @see LevitationContainer 52 | * @see ShadowConfig 53 | */ 54 | val Shape = RectangleShape 55 | 56 | /** 57 | * Indicates whether levitation gesture events are clipped (and the content) within the [Shape] or not. 58 | * 59 | * @see LevitationContainer 60 | */ 61 | const val IsClipped = true 62 | } -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/levitation/LevitationOrientation.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.levitation 2 | 3 | import com.gigamole.composelevitation.common.LevitationState 4 | 5 | /** 6 | * Levitation orientations. 7 | * 8 | * @see LevitationConfig 9 | * @see LevitationState 10 | * @author GIGAMOLE 11 | */ 12 | enum class LevitationOrientation { 13 | 14 | /** Levitation can occur in all directions (both [Horizontal] and [Vertical]). */ 15 | All, 16 | 17 | /** Levitation can occur only horizontally. */ 18 | Horizontal, 19 | 20 | /** Levitation can occur only vertically. */ 21 | Vertical 22 | } 23 | -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/press/PressConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.press 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import com.gigamole.composelevitation.common.LevitationState 5 | import com.gigamole.composelevitation.levitation.LevitationConfig 6 | import com.gigamole.composelevitation.shadow.ShadowConfig 7 | 8 | /** 9 | * A required levitation press configuration for [LevitationState]. 10 | * 11 | * @property type The levitation [PressType]. 12 | * @property downscale The levitation press downscale. Can be positive or negative. 13 | * @property pressAnimationSpec The levitation press animation specification. 14 | * @property isAwaitPressAnimation Indicates whether to await full press animation on release or not. 15 | * @property isPivotedWhenReleased Indicates whether to pivot the press point when released or not. 16 | * @see LevitationState 17 | * @see LevitationConfig 18 | * @see ShadowConfig 19 | * @author GIGAMOLE 20 | */ 21 | data class PressConfig( 22 | val type: PressType = PressDefaults.Config.Type, 23 | val downscale: Float = PressDefaults.Config.Downscale, 24 | val pressAnimationSpec: AnimationSpec = PressDefaults.Config.PressAnimationSpec, 25 | val isAwaitPressAnimation: Boolean = PressDefaults.Config.IsAwaitPressAnimation, 26 | val isPivotedWhenReleased: Boolean = PressDefaults.Config.IsPivotedWhenReleased 27 | ) -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/press/PressDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.press 2 | 3 | import androidx.compose.animation.core.spring 4 | import com.gigamole.composelevitation.levitation.LevitationConfig 5 | 6 | /** 7 | * Default values for [PressConfig] and [PressType]. 8 | * 9 | * @author GIGAMOLE 10 | */ 11 | object PressDefaults { 12 | 13 | /** Default configuration values for [PressConfig]. */ 14 | object Config { 15 | 16 | /** The default [PressType]. */ 17 | val Type = PressType.Ranged() 18 | 19 | /** The default press downscale. Can be positive or negative. */ 20 | const val Downscale = 0.03F 21 | 22 | /** The default animation specification for press. */ 23 | val PressAnimationSpec 24 | get() = spring() 25 | 26 | /** Indicates whether to await full press animation on release or not. */ 27 | const val IsAwaitPressAnimation = false 28 | 29 | /** 30 | * Indicates whether to pivot the press point when released or not. 31 | * 32 | * @see LevitationConfig.pivot 33 | */ 34 | const val IsPivotedWhenReleased = false 35 | } 36 | 37 | /** Default configuration values for [PressType] enumeration. */ 38 | object Type { 39 | 40 | /** Default configuration values for [PressType.Ranged]. Range from 0.0F to 1.0F. */ 41 | object Ranged { 42 | 43 | /** The default start fraction (from pivot point) for [PressType.Ranged]. */ 44 | const val Start = 0.0F 45 | 46 | /** The default stop fraction (from content bounds) for [PressType.Ranged]. */ 47 | const val Stop = 1.0F 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/press/PressType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.press 2 | 3 | import com.gigamole.composelevitation.levitation.LevitationConfig 4 | 5 | /** 6 | * Levitation press types. 7 | * 8 | * @author GIGAMOLE 9 | */ 10 | sealed class PressType { 11 | 12 | /** 13 | * Press type for ranged (interpolated) downscale press between [start] and [stop] fractions. Range from 0.0F to 1.0F. 14 | * 15 | * @property start The start fraction (from pivot point). 16 | * @property stop The stop fraction (from content bounds). 17 | * @see LevitationConfig.pivot 18 | */ 19 | data class Ranged( 20 | val start: Float = PressDefaults.Type.Ranged.Start, 21 | val stop: Float = PressDefaults.Type.Ranged.Stop 22 | ) : PressType() 23 | 24 | /** Press type for automatic full downscale press. */ 25 | data object Full : PressType() 26 | 27 | /** Press type for none downscale press. */ 28 | data object None : PressType() 29 | } 30 | -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/shadow/ShadowConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.shadow 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.DpOffset 5 | import com.gigamole.composelevitation.common.LevitationState 6 | import com.gigamole.composelevitation.levitation.LevitationConfig 7 | import com.gigamole.composelevitation.press.PressConfig 8 | 9 | /** 10 | * An optional levitation shadow configuration for [LevitationState]. 11 | * 12 | * The shadow shape set automatically to the [LevitationConfig.shape]. 13 | * 14 | * @property degreeMultiplier The levitation shadow degree multiplier. Can be positive or negative. 15 | * @property downscaleMultiplier The levitation shadow downscale multiplier. Can be positive or negative. 16 | * @property cameraDistanceMultiplier The levitation shadow camera distance multiplier. Can be positive or negative. 17 | * @property translationOffset The levitation shadow translation offset. Can be positive or negative. 18 | * @property isPivotedWhenPressed Indicates whether levitation shadow is pivoted when fully pressed at pivot. 19 | * @property radius The levitation shadow radius. 20 | * @property type The levitation [ShadowType]. 21 | * @see LevitationState 22 | * @see LevitationConfig 23 | * @see PressConfig 24 | * @see com.gigamole.composeshadowsplus 25 | * @author GIGAMOLE 26 | */ 27 | data class ShadowConfig( 28 | val degreeMultiplier: Float = ShadowDefaults.Config.DegreeMultiplier, 29 | val downscaleMultiplier: Float = ShadowDefaults.Config.DownscaleMultiplier, 30 | val cameraDistanceMultiplier: Float = ShadowDefaults.Config.CameraDistanceMultiplier, 31 | val translationOffset: DpOffset = ShadowDefaults.Config.TranslationOffset, 32 | val isPivotedWhenPressed: Boolean = ShadowDefaults.Config.IsPivotedWhenPressed, 33 | val radius: Dp = ShadowDefaults.Config.Radius, 34 | val type: ShadowType = ShadowDefaults.Config.Type 35 | ) 36 | 37 | -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/shadow/ShadowDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.shadow 2 | 3 | import androidx.compose.ui.graphics.DefaultShadowColor 4 | import androidx.compose.ui.unit.DpOffset 5 | import androidx.compose.ui.unit.dp 6 | import com.gigamole.composelevitation.levitation.LevitationConfig 7 | import com.gigamole.composelevitation.press.PressConfig 8 | import com.gigamole.composeshadowsplus.common.ShadowsPlusDefaults 9 | 10 | /** 11 | * Default values for [ShadowConfig] and [ShadowType]. 12 | * 13 | * @author GIGAMOLE 14 | */ 15 | object ShadowDefaults { 16 | 17 | /** Default configuration values for [ShadowConfig]. */ 18 | object Config { 19 | 20 | /** 21 | * The default shadow degree multiplier. Can be positive or negative. 22 | * 23 | * @see LevitationConfig.degree 24 | */ 25 | const val DegreeMultiplier = 1.1F 26 | 27 | /** 28 | * The default shadow downscale multiplier. Can be positive or negative. 29 | * 30 | * @see PressConfig.downscale 31 | */ 32 | const val DownscaleMultiplier = 2.5F 33 | 34 | /** 35 | * The default shadow camera distance multiplier. Can be positive or negative. 36 | * 37 | * @see LevitationConfig.cameraDistance 38 | */ 39 | const val CameraDistanceMultiplier = 0.9F 40 | 41 | /** The shadow default translation offset. Can be positive or negative. */ 42 | val TranslationOffset: DpOffset = DpOffset( 43 | x = 3.dp, 44 | y = 6.dp 45 | ) 46 | 47 | /** 48 | * Indicates whether shadow is pivoted when fully pressed at pivot. 49 | * 50 | * @see LevitationConfig.pivot 51 | */ 52 | const val IsPivotedWhenPressed = true 53 | 54 | /** 55 | * The default shadow radius. 56 | * 57 | * @see ShadowsPlusDefaults 58 | */ 59 | val Radius = ShadowsPlusDefaults.ShadowRadius 60 | 61 | /** The default [ShadowType]. */ 62 | val Type = ShadowType.ShadowsPlus.RSBlur() 63 | } 64 | 65 | /** Default configuration values for [ShadowType]. */ 66 | object Type { 67 | 68 | /** Default configuration values for [ShadowType.Elevation]. */ 69 | object Elevation { 70 | 71 | /** Indicates whether elevation shadow is clipped or not. */ 72 | const val IsClipped = true 73 | 74 | /** The default elevation shadow ambient color. */ 75 | val AmbientColor = DefaultShadowColor 76 | 77 | /** The default elevation shadow spot color. */ 78 | val SpotColor = DefaultShadowColor 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /ComposeLevitation/src/main/kotlin/com/gigamole/composelevitation/shadow/ShadowType.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.gigamole.composelevitation.shadow 4 | 5 | import android.renderscript.RenderScript 6 | import android.renderscript.ScriptIntrinsicBlur 7 | import android.view.View 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.DpOffset 12 | import com.gigamole.composeshadowsplus.common.ShadowsPlusDefaults 13 | import com.gigamole.composeshadowsplus.common.ShadowsPlusType 14 | import com.gigamole.composeshadowsplus.rsblur.RSBlurShadowDefaults 15 | import com.gigamole.composeshadowsplus.rsblur.rsBlurShadow 16 | import com.gigamole.composeshadowsplus.softlayer.SoftLayerShadowContainer 17 | import com.gigamole.composeshadowsplus.softlayer.softLayerShadow 18 | import android.graphics.Paint as NativePaint 19 | 20 | /** 21 | * Levitation shadow types. 22 | * 23 | * @author GIGAMOLE 24 | */ 25 | sealed class ShadowType { 26 | 27 | /** 28 | * Elevation shadow. 29 | * 30 | * The ComposeLevitation library advises against using elevation shadow due to a rendering issue where the shadow appears cropped and only visible at the content 31 | * border, creating an empty rectangle below the surface. 32 | * 33 | * @property isClipped Indicates whether the content drawing clips to the shape or not. 34 | * @property ambientColor The elevation ambient color. 35 | * @property spotColor The elevation spot color. 36 | */ 37 | data class Elevation( 38 | val isClipped: Boolean = ShadowDefaults.Type.Elevation.IsClipped, 39 | val ambientColor: Color = ShadowDefaults.Type.Elevation.AmbientColor, 40 | val spotColor: Color = ShadowDefaults.Type.Elevation.SpotColor 41 | ) : ShadowType() 42 | 43 | /** Custom shadows (bridge to: [ShadowsPlusType]). */ 44 | sealed class ShadowsPlus : ShadowType() { 45 | 46 | abstract val color: Color 47 | abstract val offset: DpOffset 48 | abstract val spread: Dp 49 | 50 | /** 51 | * A [NativePaint.setShadowLayer]/[View.LAYER_TYPE_SOFTWARE] custom shadow. 52 | * 53 | * You must use it with [SoftLayerShadowContainer]. 54 | * 55 | * @property color The shadow color. 56 | * @property offset The shadow offset. Can be positive or negative. 57 | * @property spread The shadow spread. Can be positive or negative. 58 | * @see Modifier.softLayerShadow 59 | */ 60 | data class SoftLayer( 61 | override val color: Color = ShadowsPlusDefaults.ShadowColor, 62 | override val offset: DpOffset = ShadowsPlusDefaults.ShadowOffset, 63 | override val spread: Dp = ShadowsPlusDefaults.ShadowSpread 64 | ) : ShadowsPlus() 65 | 66 | /** 67 | * A [RenderScript]/[ScriptIntrinsicBlur] custom shadow. 68 | * 69 | * @property color The shadow color. 70 | * @property offset The shadow offset. Can be positive or negative. 71 | * @property spread The shadow spread. Can be positive or negative. 72 | * @property isRadiusAligned Indicates whether the shadow radius is exponentially aligned. 73 | * @see Modifier.rsBlurShadow 74 | */ 75 | data class RSBlur( 76 | override val color: Color = ShadowsPlusDefaults.ShadowColor, 77 | override val offset: DpOffset = ShadowsPlusDefaults.ShadowOffset, 78 | override val spread: Dp = ShadowsPlusDefaults.ShadowSpread, 79 | val isRadiusAligned: Boolean = RSBlurShadowDefaults.RSBlurShadowAlignRadius 80 | ) : ShadowsPlus() 81 | } 82 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GIGAMOLE gigamole53@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](/media/header-new.png) 2 | 3 | ![](https://jitpack.io/v/GIGAMOLE/ComposeLevitation.svg?style=flat-square) | [Setup Guide](#setup) 4 | | [Report new issue](https://github.com/GIGAMOLE/ComposeLevitation/issues/new) 5 | 6 | # ComposeLevitation 7 | 8 | The `ComposeLevitation` is a powerful Android Compose library that empowers developers with the ability to easily create and customize levitation effects, adding an 9 | interactive and mesmerizing touch to their UI elements. 10 | 11 | ![](/media/demo.gif) 12 | 13 | Features: 14 | 15 | - **Levitation Effect:** Simulates a pressed surface with a shadow, creating a sense of depth and interactivity. 16 | - **Gesture Responsiveness:** Reacts to press and drag gestures for intuitive user interaction. 17 | - **Customization:** Fully customizable parameters such as camera distance, press angle, animation, upscale/downscale, shadow translation, offset, and color. 18 | - **Advanced Shadows:** Enhances the levitation shadow effect with [ComposeShadowsPlus](https://github.com/GIGAMOLE/ComposeShadowsPlus). 19 | - **Sample App:** Explore and experiment with [sample app](#sample-app). 20 | - **Holographic Effect:** Explore and experiment with [holographic effect](#holographic-effect). 21 | 22 | ## Sample App 23 | 24 | | Shape Levitation | Shape Press | Shape Shadow | 25 | |-|-|-| 26 | | | | | 27 | 28 | | Image Levitation | Image Press | Image Shadow | 29 | |-|-|-| 30 | | | | | 31 | 32 | Download or clone this repository to discover the sample app. 33 | 34 | ## Holographic Effect 35 | 36 | First ever **Android Holographic Effect**: 37 | 38 | https://github.com/GIGAMOLE/ComposeLevitation/assets/7150913/803bd0db-fc6b-4cb2-a741-7d13b26a30e9 39 | 40 | Inspired by the [Halo Lab -](https://dribbble.com/halolab) [Holographic Guide in Figma](https://dribbble.com/shots/15004777-Holographic-Guide-in-Figma) 41 | and [junhoyeo -](https://github.com/) [Holographic 42 | Effect Generator](https://holo.junho.io/). 43 | 44 | Download or clone this repository to discover 45 | the [holographic effect sample](https://github.com/GIGAMOLE/ComposeLevitation/blob/main/app/src/main/kotlin/com/gigamole/composelevitation/sample/MainScreenHolographicContent.kt) 46 | . 47 | 48 | ## Setup 49 | 50 | Add to the root `build.gradle.kts`: 51 | 52 | ``` groovy 53 | allprojects { 54 | repositories { 55 | ... 56 | maven("https://jitpack.io") 57 | } 58 | } 59 | ``` 60 | 61 | Add to the package `build.gradle.kts`: 62 | 63 | ``` groovy 64 | dependencies { 65 | implementation("com.github.GIGAMOLE:ComposeLevitation:{latest-version}") 66 | } 67 | ``` 68 | 69 | Latest version: ![](https://jitpack.io/v/GIGAMOLE/ComposeLevitation.svg?style=flat-square). 70 | 71 | Also, it's possible to download the latest artifact from the [releases page](https://github.com/GIGAMOLE/ComposeLevitation/releases). 72 | 73 | ## Guide 74 | 75 | `ComposeLevitation` comes with two main components: [`LevitationContainer`](#levitationcontainer) and [`LevitationState`](#levitationstate). 76 | 77 | For more technical and detailed documentation, read the library `KDoc`. 78 | 79 | ### LevitationContainer 80 | 81 | The `LevitationContainer` applies levitation effect to the provided content based on the provided [`LevitationState`](#levitationstate). 82 | 83 | You can enable or disable the levitation effect with `isEnabled` attribute. 84 | 85 | ### LevitationState 86 | 87 | The `LevitationState` consists of two required components: [`LevitationConfig`](#levitationconfig) and [`PressConfig`](#pressconfig). And one optional 88 | component: [`ShadowConfig`](#shadowconfig). 89 | 90 | To create a `LevitationState`, use the utility function: `rememberLevitationState(...)` or make it on your own. 91 | 92 | #### LevitationConfig 93 | 94 | The `LevitationConfig` setups the core levitation effect: 95 | 96 | |Param|Description| 97 | |-|-| 98 | |`orientation`|The levitation orientation: `All`, `Horizontal`, and `Vertical`.| 99 | |`degree`|The levitation degree (angle).| 100 | |`pivot`|The levitation origin pivot point.| 101 | |`cameraDistance`|The levitation camera distance.| 102 | |`isBounded`|Indicates whether levitation is bounded to its size or not.| 103 | |`shape`|The shape of the levitation content and its shadow.| 104 | |`isClipped`|Indicates whether gesture events are clipped (and the content) within their shape or not.| 105 | 106 | #### PressConfig 107 | 108 | The `PressConfig` setups the levitation gesture (press, drag, and release) effect: 109 | 110 | |Param|Description| 111 | |-|-| 112 | |`type`|The levitation press type: `Ranged`(interpolated), `Full`, and `None`.| 113 | |`downscale`|The levitation press downscale or upscale.| 114 | |`pressAnimationSpec`|The levitation press animation specification.| 115 | |`isAwaitPressAnimation`|Indicates whether to await full press animation on a release or not.| 116 | |`isPivotedWhenReleased`|Indicates whether to pivot the press point when released or not.| 117 | 118 | #### ShadowConfig 119 | 120 | An optional `ShadowConfig` setups the levitation shadow effect: 121 | 122 | |Param|Description| 123 | |-|-| 124 | |`degreeMultiplier`|The levitation shadow degree (angle) multiplier.| 125 | |`downscaleMultiplier`|The levitation shadow downscale or upscale multiplier.| 126 | |`cameraDistanceMultiplier`|The levitation shadow camera distance multiplier.| 127 | |`translationOffset`|The levitation shadow translation offset.| 128 | |`isPivotedWhenPressed`|Indicates whether the levitation shadow is pivoted when fully pressed at the pivot.| 129 | |`radius`|The levitation shadow radius.| 130 | |`type`|The levitation shadow type: `SoftLayer`, `RSBlur`, and `Elevation`.| 131 | 132 | The `SoftLayer` shadow type is required to be used with `SoftLayerShadowContainer`. 133 | 134 | > The `ComposeLevitation` library advises against using the `Elevation` shadow due to a rendering issue where the shadow appears cropped and only visible at the content 135 | > border, 136 | > creating an empty rectangle below the surface. 137 | 138 | The levitation shadow effect is powered by the [ComposeShadowsPlus](https://github.com/GIGAMOLE/ComposeShadowsPlus). You can explore it for more. 139 | 140 | ## License 141 | 142 | MIT License. See the [LICENSE](https://github.com/GIGAMOLE/ComposeLevitation/blob/master/LICENSE) file for more details. 143 | 144 | ## Credits 145 | 146 | Special thanks to the [GoDaddy](https://github.com/godaddy) for the amazing [color picker library](https://github.com/godaddy/compose-color-picker). 147 | 148 | Inspired by the uncompleted Android Google `elevation` feature and `Steam Trading Cards` hover behavior: 149 | 150 | |Steam Example 1|Steam Example 2| 151 | |-|-| 152 | ||| 153 | 154 | ## Author: 155 | 156 | [Basil Miller](https://www.linkedin.com/in/gigamole/) 157 | [gigamole53@gmail.com](mailto:gigamole53@gmail.com) 158 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | @Suppress( 4 | "DSL_SCOPE_VIOLATION", 5 | "MISSING_DEPENDENCY_CLASS", 6 | "UNRESOLVED_REFERENCE_WRONG_RECEIVER", 7 | "FUNCTION_CALL_EXPECTED" 8 | ) 9 | 10 | plugins { 11 | id("composelevitation.application") 12 | } 13 | 14 | android { 15 | val appId = "${ProjectConfig.namespace}.sample" 16 | 17 | namespace = appId 18 | 19 | defaultConfig { 20 | applicationId = appId 21 | } 22 | 23 | composeOptions { 24 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.version.get() 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation(projects.composeLevitation) 30 | 31 | implementation(libs.compose.color.picker) 32 | 33 | implementation(libs.androidx.ktx) 34 | 35 | implementation(platform(libs.compose.bom)) 36 | implementation(libs.bundles.compose) 37 | implementation(libs.compose.ui.util) 38 | 39 | debugImplementation(libs.bundles.debug.compose) 40 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep,includedescriptorclasses class net.sqlcipher.** { *; } 24 | -keep,includedescriptorclasses interface net.sqlcipher.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composelevitation/sample/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.sample 2 | 3 | import android.app.Application 4 | 5 | class MainApplication : Application() -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composelevitation/sample/MainScreenDemoContent.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.sample 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.LaunchedEffect 17 | import androidx.compose.runtime.derivedStateOf 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.rememberCoroutineScope 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.geometry.Offset 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.graphics.RectangleShape 28 | import androidx.compose.ui.layout.onPlaced 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.unit.IntSize 31 | import androidx.compose.ui.unit.center 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.util.lerp 34 | import com.gigamole.composelevitation.common.rememberLevitationState 35 | import com.gigamole.composelevitation.levitation.LevitationContainer 36 | import com.gigamole.composelevitation.press.PressConfig 37 | import com.gigamole.composelevitation.press.PressType 38 | import com.gigamole.composelevitation.shadow.ShadowConfig 39 | import com.gigamole.composelevitation.shadow.ShadowType 40 | import com.gigamole.composeshadowsplus.softlayer.SoftLayerShadowContainer 41 | import kotlinx.coroutines.delay 42 | import kotlinx.coroutines.launch 43 | import kotlin.math.cos 44 | import kotlin.math.sin 45 | 46 | @Suppress("unused") 47 | @Composable 48 | fun MainScreenDemoContent() { 49 | val shape = remember { RectangleShape } 50 | 51 | val startAngle = 20.0F 52 | val finishAngle = -380.0F 53 | val angleZoneOffset = 20.0F 54 | val startZoneAngle = startAngle - angleZoneOffset 55 | val finishZoneAngle = finishAngle + angleZoneOffset 56 | 57 | var size by remember { mutableStateOf(IntSize.Zero) } 58 | var isCycling by remember { mutableStateOf(false) } 59 | val angle = remember { 60 | Animatable(initialValue = 65.0F) 61 | } 62 | 63 | val radians by remember(angle.value) { 64 | derivedStateOf { 65 | Math.toRadians(angle.value.toDouble()).toFloat() 66 | } 67 | } 68 | 69 | val rawOffset by remember(radians) { 70 | derivedStateOf { 71 | val startFraction = 1.0F - ((angle.value - startZoneAngle) / (startAngle - startZoneAngle)).coerceIn(0.0F, 1.0F) 72 | val finishFraction = 1.0F - ((angle.value - finishZoneAngle) / (finishAngle - finishZoneAngle)).coerceIn(0.0F, 1.0F) 73 | val fraction = when (angle.value) { 74 | in startZoneAngle..startAngle -> startFraction 75 | else -> finishFraction 76 | } 77 | 78 | Offset( 79 | x = size.center.x + lerp(0.0F, size.center.x.toFloat(), fraction) * sin(radians), 80 | y = size.center.y + lerp(0.0F, size.center.y.toFloat(), fraction) * cos(radians), 81 | ) 82 | } 83 | } 84 | val pressPivot by remember(rawOffset) { 85 | derivedStateOf { 86 | Offset( 87 | x = rawOffset.x / size.width.toFloat(), 88 | y = rawOffset.y / size.height.toFloat() 89 | ) 90 | } 91 | } 92 | 93 | var isPressAnimation by remember { mutableStateOf(false) } 94 | val coroutineScope = rememberCoroutineScope() 95 | val shadowConfig = remember { 96 | ShadowConfig( 97 | type = ShadowType.ShadowsPlus.SoftLayer(), 98 | downscaleMultiplier = 3.0F 99 | ) 100 | } 101 | val pressState = rememberLevitationState( 102 | pressConfig = PressConfig( 103 | type = PressType.Full, 104 | pressAnimationSpec = tween(durationMillis = 500) 105 | ), 106 | shadowConfig = shadowConfig 107 | ) 108 | val cyclingState = rememberLevitationState( 109 | pressConfig = PressConfig( 110 | type = PressType.None, 111 | pressAnimationSpec = tween(durationMillis = 500) 112 | ), 113 | shadowConfig = shadowConfig 114 | ) 115 | 116 | SoftLayerShadowContainer { 117 | Box( 118 | modifier = Modifier 119 | .fillMaxSize() 120 | .clickable( 121 | interactionSource = MutableInteractionSource(), 122 | indication = null 123 | ) { 124 | coroutineScope.launch { 125 | isCycling = false 126 | isPressAnimation = true 127 | delay(500) 128 | pressState.press() 129 | delay(1000) 130 | isPressAnimation = false 131 | angle.snapTo(targetValue = startAngle) 132 | isCycling = true 133 | } 134 | }, 135 | contentAlignment = Alignment.Center 136 | ) { 137 | LevitationContainer( 138 | modifier = Modifier, 139 | state = if (isPressAnimation) pressState else cyclingState 140 | ) { 141 | Box( 142 | modifier = Modifier 143 | .background( 144 | color = Color.White, 145 | shape = shape 146 | ) 147 | .padding( 148 | horizontal = 32.dp, 149 | vertical = 24.dp 150 | ) 151 | .onPlaced { 152 | size = it.size 153 | } 154 | ) { 155 | Column(verticalArrangement = Arrangement.spacedBy(space = 6.dp)) { 156 | Text( 157 | text = "ComposeLevitation", 158 | style = MaterialTheme.typography.displaySmall, 159 | fontWeight = FontWeight.Bold, 160 | fontFamily = FontFamilySpaceGrotesk 161 | ) 162 | Text( 163 | text = "Empower Android Compose UI with levitation effect", 164 | style = MaterialTheme.typography.labelLarge, 165 | fontWeight = FontWeight.Normal, 166 | fontFamily = FontFamilyOpenSans 167 | ) 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | LaunchedEffect(pressPivot, isPressAnimation, isCycling) { 175 | if (pressPivot != Offset.Unspecified && isPressAnimation.not() && isCycling) { 176 | cyclingState.press(pivot = pressPivot) 177 | } 178 | } 179 | LaunchedEffect(isCycling) { 180 | if (isCycling) { 181 | angle.animateTo( 182 | targetValue = finishAngle, 183 | animationSpec = tween(durationMillis = 2500) 184 | ) 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composelevitation/sample/MainScreenHolographicContent.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.sample 2 | 3 | import androidx.compose.animation.core.tween 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.layout.width 17 | import androidx.compose.foundation.shape.RoundedCornerShape 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.drawWithContent 24 | import androidx.compose.ui.geometry.Offset 25 | import androidx.compose.ui.geometry.center 26 | import androidx.compose.ui.geometry.toRect 27 | import androidx.compose.ui.graphics.BlendMode 28 | import androidx.compose.ui.graphics.Brush 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.graphics.ColorFilter 31 | import androidx.compose.ui.graphics.CompositingStrategy 32 | import androidx.compose.ui.graphics.ImageBitmap 33 | import androidx.compose.ui.graphics.ImageShader 34 | import androidx.compose.ui.graphics.LinearGradientShader 35 | import androidx.compose.ui.graphics.Paint 36 | import androidx.compose.ui.graphics.Shape 37 | import androidx.compose.ui.graphics.TileMode 38 | import androidx.compose.ui.graphics.asAndroidBitmap 39 | import androidx.compose.ui.graphics.asImageBitmap 40 | import androidx.compose.ui.graphics.drawscope.Stroke 41 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 42 | import androidx.compose.ui.graphics.drawscope.rotate 43 | import androidx.compose.ui.graphics.drawscope.scale 44 | import androidx.compose.ui.graphics.drawscope.translate 45 | import androidx.compose.ui.graphics.graphicsLayer 46 | import androidx.compose.ui.graphics.nativeCanvas 47 | import androidx.compose.ui.layout.ContentScale 48 | import androidx.compose.ui.platform.LocalDensity 49 | import androidx.compose.ui.res.imageResource 50 | import androidx.compose.ui.res.painterResource 51 | import androidx.compose.ui.unit.DpOffset 52 | import androidx.compose.ui.unit.dp 53 | import androidx.core.graphics.scale 54 | import com.gigamole.composelevitation.common.rememberLevitationState 55 | import com.gigamole.composelevitation.levitation.LevitationConfig 56 | import com.gigamole.composelevitation.levitation.LevitationContainer 57 | import com.gigamole.composelevitation.press.PressConfig 58 | import com.gigamole.composelevitation.press.PressType 59 | import com.gigamole.composelevitation.shadow.ShadowConfig 60 | import com.gigamole.composelevitation.shadow.ShadowType 61 | import com.gigamole.composeshadowsplus.softlayer.SoftLayerShadowContainer 62 | import kotlin.math.abs 63 | 64 | private val sampleHolographicColors = listOf( 65 | Color(0xFF9FDAFF), 66 | Color(0xFFFEF1A5), 67 | Color(0xFFFBA1C9), 68 | Color(0xFFAB90D3), 69 | Color(0xFF9FDAFF), 70 | Color(0xFFFBB466), 71 | ) 72 | private val sampleHolographicAltColors = listOf( 73 | Color(0xFF2AD0CA), 74 | Color(0xFFE1F664), 75 | Color(0xFFFEB0FE), 76 | Color(0xFFABB3FC), 77 | Color(0xFF5DF7A4), 78 | Color(0xFF58C4F6), 79 | ) 80 | 81 | private val sampleHolographicMetalColors = listOf( 82 | Color.White, 83 | Color.Black, 84 | Color.White, 85 | Color.Black, 86 | Color.White, 87 | Color.Black, 88 | Color.White, 89 | Color.Black, 90 | Color.White 91 | ) 92 | 93 | private val sampleBorderHighlightColors = listOf( 94 | Color.White.copy(alpha = 0.5F), 95 | Color.White.copy(alpha = 0.0F) 96 | ) 97 | private val sampleBorderHighlightSoftColors = listOf( 98 | Color.White.copy(alpha = 0.3F), 99 | Color.White.copy(alpha = 0.0F) 100 | ) 101 | 102 | private val sampleBorderHighdarkColors = listOf( 103 | Color.Black.copy(alpha = 0.5F), 104 | Color.Black.copy(alpha = 0.0F) 105 | ) 106 | private val sampleBorderHighdarkSoftColors = listOf( 107 | Color.Black.copy(alpha = 0.3F), 108 | Color.Black.copy(alpha = 0.0F) 109 | ) 110 | 111 | @Suppress("unused") 112 | @Composable 113 | fun MainScreenHolographicContent() { 114 | val mainShape = RoundedCornerShape(size = 24.dp) 115 | 116 | // Gathering working bitmaps. 117 | val noiseImageBitmap = ImageBitmap.imageResource(id = R.drawable.noise) 118 | val scale = with(LocalDensity.current) { 65.dp.roundToPx() } 119 | val logoImageBitmap = ImageBitmap.imageResource(id = R.drawable.logo).let { 120 | // Adjusting logo size. 121 | it.asAndroidBitmap().scale( 122 | width = it.width * scale / it.height, 123 | height = scale 124 | ) 125 | }.asImageBitmap() 126 | 127 | // Prepare LevitationState. 128 | val state = rememberLevitationState( 129 | levitationConfig = LevitationConfig( 130 | shape = mainShape, 131 | pivot = Offset( 132 | x = 0.5F, 133 | y = 0.4F 134 | ), 135 | cameraDistance = 7.0F 136 | ), 137 | pressConfig = PressConfig( 138 | type = PressType.Ranged(), 139 | downscale = 0.04F, 140 | pressAnimationSpec = tween(durationMillis = 500), 141 | isAwaitPressAnimation = true 142 | ), 143 | shadowConfig = ShadowConfig( 144 | type = ShadowType.ShadowsPlus.SoftLayer( 145 | offset = DpOffset.Zero, 146 | color = Color(0xFF61707F).copy(alpha = 0.4F), 147 | spread = 0.dp 148 | ), 149 | degreeMultiplier = 1.0F, 150 | downscaleMultiplier = 1.0F, 151 | cameraDistanceMultiplier = 0.9F, 152 | translationOffset = DpOffset( 153 | x = 2.dp, 154 | y = 2.dp 155 | ), 156 | radius = 1.dp, 157 | ) 158 | ) 159 | 160 | // Modifier for a lighter side highlight. 161 | fun Modifier.highlightBorder( 162 | shape: Shape, 163 | start: Offset, 164 | end: Offset, 165 | isStart: Boolean, 166 | isBig: Boolean = false, 167 | isSoft: Boolean = false 168 | ): Modifier = 169 | border( 170 | width = if (isBig) { 171 | 2.dp 172 | } else { 173 | 1.dp 174 | }, 175 | shape = shape, 176 | brush = Brush.linearGradient( 177 | colors = if (isSoft) { 178 | sampleBorderHighlightSoftColors 179 | } else { 180 | sampleBorderHighlightColors 181 | }.let { 182 | if (isStart) { 183 | it 184 | } else { 185 | it.reversed() 186 | } 187 | }, 188 | start = start, 189 | end = end, 190 | tileMode = TileMode.Clamp 191 | ) 192 | ) 193 | 194 | // Modifier for a darker side highlight. 195 | fun Modifier.highdarkBorder( 196 | shape: Shape, 197 | start: Offset, 198 | end: Offset, 199 | isStart: Boolean, 200 | isBig: Boolean = false, 201 | isSoft: Boolean = false 202 | ): Modifier = 203 | border( 204 | width = if (isBig) { 205 | 2.dp 206 | } else { 207 | 1.dp 208 | }, 209 | shape = shape, 210 | brush = Brush.linearGradient( 211 | colors = if (isSoft) { 212 | sampleBorderHighdarkSoftColors 213 | } else { 214 | sampleBorderHighdarkColors 215 | }.let { 216 | if (isStart) { 217 | it 218 | } else { 219 | it.reversed() 220 | } 221 | }, 222 | start = start, 223 | end = end, 224 | tileMode = TileMode.Clamp 225 | ) 226 | ) 227 | 228 | SoftLayerShadowContainer { 229 | Box( 230 | modifier = Modifier.fillMaxSize(), 231 | contentAlignment = Alignment.Center 232 | ) { 233 | Image( 234 | modifier = Modifier.fillMaxSize(), 235 | painter = painterResource(id = R.drawable.scene), 236 | contentDescription = "", 237 | contentScale = ContentScale.Crop, 238 | alignment = Alignment.Center 239 | ) 240 | 241 | LevitationContainer( 242 | modifier = Modifier 243 | .width(width = 250.dp) 244 | .height(height = 270.dp), 245 | state = state 246 | ) { 247 | Box( 248 | modifier = Modifier 249 | .background( 250 | color = Color.White, 251 | shape = mainShape 252 | ) 253 | // Applying card container highlights. 254 | .highlightBorder( 255 | shape = mainShape, 256 | start = Offset.Zero, 257 | end = state.size.center.times(operand = 1.75F), 258 | isStart = true, 259 | isBig = true 260 | ) 261 | .highdarkBorder( 262 | shape = mainShape, 263 | start = state.size.center.times(operand = 0.25F), 264 | end = Offset( 265 | x = state.size.width, 266 | y = state.size.height 267 | ), 268 | isStart = false, 269 | isBig = true 270 | ) 271 | .drawWithContent { 272 | val pivot = Offset( 273 | x = size.center.x, 274 | y = size.height * 0.4F 275 | ) 276 | val degreeFraction = maxOf(abs(state.degreeProgress.x), abs(state.degreeProgress.y)) 277 | 278 | // Drawing main holographic gradient. 279 | drawRect( 280 | brush = Brush.radialGradient( 281 | colorStops = sampleHolographicColors.let { 282 | arrayOf( 283 | (0.0F + 0.1F * degreeFraction) to it[0], 284 | (0.2F + 0.1F * degreeFraction) to it[1], 285 | (0.4F + 0.08F * degreeFraction) to it[2], 286 | (0.6F + 0.08F * degreeFraction) to it[3], 287 | (0.8F + 0.06F * degreeFraction) to it[4], 288 | (1.0F + 0.06F * degreeFraction) to it[5], 289 | ) 290 | }, 291 | center = Offset( 292 | x = (size.width * 0.5F) + (size.width * 0.25F * -state.degreeProgress.x * state.pressFraction), 293 | y = (size.height * 0.4F) + (size.height * 0.2F * -state.degreeProgress.y * state.pressFraction) 294 | ), 295 | radius = (size.width * 0.75F) + (size.height * 0.6F * degreeFraction * state.pressFraction), 296 | tileMode = TileMode.Mirror 297 | ), 298 | ) 299 | // Drawing support holographic gradient. 300 | drawRect( 301 | brush = Brush.radialGradient( 302 | colorStops = sampleHolographicAltColors.let { 303 | arrayOf( 304 | (0.0F + 0.1F * degreeFraction) to it[0], 305 | (0.2F + 0.1F * degreeFraction) to it[1], 306 | (0.4F + 0.08F * degreeFraction) to it[2], 307 | (0.6F + 0.08F * degreeFraction) to it[3], 308 | (0.8F + 0.06F * degreeFraction) to it[4], 309 | (1.0F + 0.06F * degreeFraction) to it[5], 310 | ) 311 | }, 312 | center = Offset( 313 | x = (size.width * 0.5F) + (size.width * 0.5F * state.degreeProgress.x * state.pressFraction), 314 | y = (size.height * 0.6F) + (size.height * 0.5F * state.degreeProgress.y * state.pressFraction) 315 | ), 316 | radius = (size.height * 0.9F) - (size.height * 0.35F * (1.0F - degreeFraction) * state.pressFraction), 317 | tileMode = TileMode.Clamp 318 | ), 319 | alpha = 0.5F + (0.2F * degreeFraction), 320 | blendMode = BlendMode.Overlay 321 | ) 322 | 323 | val rawSweepFraction = (state.degreeProgress.x + state.degreeProgress.y) * state.pressFraction 324 | val sweepFraction = rawSweepFraction * 0.3F 325 | 326 | // Skewing sweep gradients on different angles. 327 | scale( 328 | scaleX = 1.5F + 0.3F * state.degreeProgress.x * state.pressFraction, 329 | scaleY = 1.5F - 0.3F * state.degreeProgress.y * state.pressFraction, 330 | pivot = pivot 331 | ) { 332 | // Rotating sweep gradients to start diagonally. 333 | rotate( 334 | degrees = 45.0F + 8.0F * rawSweepFraction, 335 | pivot = pivot 336 | ) { 337 | // Drawing main sweep gradient. 338 | drawRect( 339 | topLeft = size.center.times(operand = -2.0F), 340 | size = size.times(operand = 3.0F), 341 | brush = Brush.sweepGradient( 342 | colorStops = sampleHolographicMetalColors.let { 343 | arrayOf( 344 | 0.0F to it[0], 345 | (0.15F + (0.1F * sweepFraction)) to it[1], 346 | (0.25F + (0.08F * sweepFraction)) to it[2], 347 | (0.4F + (0.05F * sweepFraction)) to it[3], 348 | (0.5F + (0.05F * sweepFraction)) to it[4], 349 | (0.55F + (0.03F * sweepFraction)) to it[5], 350 | (0.76F + (0.1F * sweepFraction)) to it[6], 351 | (0.87F + (0.05F * sweepFraction)) to it[7], 352 | 1.0F to it[8], 353 | ) 354 | }, 355 | center = pivot 356 | ), 357 | alpha = 1.0F - (0.25F * (1.0F - degreeFraction) * state.pressFraction), 358 | blendMode = BlendMode.Difference 359 | ) 360 | 361 | // Drawing support sweep gradient. 362 | drawRect( 363 | topLeft = size.center.times(operand = -2.0F), 364 | size = size.times(operand = 3.0F), 365 | brush = Brush.sweepGradient( 366 | colorStops = sampleHolographicMetalColors.let { 367 | arrayOf( 368 | 0.0F to it[0], 369 | (0.12F + (0.12F * sweepFraction)) to it[1], 370 | (0.23F + (0.05F * sweepFraction)) to it[2], 371 | (0.38F + (0.1F * sweepFraction)) to it[3], 372 | (0.52F + (0.12F * sweepFraction)) to it[4], 373 | (0.6F + (0.1F * sweepFraction)) to it[5], 374 | (0.75F + (0.05F * sweepFraction)) to it[6], 375 | (0.86F + (0.07F * sweepFraction)) to it[7], 376 | 1.0F to it[8], 377 | ) 378 | }, 379 | center = pivot 380 | ), 381 | alpha = 1.0F - (0.2F * (1.0F - degreeFraction) * state.pressFraction), 382 | blendMode = BlendMode.Screen 383 | ) 384 | } 385 | } 386 | drawIntoCanvas { 387 | // Drawing noise bitmap as a Paint with tile mode support. 388 | it.nativeCanvas.drawPaint( 389 | Paint() 390 | .apply { 391 | alpha = 0.4F 392 | blendMode = BlendMode.Overlay 393 | } 394 | .asFrameworkPaint() 395 | .apply { 396 | isAntiAlias = true 397 | isDither = true 398 | 399 | shader = ImageShader( 400 | image = noiseImageBitmap, 401 | tileModeX = TileMode.Repeated, 402 | tileModeY = TileMode.Repeated 403 | ) 404 | } 405 | ) 406 | } 407 | drawContent() 408 | } 409 | .padding(all = 9.dp) 410 | // Applying card outline outer border highlights. 411 | .highdarkBorder( 412 | shape = RoundedCornerShape(size = 15.dp), 413 | start = Offset( 414 | x = 0.0F, 415 | y = 0.0F 416 | ), 417 | end = state.size.center.times(operand = 1.65F), 418 | isStart = true 419 | ) 420 | .highlightBorder( 421 | shape = RoundedCornerShape(size = 15.dp), 422 | start = state.size.center.times(operand = 0.35F), 423 | end = Offset( 424 | x = state.size.width * 0.85F, 425 | y = state.size.height * 0.85F 426 | ), 427 | isStart = false 428 | ) 429 | .padding(all = 1.dp) 430 | .highdarkBorder( 431 | shape = RoundedCornerShape(size = 14.dp), 432 | start = Offset( 433 | x = 0.0F, 434 | y = 0.0F 435 | ), 436 | end = state.size.center.times(operand = 1.65F), 437 | isStart = true 438 | ) 439 | .highlightBorder( 440 | shape = RoundedCornerShape(size = 14.dp), 441 | start = state.size.center.times(operand = 0.35F), 442 | end = Offset( 443 | x = state.size.width * 0.85F, 444 | y = state.size.height * 0.85F 445 | ), 446 | isStart = false 447 | ) 448 | ) { 449 | // Modifier for a black outlines with a spot highlighter. 450 | fun Modifier.spotHighlighter(): Modifier = 451 | graphicsLayer { 452 | compositingStrategy = CompositingStrategy.Offscreen 453 | }.drawWithContent { 454 | drawContent() 455 | drawIntoCanvas { 456 | drawRect( 457 | brush = Brush.radialGradient( 458 | colors = listOf( 459 | Color.White.copy(alpha = 0.4F), 460 | Color.Transparent 461 | ), 462 | center = Offset( 463 | x = size.width * state.offsetProgress.x * state.pressFraction, 464 | y = size.height * state.offsetProgress.y * state.pressFraction 465 | ), 466 | radius = size.height * 1.25F, 467 | tileMode = TileMode.Clamp, 468 | ), 469 | alpha = 0.8F - 0.2F * state.offsetProgress.y * state.pressFraction, 470 | blendMode = BlendMode.SrcAtop 471 | ) 472 | } 473 | } 474 | 475 | // Recreating card structure and apply spot highlighter to the black outlines. 476 | Box( 477 | modifier = Modifier 478 | .fillMaxSize() 479 | .spotHighlighter() 480 | ) { 481 | Column( 482 | modifier = Modifier 483 | .fillMaxSize() 484 | .border( 485 | width = 6.dp, 486 | color = Color.Black, 487 | shape = RoundedCornerShape(size = 14.dp) 488 | ) 489 | ) { 490 | Box( 491 | modifier = Modifier 492 | .fillMaxWidth() 493 | .weight(weight = 1.0F) 494 | .padding(all = 6.dp), 495 | contentAlignment = Alignment.Center 496 | ) { 497 | Box( 498 | modifier = Modifier 499 | .size(size = 100.dp) 500 | .background( 501 | color = Color.Black, 502 | shape = RoundedCornerShape(size = 8.dp) 503 | ) 504 | ) 505 | } 506 | Box( 507 | modifier = Modifier 508 | .fillMaxWidth() 509 | .height(height = 6.dp) 510 | .background(color = Color.Black) 511 | ) 512 | Box( 513 | modifier = Modifier 514 | .fillMaxWidth() 515 | .padding(bottom = 6.dp) 516 | .height(height = 32.dp) 517 | ) 518 | } 519 | } 520 | 521 | // Main card content. 522 | Column( 523 | modifier = Modifier.fillMaxSize(), 524 | verticalArrangement = Arrangement.Center, 525 | horizontalAlignment = Alignment.CenterHorizontally 526 | ) { 527 | Box( 528 | modifier = Modifier 529 | .fillMaxWidth() 530 | .weight(weight = 1.0F) 531 | .padding( 532 | top = 5.dp, 533 | start = 5.dp, 534 | end = 5.dp, 535 | bottom = 4.dp 536 | ) 537 | // Applying top card content border highlights. 538 | .highlightBorder( 539 | shape = RoundedCornerShape( 540 | topStart = 9.dp, 541 | topEnd = 9.dp 542 | ), 543 | start = Offset( 544 | x = state.size.width * 0.1F, 545 | y = state.size.height * 0.1F 546 | ), 547 | end = state.size.center.times(operand = 1.45F), 548 | isStart = true, 549 | isSoft = true 550 | ) 551 | .highdarkBorder( 552 | shape = RoundedCornerShape( 553 | topStart = 9.dp, 554 | topEnd = 9.dp 555 | ), 556 | start = state.size.center.times(operand = 0.45F), 557 | end = Offset( 558 | x = state.size.width * 0.9F, 559 | y = state.size.height * 0.9F 560 | ), 561 | isStart = false, 562 | isSoft = true 563 | ) 564 | .padding(all = 1.dp) 565 | .highlightBorder( 566 | shape = RoundedCornerShape( 567 | topStart = 8.dp, 568 | topEnd = 8.dp 569 | ), 570 | start = Offset( 571 | x = state.size.width * 0.1F, 572 | y = state.size.height * 0.1F 573 | ), 574 | end = state.size.center.times(operand = 1.45F), 575 | isStart = true, 576 | isSoft = true 577 | ) 578 | .highdarkBorder( 579 | shape = RoundedCornerShape( 580 | topStart = 8.dp, 581 | topEnd = 8.dp 582 | ), 583 | start = state.size.center.times(operand = 0.45F), 584 | end = Offset( 585 | x = state.size.width * 0.9F, 586 | y = state.size.height * 0.9F 587 | ), 588 | isStart = false, 589 | isSoft = true 590 | ), 591 | contentAlignment = Alignment.Center 592 | ) { 593 | Box( 594 | modifier = Modifier 595 | .padding(bottom = 6.dp) 596 | .size(size = 102.dp) 597 | // Applying center logo black outline border highlights. 598 | .highdarkBorder( 599 | shape = RoundedCornerShape(size = 9.dp), 600 | start = Offset.Zero, 601 | end = Offset( 602 | x = state.size.width * 0.32F, 603 | y = state.size.height * 0.24F 604 | ), 605 | isStart = true 606 | ) 607 | .highlightBorder( 608 | shape = RoundedCornerShape(size = 9.dp), 609 | start = Offset( 610 | x = state.size.width * 0.13F, 611 | y = state.size.height * 0.11F 612 | ), 613 | end = Offset( 614 | x = state.size.width * 0.3F, 615 | y = state.size.height * 0.28F 616 | ), 617 | isStart = false 618 | ) 619 | .padding(all = 1.dp) 620 | .highdarkBorder( 621 | shape = RoundedCornerShape(size = 8.dp), 622 | start = Offset.Zero, 623 | end = Offset( 624 | x = state.size.width * 0.32F, 625 | y = state.size.height * 0.24F 626 | ), 627 | isStart = true 628 | ) 629 | .highlightBorder( 630 | shape = RoundedCornerShape(size = 8.dp), 631 | start = Offset( 632 | x = state.size.width * 0.13F, 633 | y = state.size.height * 0.11F 634 | ), 635 | end = Offset( 636 | x = state.size.width * 0.3F, 637 | y = state.size.height * 0.28F 638 | ), 639 | isStart = false 640 | ), 641 | contentAlignment = Alignment.Center 642 | ) { 643 | val offset = with(LocalDensity.current) { 1.dp.toPx() } 644 | 645 | // Applying sweep gradient to the logo and draw it with a border highlights. 646 | Box( 647 | modifier = Modifier 648 | .size(size = 67.dp) 649 | .graphicsLayer { 650 | compositingStrategy = CompositingStrategy.Offscreen 651 | } 652 | .drawWithContent { 653 | val degreeFraction = maxOf(abs(state.degreeProgress.x), abs(state.degreeProgress.y)) 654 | val rawSweepFraction = (state.degreeProgress.x + state.degreeProgress.y) * state.pressFraction 655 | val sweepFraction = rawSweepFraction * 0.5F 656 | 657 | drawIntoCanvas { canvas -> 658 | translate( 659 | left = center.x - logoImageBitmap.width / 2.0F, 660 | top = center.y - logoImageBitmap.height / 2.0F 661 | ) { 662 | drawImage( 663 | image = logoImageBitmap, 664 | colorFilter = ColorFilter.tint(color = Color.White) 665 | ) 666 | drawRect( 667 | brush = Brush.sweepGradient( 668 | colorStops = sampleHolographicMetalColors.let { 669 | arrayOf( 670 | 0.0F to it[0], 671 | (0.15F + (0.1F * sweepFraction)) to it[1], 672 | (0.25F + (0.08F * sweepFraction)) to it[2], 673 | (0.4F + (0.05F * sweepFraction)) to it[3], 674 | (0.5F + (0.05F * sweepFraction)) to it[4], 675 | (0.55F + (0.03F * sweepFraction)) to it[5], 676 | (0.76F + (0.1F * sweepFraction)) to it[6], 677 | (0.87F + (0.05F * sweepFraction)) to it[7], 678 | 1.0F to it[8], 679 | ) 680 | }, 681 | center = Offset( 682 | x = size.width * 0.1F, 683 | y = -size.height * 0.15F 684 | ) 685 | ), 686 | alpha = 0.8F - (0.4F * (1.0F - degreeFraction) * state.pressFraction), 687 | blendMode = BlendMode.SrcAtop 688 | ) 689 | } 690 | canvas.saveLayer( 691 | bounds = size.toRect(), 692 | paint = Paint().apply { 693 | alpha = 0.4F 694 | } 695 | ) 696 | translate( 697 | left = center.x - logoImageBitmap.width / 2.0F, 698 | top = center.y - logoImageBitmap.height / 2.0F 699 | ) { 700 | drawImage( 701 | image = logoImageBitmap, 702 | topLeft = Offset( 703 | x = -offset, 704 | y = -offset 705 | ) 706 | ) 707 | drawImage( 708 | image = logoImageBitmap, 709 | topLeft = Offset( 710 | x = offset, 711 | y = -offset 712 | ) 713 | ) 714 | drawImage( 715 | image = logoImageBitmap, 716 | topLeft = Offset( 717 | x = offset, 718 | y = offset 719 | ) 720 | ) 721 | drawImage( 722 | image = logoImageBitmap, 723 | topLeft = Offset( 724 | x = -offset, 725 | y = offset 726 | ) 727 | ) 728 | canvas.nativeCanvas.drawPaint( 729 | Paint() 730 | .apply { 731 | shader = LinearGradientShader( 732 | colors = listOf( 733 | Color.White, 734 | Color.Black 735 | ), 736 | from = Offset( 737 | x = size.width * 0.3F, 738 | y = size.height * 0.3F 739 | ), 740 | to = Offset( 741 | x = size.width * 0.7F, 742 | y = size.height * 0.7F 743 | ), 744 | ) 745 | blendMode = BlendMode.SrcAtop 746 | } 747 | .asFrameworkPaint() 748 | ) 749 | } 750 | canvas.restore() 751 | } 752 | } 753 | ) 754 | } 755 | } 756 | Box( 757 | modifier = Modifier 758 | .fillMaxWidth() 759 | .padding( 760 | start = 5.dp, 761 | end = 5.dp, 762 | bottom = 5.dp 763 | ) 764 | // Applying border highlights to the bottom card content panel. 765 | .highlightBorder( 766 | shape = RoundedCornerShape( 767 | bottomStart = 9.dp, 768 | bottomEnd = 9.dp 769 | ), 770 | start = Offset( 771 | x = state.size.width * 0.1F, 772 | y = state.size.height * 0.8F 773 | ), 774 | end = Offset( 775 | x = state.size.width * 0.2F, 776 | y = state.size.height * 0.87F 777 | ), 778 | isStart = true, 779 | isSoft = true 780 | ) 781 | .highdarkBorder( 782 | shape = RoundedCornerShape( 783 | bottomStart = 9.dp, 784 | bottomEnd = 9.dp 785 | ), 786 | start = Offset( 787 | x = state.size.center.x * 0.9F, 788 | y = state.size.height * 0.85F 789 | ), 790 | end = Offset( 791 | x = state.size.width * 0.9F, 792 | y = state.size.height * 0.9F 793 | ), 794 | isStart = false, 795 | isSoft = true 796 | ) 797 | .padding(all = 1.dp) 798 | .highlightBorder( 799 | shape = RoundedCornerShape( 800 | bottomStart = 8.dp, 801 | bottomEnd = 8.dp 802 | ), 803 | start = Offset( 804 | x = state.size.width * 0.1F, 805 | y = state.size.height * 0.8F 806 | ), 807 | end = Offset( 808 | x = state.size.width * 0.2F, 809 | y = state.size.height * 0.87F 810 | ), 811 | isStart = true, 812 | isSoft = true 813 | ) 814 | .highdarkBorder( 815 | shape = RoundedCornerShape( 816 | bottomStart = 8.dp, 817 | bottomEnd = 8.dp 818 | ), 819 | start = Offset( 820 | x = state.size.center.x * 0.9F, 821 | y = state.size.height * 0.85F 822 | ), 823 | end = Offset( 824 | x = state.size.width * 0.9F, 825 | y = state.size.height * 0.9F 826 | ), 827 | isStart = false, 828 | isSoft = true 829 | ) 830 | .height(height = 32.dp), 831 | contentAlignment = Alignment.Center 832 | ) { 833 | val heightPx = with(LocalDensity.current) { 20.dp.toPx() } 834 | val widthPx = with(LocalDensity.current) { 18.dp.toPx() } 835 | val strokePx = with(LocalDensity.current) { 2.dp.toPx() } 836 | 837 | // Drawing each letter separately to apply border highlight to each. 838 | Row( 839 | modifier = Modifier.fillMaxWidth(), 840 | horizontalArrangement = Arrangement.spacedBy( 841 | space = 2.dp, 842 | alignment = Alignment.CenterHorizontally 843 | ), 844 | verticalAlignment = Alignment.CenterVertically 845 | ) { 846 | "GIGAMOLE".forEach { 847 | Box(contentAlignment = Alignment.Center) { 848 | Text( 849 | modifier = Modifier.padding(bottom = 1.dp), 850 | text = it.toString(), 851 | style = MaterialTheme.typography.titleLarge.copy( 852 | brush = Brush.linearGradient( 853 | colors = sampleBorderHighlightSoftColors.asReversed(), 854 | start = Offset( 855 | x = 0.0F, 856 | y = 0.0F 857 | ), 858 | end = Offset( 859 | x = widthPx, 860 | y = heightPx 861 | ), 862 | tileMode = TileMode.Clamp 863 | ), 864 | drawStyle = Stroke(width = strokePx), 865 | ), 866 | fontFamily = FontFamilyDaysOne 867 | ) 868 | 869 | Text( 870 | modifier = Modifier.padding(bottom = 1.dp), 871 | text = it.toString(), 872 | style = MaterialTheme.typography.titleLarge.copy( 873 | brush = Brush.linearGradient( 874 | colors = sampleBorderHighdarkSoftColors, 875 | start = Offset( 876 | x = 0.0F, 877 | y = 0.0F 878 | ), 879 | end = Offset( 880 | x = widthPx, 881 | y = heightPx 882 | ), 883 | tileMode = TileMode.Clamp 884 | ), 885 | drawStyle = Stroke(width = strokePx), 886 | ), 887 | fontFamily = FontFamilyDaysOne 888 | ) 889 | } 890 | } 891 | } 892 | } 893 | } 894 | 895 | // Recreating the card content structure again to draw a card title with a spot highlighter. 896 | Box( 897 | modifier = Modifier 898 | .fillMaxSize() 899 | .spotHighlighter() 900 | ) { 901 | Column( 902 | modifier = Modifier.fillMaxSize() 903 | ) { 904 | Box( 905 | modifier = Modifier 906 | .fillMaxWidth() 907 | .weight(weight = 1.0F) 908 | ) 909 | Box( 910 | modifier = Modifier 911 | .fillMaxWidth() 912 | .height(height = 6.dp) 913 | ) 914 | Box( 915 | modifier = Modifier 916 | .fillMaxWidth() 917 | .padding(bottom = 6.dp) 918 | .height(height = 32.dp), 919 | contentAlignment = Alignment.Center 920 | ) { 921 | Row( 922 | modifier = Modifier.fillMaxWidth(), 923 | horizontalArrangement = Arrangement.spacedBy( 924 | space = 2.dp, 925 | alignment = Alignment.CenterHorizontally 926 | ), 927 | verticalAlignment = Alignment.CenterVertically 928 | ) { 929 | "GIGAMOLE".forEach { 930 | Text( 931 | modifier = Modifier.padding(bottom = 1.dp), 932 | text = it.toString(), 933 | style = MaterialTheme.typography.titleLarge, 934 | fontFamily = FontFamilyDaysOne 935 | ) 936 | } 937 | } 938 | } 939 | } 940 | } 941 | } 942 | } 943 | } 944 | } 945 | } 946 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composelevitation/sample/MainTheme.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composelevitation.sample 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.lightColorScheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.text.font.Font 8 | import androidx.compose.ui.text.font.FontFamily 9 | import androidx.compose.ui.text.font.FontStyle 10 | import androidx.compose.ui.text.font.FontWeight 11 | 12 | val FontFamilySpaceGrotesk = FontFamily( 13 | Font( 14 | resId = R.font.space_grotesk_bold, 15 | weight = FontWeight.Bold, 16 | style = FontStyle.Normal 17 | ) 18 | ) 19 | val FontFamilyOpenSans = FontFamily( 20 | Font( 21 | resId = R.font.opensans_regular, 22 | weight = FontWeight.Normal, 23 | style = FontStyle.Normal 24 | ) 25 | ) 26 | val FontFamilyDaysOne = FontFamily( 27 | Font( 28 | resId = R.font.days_one_regular, 29 | weight = FontWeight.Normal, 30 | style = FontStyle.Normal 31 | ) 32 | ) 33 | 34 | @Composable 35 | fun MainTheme(content: @Composable () -> Unit) { 36 | MaterialTheme( 37 | colorScheme = lightColorScheme( 38 | primary = Color.Black, 39 | primaryContainer = Color.White, 40 | onPrimary = Color.White, 41 | secondary = Color.Black, 42 | secondaryContainer = Color.White, 43 | onSecondary = Color.White, 44 | tertiary = Color.Black, 45 | tertiaryContainer = Color.White, 46 | onTertiary = Color.White, 47 | surface = Color.White, 48 | onSurface = Color.Black, 49 | surfaceVariant = Color.LightGray, 50 | onSurfaceVariant = Color.DarkGray, 51 | outline = Color.DarkGray, 52 | background = Color.White, 53 | onBackground = Color.Black 54 | ), 55 | content = content 56 | ) 57 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/drawable/android.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/drawable/noise.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/scene.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/drawable/scene.jpg -------------------------------------------------------------------------------- /app/src/main/res/font/days_one_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/font/days_one_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/font/opensans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/space_grotesk_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/font/space_grotesk_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ComposeLevitation 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("LongLine") 2 | 3 | // As a suggestions from here: https://youtrack.jetbrains.com/issue/KTIJ-19369/False-positive-cant-be-called-in-this-context-by-implicit-receiver-with-plugins-in-Gradle-version-catalogs-as-a-TOML-file#focus=Comments-27-5860112.0-0 4 | @Suppress( 5 | "DSL_SCOPE_VIOLATION", 6 | "MISSING_DEPENDENCY_CLASS", 7 | "UNRESOLVED_REFERENCE_WRONG_RECEIVER", 8 | "FUNCTION_CALL_EXPECTED" 9 | ) 10 | 11 | plugins { 12 | alias(libs.plugins.ksp.gradle.plugin) 13 | } 14 | 15 | buildscript { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | gradlePluginPortal() 20 | } 21 | dependencies { 22 | classpath(libs.android.gradle.plugin) 23 | classpath(libs.kotlin.gradle.plugin) 24 | } 25 | } 26 | 27 | allprojects { 28 | repositories { 29 | google() 30 | mavenCentral() 31 | maven("https://jitpack.io") 32 | } 33 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | org.gradle.parallel=true 11 | org.gradle.unsafe.configuration-cache=true 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | # AndroidX package structure to make it clearer which packages are bundled with the 17 | # Android operating system, and which are packaged with your app's APK 18 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 19 | android.useAndroidX=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | # Enables namespacing of each library's R class so that its R class includes only the 23 | # resources declared in the library itself and none from the library's dependencies, 24 | # thereby reducing the size of the R class for that library 25 | android.nonTransitiveRClass=true 26 | android.defaults.buildfeatures.buildconfig=true 27 | android.nonFinalResIds=false 28 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | # Android/Kotlin 4 | android_gradle_plugin_version = "8.1.1" 5 | androidx_ktx_version = "1.12.0" 6 | kotlin_version = "1.9.10" 7 | 8 | # Plugins 9 | ksp_version = "1.9.10-1.0.13" 10 | 11 | # Compose 12 | compose_compiler_version = "1.5.3" 13 | compose_bom_version = "2023.09.01" 14 | compose_activity_version = "1.7.2" 15 | compose_color_picker_version = "0.7.0" 16 | compose_shadows_plus_version = "1.0.3" 17 | 18 | [plugins] 19 | 20 | ksp_gradle_plugin = { id = "com.google.devtools.ksp", version.ref = "ksp_version" } 21 | 22 | [libraries] 23 | 24 | # Android/Kotlin 25 | android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin_version" } 26 | androidx_core = { module = "androidx.core:core", version.ref = "androidx_ktx_version" } 27 | androidx_ktx = { module = "androidx.core:core-ktx", version.ref = "androidx_ktx_version" } 28 | kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" } 29 | 30 | # Compose 31 | compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom_version" } 32 | compose_material3 = { module = "androidx.compose.material3:material3" } 33 | compose_preview = { module = "androidx.compose.ui:ui-tooling-preview" } 34 | compose_runtime = { module = "androidx.compose.runtime:runtime" } 35 | compose_ui_util = { module = "androidx.compose.ui:ui-util" } 36 | compose_activity = { module = "androidx.activity:activity-compose", version.ref = "compose_activity_version" } 37 | compose_color_picker = { module = "com.godaddy.android.colorpicker:compose-color-picker", version.ref = "compose_color_picker_version" } 38 | compose_shadows_plus = { module = "com.github.GIGAMOLE:ComposeShadowsPlus", version.ref = "compose_shadows_plus_version" } 39 | debug_compose_tooling = { module = "androidx.compose.ui:ui-tooling" } 40 | 41 | [bundles] 42 | 43 | compose = [ 44 | "compose.material3", 45 | "compose.preview", 46 | "compose.runtime", 47 | "compose.activity" 48 | ] 49 | debug_compose = [ 50 | "debug.compose.tooling" 51 | ] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 16 10:06:11 CET 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 -------------------------------------------------------------------------------- /media/credits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/credits.png -------------------------------------------------------------------------------- /media/demo-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/demo-img.png -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/demo.gif -------------------------------------------------------------------------------- /media/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/footer.png -------------------------------------------------------------------------------- /media/header-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/header-new.png -------------------------------------------------------------------------------- /media/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/header.png -------------------------------------------------------------------------------- /media/image-levitation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/image-levitation.gif -------------------------------------------------------------------------------- /media/image-press.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/image-press.gif -------------------------------------------------------------------------------- /media/image-shadow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/image-shadow.gif -------------------------------------------------------------------------------- /media/shape-levitation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/shape-levitation.gif -------------------------------------------------------------------------------- /media/shape-press.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/shape-press.gif -------------------------------------------------------------------------------- /media/shape-shadow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/shape-shadow.gif -------------------------------------------------------------------------------- /media/steam-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/steam-1.gif -------------------------------------------------------------------------------- /media/steam-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/steam-2.gif -------------------------------------------------------------------------------- /media/steam-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeLevitation/013f6e6f8874458ad333e9e15ef57af64926b7a4/media/steam-3.gif -------------------------------------------------------------------------------- /plugins/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | -------------------------------------------------------------------------------- /plugins/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | `kotlin-dsl-precompiled-script-plugins` 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | google() 9 | } 10 | 11 | dependencies { 12 | implementation(libs.android.gradle.plugin) 13 | implementation(libs.kotlin.gradle.plugin) 14 | } 15 | -------------------------------------------------------------------------------- /plugins/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | dependencyResolutionManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | versionCatalogs { 9 | create("libs") { 10 | from(files("../gradle/libs.versions.toml")) 11 | } 12 | } 13 | } 14 | 15 | rootProject.name = "plugins" 16 | -------------------------------------------------------------------------------- /plugins/src/main/java/CommonExtension.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage", "PackageDirectoryMismatch", "unused") 2 | 3 | import com.android.build.gradle.BaseExtension 4 | import com.android.build.gradle.LibraryExtension 5 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension 6 | 7 | private fun BaseExtension.baseSetup() { 8 | defaultConfig { 9 | minSdk = ProjectConfig.minSdk 10 | targetSdk = ProjectConfig.targetSdk 11 | 12 | versionCode = ProjectConfig.versionCode 13 | versionName = ProjectConfig.versionName 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility = ProjectConfig.javaCompileVersion 18 | targetCompatibility = ProjectConfig.javaCompileVersion 19 | } 20 | } 21 | 22 | fun LibraryExtension.setup() { 23 | baseSetup() 24 | 25 | defaultConfig { 26 | compileSdk = ProjectConfig.compileSdk 27 | } 28 | 29 | buildFeatures { 30 | compose = true 31 | } 32 | } 33 | 34 | fun BaseAppModuleExtension.setup() { 35 | baseSetup() 36 | 37 | defaultConfig { 38 | compileSdk = ProjectConfig.compileSdk 39 | } 40 | 41 | buildFeatures { 42 | compose = true 43 | } 44 | } -------------------------------------------------------------------------------- /plugins/src/main/java/ProjectConfig.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | import org.gradle.api.JavaVersion 4 | import org.gradle.jvm.toolchain.JavaLanguageVersion 5 | 6 | object ProjectConfig { 7 | const val versionCode = 1 8 | const val versionName = "1.0.3" 9 | 10 | const val namespace = "com.gigamole.composelevitation" 11 | const val group = "com.github.GIGAMOLE" 12 | const val artifact = "ComposeLevitation" 13 | const val publication = "release" 14 | 15 | const val compileSdk = 34 16 | const val targetSdk = 34 17 | const val minSdk = 21 18 | 19 | val javaCompileVersion = JavaVersion.VERSION_17 20 | private val javaCompileVersionText = javaCompileVersion.toString() 21 | val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(javaCompileVersionText) 22 | val kotlinJvmTarget = javaCompileVersionText 23 | } 24 | -------------------------------------------------------------------------------- /plugins/src/main/java/composelevitation.application.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | setup() 8 | 9 | kotlinOptions { 10 | jvmTarget = ProjectConfig.kotlinJvmTarget 11 | } 12 | } -------------------------------------------------------------------------------- /plugins/src/main/java/composelevitation.library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | setup() 8 | 9 | kotlinOptions { 10 | jvmTarget = ProjectConfig.kotlinJvmTarget 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | include(":app") 4 | include(":ComposeLevitation") 5 | 6 | pluginManagement { 7 | repositories { 8 | gradlePluginPortal() 9 | mavenCentral() 10 | google() 11 | includeBuild("plugins") 12 | } 13 | } 14 | 15 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 16 | 17 | rootProject.name = "ComposeLevitationProject" 18 | --------------------------------------------------------------------------------