├── .editorconfig ├── .gitignore ├── ComposeScrollbars ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── gigamole │ └── composescrollbars │ ├── Scrollbars.kt │ ├── ScrollbarsState.kt │ ├── common │ └── ScrollbarsUtil.kt │ ├── config │ ├── ScrollbarsConfig.kt │ ├── ScrollbarsConfigDefaults.kt │ ├── ScrollbarsGravity.kt │ ├── ScrollbarsOrientation.kt │ ├── layercontenttype │ │ ├── ScrollbarsLayerContentType.kt │ │ ├── ScrollbarsLayerContentTypeDefaults.kt │ │ └── layercontentstyletype │ │ │ ├── ScrollbarsLayerContentStyleType.kt │ │ │ └── ScrollbarsLayerContentStyleTypeDefaults.kt │ ├── layersType │ │ ├── ScrollbarsLayersType.kt │ │ ├── ScrollbarsLayersTypeDefaults.kt │ │ ├── layerConfig │ │ │ ├── ScrollbarsLayerConfig.kt │ │ │ ├── ScrollbarsLayerConfigDefaults.kt │ │ │ └── ScrollbarsLayerGravity.kt │ │ └── thicknessType │ │ │ ├── ScrollbarsThicknessType.kt │ │ │ └── ScrollbarsThicknessTypeDefaults.kt │ ├── sizetype │ │ ├── ScrollbarsSizeType.kt │ │ └── ScrollbarsSizeTypeDefaults.kt │ └── visibilitytype │ │ ├── ScrollbarsVisibilityType.kt │ │ └── ScrollbarsVisibilityTypeDefaults.kt │ └── scrolltype │ ├── ScrollbarsScrollType.kt │ ├── ScrollbarsScrollTypeDefaults.kt │ └── knobtype │ ├── ScrollbarsDynamicKnobType.kt │ ├── ScrollbarsKnobType.kt │ ├── ScrollbarsKnobTypeDefaults.kt │ └── ScrollbarsStaticKnobType.kt ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ └── com │ │ └── gigamole │ │ └── composescrollbars │ │ └── sample │ │ ├── MainActivity.kt │ │ ├── MainApplication.kt │ │ ├── MainScreenContent.kt │ │ ├── MainScreenDemoContent.kt │ │ └── MainTheme.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_star.xml │ ├── font │ ├── 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.gif ├── footer.png ├── header.png ├── sample-1.gif ├── sample-2.gif ├── sample-3.gif ├── sample-4.gif ├── sample-5.gif └── sample-6.gif ├── plugins ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── java │ ├── CommonExtension.kt │ ├── ProjectConfig.kt │ ├── composescrollbars.application.gradle.kts │ └── composescrollbars.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 | [*.md] 393 | max_line_length = 1024 394 | 395 | [{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] 396 | ij_toml_keep_indents_on_empty_lines = false 397 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /ComposeScrollbars/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ComposeScrollbars/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("composescrollbars.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 | debugImplementation(libs.bundles.debug.compose) 54 | } -------------------------------------------------------------------------------- /ComposeScrollbars/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 -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/Scrollbars.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | @file:OptIn(ExperimentalComposeUiApi::class) 3 | 4 | package com.gigamole.composescrollbars 5 | 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.gestures.awaitEachGesture 9 | import androidx.compose.foundation.gestures.waitForUpOrCancellation 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.BoxScope 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.ColumnScope 14 | import androidx.compose.foundation.layout.IntrinsicSize 15 | import androidx.compose.foundation.layout.Row 16 | import androidx.compose.foundation.layout.RowScope 17 | import androidx.compose.foundation.layout.Spacer 18 | import androidx.compose.foundation.layout.fillMaxHeight 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.foundation.layout.fillMaxWidth 21 | import androidx.compose.foundation.layout.height 22 | import androidx.compose.foundation.layout.offset 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.layout.width 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.ExperimentalComposeUiApi 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.draw.alpha 30 | import androidx.compose.ui.draw.drawWithContent 31 | import androidx.compose.ui.draw.scale 32 | import androidx.compose.ui.input.pointer.pointerInput 33 | import androidx.compose.ui.input.pointer.pointerInteropFilter 34 | import androidx.compose.ui.layout.onSizeChanged 35 | import androidx.compose.ui.unit.Dp 36 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 37 | import com.gigamole.composescrollbars.config.ScrollbarsGravity 38 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 39 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 40 | import com.gigamole.composescrollbars.config.layercontenttype.layercontentstyletype.ScrollbarsLayerContentStyleType 41 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 42 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerGravity 43 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 44 | import com.gigamole.composescrollbars.config.sizetype.ScrollbarsSizeType 45 | import com.gigamole.composescrollbars.config.visibilitytype.ScrollbarsVisibilityType 46 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 47 | 48 | /** 49 | * The scrollbars main [Composable]. 50 | * 51 | * The Scrollbars container organised in the following way: 52 | * - The global container with a max size, a [handleTouchDown] if supported, and a correct [alignment] of the layers container. 53 | * - The layers container which handles [visibility], [size], [intrinsicSide] and paddings. 54 | * - The layers container contains two layers content holders: background layer and knob layer, and their behavior is different by [ScrollbarsLayersType]. 55 | * 56 | * In a [ScrollbarsLayersType.Wrap] the background layer is wrapped around the knob layer and both of them are centered. 57 | * 58 | * In a [ScrollbarsLayersType.Split] the background layer and knob layer are configured individually. 59 | * 60 | * @param state The required [ScrollbarsState]. 61 | * @param modifier The custom [Modifier]. 62 | * @see ScrollbarsLayerContent 63 | * @see ScrollbarsConfig 64 | * @see ScrollbarsScrollType 65 | * @author GIGAMOLE 66 | */ 67 | @Composable 68 | fun Scrollbars( 69 | state: ScrollbarsState, 70 | modifier: Modifier = Modifier 71 | ) { 72 | state.HandleState() 73 | 74 | val orientation = state.config.orientation 75 | val gravity = state.config.gravity 76 | val layersType = state.config.layersType 77 | val startWeight = state.startKnobFraction 78 | val endWeight = 1.0F - state.endKnobFraction 79 | val knobWeight = state.endKnobFraction - state.startKnobFraction 80 | 81 | Box( 82 | modifier = modifier 83 | .fillMaxSize() 84 | .handleTouchDown(state = state), 85 | contentAlignment = alignment( 86 | orientation = orientation, 87 | gravity = gravity 88 | ) 89 | ) { 90 | Box( 91 | modifier = Modifier 92 | .visibility(state = state) 93 | .size( 94 | orientation = orientation, 95 | sizeType = state.config.sizeType 96 | ) 97 | .intrinsicSide(orientation = orientation) 98 | .padding(paddingValues = state.config.paddingValues) 99 | ) { 100 | @Composable 101 | fun BackgroundLayerContainer( 102 | modifier: Modifier = Modifier 103 | ) { 104 | Box( 105 | modifier = modifier.fillMaxSide(orientation = orientation) 106 | ) { 107 | ScrollbarsLayerContent( 108 | state = state, 109 | layerContentType = state.config.backgroundLayerContentType 110 | ) 111 | } 112 | } 113 | 114 | @Composable 115 | fun KnobLayerContainer( 116 | modifier: Modifier = Modifier, 117 | knobLayerContent: @Composable BoxScope.() -> Unit = { 118 | ScrollbarsLayerContent( 119 | state = state, 120 | layerContentType = state.config.knobLayerContentType 121 | ) 122 | } 123 | ) { 124 | when (orientation) { 125 | ScrollbarsOrientation.Vertical -> { 126 | Column( 127 | modifier = modifier.fillMaxHeight() 128 | ) { 129 | Spacer( 130 | modifier = Modifier.nullableWeight( 131 | columnScope = this, 132 | weight = startWeight 133 | ) 134 | ) 135 | Box( 136 | modifier = Modifier.nullableWeight( 137 | columnScope = this, 138 | weight = knobWeight 139 | ), 140 | content = knobLayerContent 141 | ) 142 | Spacer( 143 | modifier = Modifier.nullableWeight( 144 | columnScope = this, 145 | weight = endWeight 146 | ) 147 | ) 148 | } 149 | } 150 | ScrollbarsOrientation.Horizontal -> { 151 | Row( 152 | modifier = modifier.fillMaxWidth() 153 | ) { 154 | Spacer( 155 | modifier = Modifier.nullableWeight( 156 | rowScope = this, 157 | weight = startWeight 158 | ) 159 | ) 160 | Box( 161 | modifier = Modifier.nullableWeight( 162 | rowScope = this, 163 | weight = knobWeight 164 | ), 165 | content = knobLayerContent 166 | ) 167 | Spacer( 168 | modifier = Modifier.nullableWeight( 169 | rowScope = this, 170 | weight = endWeight 171 | ) 172 | ) 173 | } 174 | } 175 | } 176 | } 177 | 178 | BackgroundLayerContainer( 179 | modifier = when (layersType) { 180 | is ScrollbarsLayersType.Split -> { 181 | Modifier 182 | .align( 183 | alignment = layerAlignment( 184 | orientation = orientation, 185 | layerGravity = layersType.backgroundLayerConfig.layerGravity 186 | ) 187 | ) 188 | .padding(paddingValues = layersType.backgroundLayerConfig.paddingValues) 189 | .thickness( 190 | orientation = orientation, 191 | thicknessType = layersType.backgroundLayerConfig.thicknessType 192 | ) 193 | } 194 | is ScrollbarsLayersType.Wrap -> { 195 | Modifier 196 | .align(alignment = Alignment.Center) 197 | .fillMaxOppositeSide(orientation = orientation) 198 | } 199 | } 200 | ) 201 | KnobLayerContainer( 202 | modifier = when (layersType) { 203 | is ScrollbarsLayersType.Split -> { 204 | Modifier 205 | .align( 206 | alignment = layerAlignment( 207 | orientation = orientation, 208 | layerGravity = layersType.knobLayerConfig.layerGravity 209 | ) 210 | ) 211 | .padding(paddingValues = layersType.knobLayerConfig.paddingValues) 212 | .thickness( 213 | orientation = orientation, 214 | thicknessType = layersType.knobLayerConfig.thicknessType 215 | ) 216 | } 217 | is ScrollbarsLayersType.Wrap -> { 218 | Modifier 219 | .align(alignment = Alignment.Center) 220 | .padding(paddingValues = layersType.paddingValues) 221 | .thickness( 222 | orientation = orientation, 223 | thicknessType = layersType.thicknessType 224 | ) 225 | } 226 | } 227 | .drawWithContent { 228 | state.handleTargetKnobFraction(this) 229 | 230 | drawContent() 231 | } 232 | ) 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * The scrollbars layer content [Composable] provider within a [BoxScope], which handles [ScrollbarsLayerContentType]. 239 | * 240 | * @param state The current [ScrollbarsState]. 241 | * @param layerContentType The [ScrollbarsLayerContentType]. 242 | * @author GIGAMOLE 243 | */ 244 | @Composable 245 | private fun BoxScope.ScrollbarsLayerContent( 246 | state: ScrollbarsState, 247 | layerContentType: ScrollbarsLayerContentType 248 | ) { 249 | when (layerContentType) { 250 | is ScrollbarsLayerContentType.Default -> { 251 | Box( 252 | modifier = Modifier 253 | .fillMaxSize() 254 | .defaultLayerContentType(defaultLayerContentType = layerContentType) 255 | ) 256 | } 257 | is ScrollbarsLayerContentType.Custom -> { 258 | layerContentType.content(this, state) 259 | } 260 | ScrollbarsLayerContentType.None -> { 261 | /* Do nothing. */ 262 | } 263 | } 264 | } 265 | 266 | /** 267 | * The [Modifier] which handles the [ScrollbarsVisibilityType.Dynamic.isVisibleOnTouchDown] indicator. 268 | * 269 | * @param state The current [ScrollbarsState]. 270 | * @return The [Modifier] chain with a [handleTouchDown] in it. 271 | * @author GIGAMOLE 272 | */ 273 | private fun Modifier.handleTouchDown( 274 | state: ScrollbarsState 275 | ): Modifier = then( 276 | with(state) { 277 | if (config.visibilityType is ScrollbarsVisibilityType.Dynamic && 278 | config.visibilityType.isVisibleOnTouchDown 279 | ) { 280 | Modifier 281 | .pointerInteropFilter( 282 | onTouchEvent = { 283 | state.handleTouchDown(motionEvent = it) 284 | 285 | false 286 | } 287 | ) 288 | .pointerInput(this) { 289 | awaitEachGesture { 290 | state.handleTouchDownRelease(awaitPointerEventScope = this) 291 | 292 | waitForUpOrCancellation() 293 | } 294 | } 295 | } else { 296 | Modifier 297 | } 298 | } 299 | ) 300 | 301 | /** 302 | * The [Modifier] which handles a [ScrollbarsSizeType] by [ScrollbarsOrientation]. 303 | * 304 | * @param orientation The [ScrollbarsOrientation]. 305 | * @param sizeType The [ScrollbarsSizeType]. 306 | * @return The [Modifier] chain with a [size] in it. 307 | * @author GIGAMOLE 308 | */ 309 | private fun Modifier.size( 310 | orientation: ScrollbarsOrientation, 311 | sizeType: ScrollbarsSizeType 312 | ): Modifier = then( 313 | when (sizeType) { 314 | is ScrollbarsSizeType.Exact -> { 315 | Modifier.exactSize( 316 | orientation = orientation, 317 | size = sizeType.size 318 | ) 319 | } 320 | is ScrollbarsSizeType.Fraction -> { 321 | Modifier.fillMaxSide( 322 | orientation = orientation, 323 | fraction = sizeType.fraction 324 | ) 325 | } 326 | ScrollbarsSizeType.Full -> { 327 | Modifier.fillMaxSide( 328 | orientation = orientation, 329 | fraction = 1.0F 330 | ) 331 | } 332 | } 333 | ) 334 | 335 | /** 336 | * The [Modifier] which handles an exact size by [ScrollbarsOrientation]. 337 | * 338 | * @param orientation The [ScrollbarsOrientation]. 339 | * @param size The exact size. 340 | * @return The [Modifier] chain with an [exactSize] in it. 341 | * @author GIGAMOLE 342 | */ 343 | private fun Modifier.exactSize( 344 | orientation: ScrollbarsOrientation, 345 | size: Dp 346 | ): Modifier = then( 347 | when (orientation) { 348 | ScrollbarsOrientation.Vertical -> { 349 | Modifier.height(height = size) 350 | } 351 | ScrollbarsOrientation.Horizontal -> { 352 | Modifier.width(width = size) 353 | } 354 | } 355 | ) 356 | 357 | /** 358 | * The [Modifier] which handles a filling of max size by [ScrollbarsOrientation] and optional weight [fraction]. 359 | * 360 | * @param orientation The [ScrollbarsOrientation]. 361 | * @param fraction The optional weight fraction. 362 | * @return The [Modifier] chain with a [fillMaxSide] in it. 363 | * @author GIGAMOLE 364 | */ 365 | private fun Modifier.fillMaxSide( 366 | orientation: ScrollbarsOrientation, 367 | fraction: Float = 1.0F 368 | ): Modifier = then( 369 | when (orientation) { 370 | ScrollbarsOrientation.Vertical -> { 371 | Modifier.fillMaxHeight(fraction = fraction) 372 | } 373 | ScrollbarsOrientation.Horizontal -> { 374 | Modifier.fillMaxWidth(fraction = fraction) 375 | } 376 | } 377 | ) 378 | 379 | /** 380 | * The [Modifier] which handles a filling of max opposite size (basically, wrap thickness) by [ScrollbarsOrientation]. 381 | * 382 | * @param orientation The [ScrollbarsOrientation]. 383 | * @return The [Modifier] chain with a [fillMaxOppositeSide] in it. 384 | * @author GIGAMOLE 385 | */ 386 | private fun Modifier.fillMaxOppositeSide( 387 | orientation: ScrollbarsOrientation 388 | ): Modifier = then( 389 | when (orientation) { 390 | ScrollbarsOrientation.Vertical -> { 391 | Modifier.fillMaxWidth() 392 | } 393 | ScrollbarsOrientation.Horizontal -> { 394 | Modifier.fillMaxHeight() 395 | } 396 | } 397 | ) 398 | 399 | /** 400 | * The [Modifier] which handles an intrinsic size of a side by [ScrollbarsOrientation]. 401 | * 402 | * @param orientation The [ScrollbarsOrientation]. 403 | * @return The [Modifier] chain with an [intrinsicSide] in it. 404 | * @author GIGAMOLE 405 | */ 406 | private fun Modifier.intrinsicSide( 407 | orientation: ScrollbarsOrientation 408 | ): Modifier = then( 409 | when (orientation) { 410 | ScrollbarsOrientation.Vertical -> { 411 | Modifier.width(intrinsicSize = IntrinsicSize.Max) 412 | } 413 | ScrollbarsOrientation.Horizontal -> { 414 | Modifier.height(intrinsicSize = IntrinsicSize.Max) 415 | } 416 | } 417 | ) 418 | 419 | /** 420 | * The [Modifier] which handles a [ScrollbarsThicknessType] by [ScrollbarsOrientation]. 421 | * 422 | * @param orientation The [ScrollbarsOrientation]. 423 | * @param thicknessType The [ScrollbarsThicknessType]. 424 | * @return The [Modifier] chain with a [thickness] in it. 425 | * @author GIGAMOLE 426 | */ 427 | private fun Modifier.thickness( 428 | orientation: ScrollbarsOrientation, 429 | thicknessType: ScrollbarsThicknessType 430 | ): Modifier = then( 431 | when (orientation) { 432 | ScrollbarsOrientation.Vertical -> { 433 | when (thicknessType) { 434 | is ScrollbarsThicknessType.Exact -> { 435 | Modifier.width(width = thicknessType.thickness) 436 | } 437 | ScrollbarsThicknessType.Wrap -> { 438 | Modifier 439 | } 440 | } 441 | } 442 | ScrollbarsOrientation.Horizontal -> { 443 | when (thicknessType) { 444 | is ScrollbarsThicknessType.Exact -> { 445 | Modifier.height(height = thicknessType.thickness) 446 | } 447 | ScrollbarsThicknessType.Wrap -> { 448 | Modifier 449 | } 450 | } 451 | } 452 | } 453 | ) 454 | 455 | /** 456 | * The [Modifier] which handles a zero value weight within a [RowScope]. 457 | * 458 | * @param rowScope The [RowScope]. 459 | * @param weight The weight value. 460 | * @return The [Modifier] chain with a [nullableWeight] in it. 461 | * @author GIGAMOLE 462 | */ 463 | private fun Modifier.nullableWeight( 464 | rowScope: RowScope, 465 | weight: Float 466 | ): Modifier = then( 467 | if (weight > 0.0F) { 468 | with(rowScope) { 469 | Modifier.weight(weight = weight) 470 | } 471 | } else { 472 | Modifier 473 | } 474 | ) 475 | 476 | /** 477 | * The [Modifier] which handles a zero value weight within a [ColumnScope]. 478 | * 479 | * @param columnScope The [ColumnScope]. 480 | * @param weight The weight value. 481 | * @return The [Modifier] chain with a [nullableWeight] in it. 482 | * @author GIGAMOLE 483 | */ 484 | private fun Modifier.nullableWeight( 485 | columnScope: ColumnScope, 486 | weight: Float 487 | ): Modifier = then( 488 | if (weight > 0.0F) { 489 | with(columnScope) { 490 | Modifier.weight(weight = weight) 491 | } 492 | } else { 493 | Modifier 494 | } 495 | ) 496 | 497 | /** 498 | * The [Modifier] which handles a [ScrollbarsVisibilityType]. 499 | * 500 | * @param state The current [ScrollbarsState]. 501 | * @return The [Modifier] chain with a [visibility] in it. 502 | * @author GIGAMOLE 503 | */ 504 | private fun Modifier.visibility( 505 | state: ScrollbarsState 506 | ): Modifier { 507 | val visibilityType = state.config.visibilityType 508 | 509 | return then( 510 | when (visibilityType) { 511 | is ScrollbarsVisibilityType.Dynamic.Fade -> { 512 | Modifier.alpha(alpha = visibilityType.fadeFraction) 513 | } 514 | is ScrollbarsVisibilityType.Dynamic.Scale -> { 515 | Modifier 516 | .scale(scale = visibilityType.scaleValue) 517 | .alpha(alpha = visibilityType.fadeFraction) 518 | } 519 | is ScrollbarsVisibilityType.Dynamic.Slide -> { 520 | Modifier 521 | .onSizeChanged { 522 | // Capture and handle the new size of the layers container. 523 | visibilityType.handleTargetSlideOffset( 524 | state = state, 525 | size = it 526 | ) 527 | } 528 | .offset { 529 | visibilityType.slideOffset 530 | } 531 | .alpha(alpha = visibilityType.fadeFraction) 532 | } 533 | is ScrollbarsVisibilityType.Static -> { 534 | Modifier 535 | } 536 | } 537 | ) 538 | } 539 | 540 | /** 541 | * The [Modifier] which handles the appearance of [ScrollbarsLayerContentType.Default]. 542 | * 543 | * @param defaultLayerContentType The [ScrollbarsLayerContentType.Default]. 544 | * @return The [Modifier] chain with a [defaultLayerContentType] in it. 545 | * @author GIGAMOLE 546 | */ 547 | private fun Modifier.defaultLayerContentType( 548 | defaultLayerContentType: ScrollbarsLayerContentType.Default 549 | ): Modifier = then( 550 | when (val drawStyleType = defaultLayerContentType.styleType) { 551 | ScrollbarsLayerContentStyleType.Background -> { 552 | Modifier.background( 553 | shape = defaultLayerContentType.shape, 554 | brush = defaultLayerContentType.brush 555 | ) 556 | } 557 | is ScrollbarsLayerContentStyleType.Border -> { 558 | Modifier.border( 559 | width = drawStyleType.width, 560 | shape = defaultLayerContentType.shape, 561 | brush = defaultLayerContentType.brush 562 | ) 563 | } 564 | } 565 | ) 566 | 567 | /** 568 | * The [Modifier] which handles a global container content alignment by [ScrollbarsOrientation] and [ScrollbarsGravity]. 569 | * 570 | * @param orientation The [ScrollbarsOrientation]. 571 | * @param gravity The [ScrollbarsGravity]. 572 | * @return The [Modifier] chain with an [alignment] in it. 573 | * @author GIGAMOLE 574 | */ 575 | @Composable 576 | private fun alignment( 577 | orientation: ScrollbarsOrientation, 578 | gravity: ScrollbarsGravity 579 | ): Alignment = when (orientation) { 580 | ScrollbarsOrientation.Vertical -> { 581 | when (gravity) { 582 | ScrollbarsGravity.Start -> { 583 | Alignment.CenterStart 584 | } 585 | ScrollbarsGravity.End -> { 586 | Alignment.CenterEnd 587 | } 588 | } 589 | } 590 | ScrollbarsOrientation.Horizontal -> { 591 | when (gravity) { 592 | ScrollbarsGravity.Start -> { 593 | Alignment.TopCenter 594 | } 595 | ScrollbarsGravity.End -> { 596 | Alignment.BottomCenter 597 | } 598 | } 599 | } 600 | } 601 | 602 | /** 603 | * The [Modifier] which handles a scrollbars layer alignment by [ScrollbarsOrientation] and [ScrollbarsLayerGravity]. 604 | * 605 | * @param orientation The [ScrollbarsOrientation]. 606 | * @param layerGravity The [ScrollbarsLayerGravity]. 607 | * @return The [Modifier] chain with an [layerAlignment] in it. 608 | * @author GIGAMOLE 609 | */ 610 | @Composable 611 | private fun layerAlignment( 612 | orientation: ScrollbarsOrientation, 613 | layerGravity: ScrollbarsLayerGravity 614 | ): Alignment = when (orientation) { 615 | ScrollbarsOrientation.Vertical -> { 616 | when (layerGravity) { 617 | ScrollbarsLayerGravity.Start -> { 618 | Alignment.CenterStart 619 | } 620 | ScrollbarsLayerGravity.Center -> { 621 | Alignment.Center 622 | } 623 | ScrollbarsLayerGravity.End -> { 624 | Alignment.CenterEnd 625 | } 626 | } 627 | } 628 | ScrollbarsOrientation.Horizontal -> { 629 | when (layerGravity) { 630 | ScrollbarsLayerGravity.Start -> { 631 | Alignment.TopCenter 632 | } 633 | ScrollbarsLayerGravity.Center -> { 634 | Alignment.Center 635 | } 636 | ScrollbarsLayerGravity.End -> { 637 | Alignment.BottomCenter 638 | } 639 | } 640 | } 641 | } 642 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/common/ScrollbarsUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.common 2 | 3 | import androidx.compose.animation.core.Easing 4 | import com.gigamole.composescrollbars.ScrollbarsState 5 | 6 | /** 7 | * Performs a linear interpolation (lerp) between two values ([start] and [stop]) using a sub-fraction of a given [Float]. 8 | * 9 | * Optionally, an [Easing] function can be provided to modify the interpolation curve. 10 | * 11 | * @param start The starting value of the interpolation. 12 | * @param stop The ending value of the interpolation. 13 | * @param easing An optional [Easing] function to modify the interpolation curve. 14 | * @return The interpolated value based on the sub-fraction of the current [Float]. 15 | * @see ScrollbarsState 16 | * @author GIGAMOLE 17 | */ 18 | fun Float.subLerp(start: Float, stop: Float, easing: Easing? = null): Float { 19 | return when { 20 | start == stop -> start 21 | this < start -> 0.0F 22 | this > stop -> 1.0F 23 | else -> { 24 | val subFraction = (this - start) / (stop - start) 25 | 26 | easing?.transform(subFraction) ?: subFraction 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/ScrollbarsConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import com.gigamole.composescrollbars.ScrollbarsState 5 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 6 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 7 | import com.gigamole.composescrollbars.config.sizetype.ScrollbarsSizeType 8 | import com.gigamole.composescrollbars.config.visibilitytype.ScrollbarsVisibilityType 9 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 10 | 11 | /** 12 | * The scrollbars appearance configuration for [ScrollbarsState]. 13 | * 14 | * The [orientation] should be the same as the orientation of [ScrollbarsScrollType.knobType] state. 15 | * 16 | * @property orientation The [ScrollbarsOrientation]. 17 | * @property gravity The [ScrollbarsGravity]. 18 | * @property isReverseLayout The reverse layout indicator. 19 | * @property paddingValues The scrollbars layers container [PaddingValues]. 20 | * @property sizeType The [ScrollbarsSizeType]. 21 | * @property layersType The [ScrollbarsLayersType]. 22 | * @property backgroundLayerContentType The background [ScrollbarsLayerContentType]. 23 | * @property knobLayerContentType The knob [ScrollbarsLayerContentType]. 24 | * @property visibilityType The [ScrollbarsVisibilityType]. 25 | * @see ScrollbarsScrollType 26 | * @author GIGAMOLE 27 | */ 28 | data class ScrollbarsConfig( 29 | val orientation: ScrollbarsOrientation, 30 | val gravity: ScrollbarsGravity = ScrollbarsConfigDefaults.Gravity, 31 | val isReverseLayout: Boolean = ScrollbarsConfigDefaults.IsReverseLayout, 32 | val paddingValues: PaddingValues = ScrollbarsConfigDefaults.PaddingValues, 33 | val sizeType: ScrollbarsSizeType = ScrollbarsConfigDefaults.SizeType, 34 | val layersType: ScrollbarsLayersType = ScrollbarsConfigDefaults.LayersType, 35 | val backgroundLayerContentType: ScrollbarsLayerContentType = ScrollbarsConfigDefaults.BackgroundLayerContentType, 36 | val knobLayerContentType: ScrollbarsLayerContentType = ScrollbarsConfigDefaults.KnobLayerContentType, 37 | val visibilityType: ScrollbarsVisibilityType = ScrollbarsConfigDefaults.VisibilityType 38 | ) -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/ScrollbarsConfigDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.ui.unit.dp 5 | import com.gigamole.composescrollbars.ScrollbarsState 6 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 7 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 8 | import com.gigamole.composescrollbars.config.sizetype.ScrollbarsSizeType 9 | import com.gigamole.composescrollbars.config.visibilitytype.ScrollbarsVisibilityType 10 | 11 | /** 12 | * The default values for [ScrollbarsConfig]. 13 | * 14 | * @see ScrollbarsState 15 | * @see ScrollbarsGravity 16 | * @see ScrollbarsOrientation 17 | * @author GIGAMOLE 18 | */ 19 | object ScrollbarsConfigDefaults { 20 | 21 | /** The default [ScrollbarsGravity]. */ 22 | val Gravity: ScrollbarsGravity = ScrollbarsGravity.End 23 | 24 | /** The default is reverse layout indicator. */ 25 | const val IsReverseLayout: Boolean = false 26 | 27 | /** The default scrollbars layers container [PaddingValues]. */ 28 | val PaddingValues: PaddingValues = PaddingValues(all = 4.dp) 29 | 30 | /** The default [ScrollbarsSizeType]. */ 31 | val SizeType: ScrollbarsSizeType = ScrollbarsSizeType.Full 32 | 33 | /** The default [ScrollbarsLayersType]. */ 34 | val LayersType: ScrollbarsLayersType = ScrollbarsLayersType.Wrap() 35 | 36 | /** The default background [ScrollbarsLayerContentType]. */ 37 | val BackgroundLayerContentType: ScrollbarsLayerContentType = ScrollbarsLayerContentType.None 38 | 39 | /** The default knob [ScrollbarsLayerContentType]. */ 40 | val KnobLayerContentType: ScrollbarsLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive() 41 | 42 | /** The default [ScrollbarsVisibilityType]. */ 43 | val VisibilityType: ScrollbarsVisibilityType = ScrollbarsVisibilityType.Dynamic.Fade() 44 | } 45 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/ScrollbarsGravity.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config 2 | 3 | import com.gigamole.composescrollbars.config.visibilitytype.ScrollbarsVisibilityType 4 | 5 | /** 6 | * The scrollbars layers container gravity for [ScrollbarsConfig]. 7 | * 8 | * @see ScrollbarsOrientation 9 | * @see ScrollbarsVisibilityType 10 | * @author GIGAMOLE 11 | */ 12 | enum class ScrollbarsGravity { 13 | 14 | /** The scrollbars layers container gravity at the start anchor (left or top, depends on [ScrollbarsOrientation]). */ 15 | Start, 16 | 17 | /** The scrollbars layers container gravity at the end anchor (right or bottom, depends on [ScrollbarsOrientation]). */ 18 | End 19 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/ScrollbarsOrientation.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config 2 | 3 | import com.gigamole.composescrollbars.config.visibilitytype.ScrollbarsVisibilityType 4 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 5 | 6 | /** 7 | * The scrollbars content orientation for [ScrollbarsConfig]. 8 | * 9 | * @see ScrollbarsScrollType 10 | * @see ScrollbarsVisibilityType 11 | * @author GIGAMOLE 12 | */ 13 | enum class ScrollbarsOrientation { 14 | 15 | /** The scrollbars orientation for a vertical scroll content. */ 16 | Vertical, 17 | 18 | /** The scrollbars orientation for a horizontal scroll content. */ 19 | Horizontal 20 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layercontenttype/ScrollbarsLayerContentType.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.gigamole.composescrollbars.config.layercontenttype 4 | 5 | import androidx.compose.animation.animateColorAsState 6 | import androidx.compose.animation.core.AnimationSpec 7 | import androidx.compose.foundation.layout.BoxScope 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.graphics.Brush 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.graphics.SolidColor 16 | import com.gigamole.composescrollbars.ScrollbarsState 17 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 18 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 19 | import com.gigamole.composescrollbars.config.layercontenttype.layercontentstyletype.ScrollbarsLayerContentStyleType 20 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 21 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 22 | 23 | /** 24 | * The scrollbars layer content configuration (background or knob) for [ScrollbarsConfig]. 25 | * 26 | * @see ScrollbarsLayersType 27 | * @author GIGAMOLE 28 | */ 29 | sealed interface ScrollbarsLayerContentType { 30 | 31 | /** The empty (not visible) layer content. */ 32 | data object None : ScrollbarsLayerContentType 33 | 34 | /** 35 | * The custom layer content. 36 | * 37 | * When the thickness type is [ScrollbarsThicknessType.Wrap], then the custom content thickness size (width or height, depends on [ScrollbarsOrientation]) should be 38 | * handled manually. 39 | * 40 | * @property content The block with the provided current [ScrollbarsState] that provides a custom [Composable] within a [BoxScope]. 41 | */ 42 | data class Custom( 43 | val content: @Composable BoxScope.(state: ScrollbarsState) -> Unit 44 | ) : ScrollbarsLayerContentType 45 | 46 | /** The default layer content. */ 47 | sealed interface Default : ScrollbarsLayerContentType { 48 | 49 | /** The layer content [Shape]. */ 50 | val shape: Shape 51 | 52 | /** The layer content [Brush]. */ 53 | val brush: Brush 54 | 55 | /** The [ScrollbarsLayerContentStyleType]. */ 56 | val styleType: ScrollbarsLayerContentStyleType 57 | 58 | /** 59 | * The original raw layer content type. 60 | * 61 | * @property shape The layer content [Shape]. 62 | * @property brush The layer content [Brush]. 63 | * @property styleType The [ScrollbarsLayerContentStyleType]. 64 | */ 65 | data class Original( 66 | override val shape: Shape = ScrollbarsLayerContentTypeDefaults.Default.Shape, 67 | override val brush: Brush = ScrollbarsLayerContentTypeDefaults.Default.Brush, 68 | override val styleType: ScrollbarsLayerContentStyleType = ScrollbarsLayerContentTypeDefaults.Default.StyleType 69 | ) : Default 70 | 71 | /** 72 | * The colored layer content type. 73 | * 74 | * @property shape The layer content [Shape]. 75 | * @property brush The layer content [Brush]. 76 | * @property styleType The [ScrollbarsLayerContentStyleType]. 77 | */ 78 | sealed interface Colored : Default { 79 | 80 | /** The idle layer content [Color]. */ 81 | val idleColor: Color 82 | 83 | /** 84 | * The idle colored layer content type. 85 | * 86 | * @property idleColor The idle layer content [Color]. 87 | * @property shape The layer content [Shape]. 88 | * @property styleType The [ScrollbarsLayerContentStyleType]. 89 | */ 90 | data class Idle( 91 | override val idleColor: Color = ScrollbarsLayerContentTypeDefaults.Default.Colored.IdleColor, 92 | override val shape: Shape = ScrollbarsLayerContentTypeDefaults.Default.Shape, 93 | override val styleType: ScrollbarsLayerContentStyleType = ScrollbarsLayerContentTypeDefaults.Default.StyleType 94 | ) : Colored { 95 | 96 | /** The idle [SolidColor] brush with [idleColor]. */ 97 | override val brush: Brush 98 | get() = SolidColor(value = idleColor) 99 | } 100 | 101 | /** 102 | * The idle colored layer content type. 103 | * 104 | * @property idleColor The idle layer content [Color]. 105 | * @property shape The layer content [Shape]. 106 | * @property styleType The [ScrollbarsLayerContentStyleType]. 107 | * @property activeColor The active layer content [Color]. 108 | * @property inAnimationSpec The [activeColor] layer content in [AnimationSpec]. Null disables the animation. 109 | * @property outAnimationSpec The [activeColor] layer content out [AnimationSpec]. Null disables the animation. 110 | */ 111 | data class IdleActive( 112 | override val idleColor: Color = ScrollbarsLayerContentTypeDefaults.Default.Colored.IdleColor, 113 | override val shape: Shape = ScrollbarsLayerContentTypeDefaults.Default.Shape, 114 | override val styleType: ScrollbarsLayerContentStyleType = ScrollbarsLayerContentTypeDefaults.Default.StyleType, 115 | val activeColor: Color = ScrollbarsLayerContentTypeDefaults.Default.Colored.IdleActive.ActiveColor, 116 | val inAnimationSpec: AnimationSpec? = ScrollbarsLayerContentTypeDefaults.Default.Colored.IdleActive.InAnimationSpec, 117 | val outAnimationSpec: AnimationSpec? = ScrollbarsLayerContentTypeDefaults.Default.Colored.IdleActive.OutAnimationSpec 118 | ) : Colored { 119 | 120 | /** The [SolidColor] brush with current [idleActiveColor]. */ 121 | override val brush: Brush 122 | get() = SolidColor(value = idleActiveColorState) 123 | 124 | private var idleActiveColorState by mutableStateOf(idleColor) 125 | 126 | /** 127 | * The current color (between [idleColor] and [activeColor]). 128 | * 129 | * @see brush 130 | */ 131 | val idleActiveColor: Color 132 | get() = idleActiveColorState 133 | 134 | /** 135 | * Handles the current [idleActiveColor]. 136 | * 137 | * @param state The current [ScrollbarsState]. 138 | */ 139 | @Composable 140 | internal fun HandleIdleActiveColor(state: ScrollbarsState) { 141 | val isScrollInProgress = state.scrollType.isScrollInProgress 142 | val idleActiveColor = if (isScrollInProgress) { 143 | activeColor 144 | } else { 145 | idleColor 146 | } 147 | 148 | idleActiveColorState = if (inAnimationSpec == null || outAnimationSpec == null) { 149 | idleActiveColor 150 | } else { 151 | animateColorAsState( 152 | targetValue = idleActiveColor, 153 | animationSpec = if (isScrollInProgress) { 154 | inAnimationSpec 155 | } else { 156 | outAnimationSpec 157 | }, 158 | label = "IdleActiveColor" 159 | ).value 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layercontenttype/ScrollbarsLayerContentTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layercontenttype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.animation.core.spring 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.ui.graphics.Brush 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.Shape 10 | import androidx.compose.ui.graphics.SolidColor 11 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 12 | import com.gigamole.composescrollbars.config.layercontenttype.layercontentstyletype.ScrollbarsLayerContentStyleType 13 | 14 | /** 15 | * The default values for [ScrollbarsLayerContentType]. 16 | * 17 | * @see ScrollbarsConfig 18 | * @author GIGAMOLE 19 | */ 20 | object ScrollbarsLayerContentTypeDefaults { 21 | 22 | /** The default values for [ScrollbarsLayerContentType.Default]. */ 23 | object Default { 24 | 25 | /** The default content shape. */ 26 | val Shape: Shape = CircleShape 27 | 28 | /** 29 | * The default content brush. 30 | * 31 | * @see Colored.IdleColor 32 | */ 33 | val Brush: Brush = SolidColor(value = Colored.IdleColor) 34 | 35 | /** The default [ScrollbarsLayerContentStyleType]. */ 36 | val StyleType: ScrollbarsLayerContentStyleType = ScrollbarsLayerContentStyleType.Background 37 | 38 | /** The default values for [ScrollbarsLayerContentType.Default.Colored]. */ 39 | object Colored { 40 | 41 | /** The default content idle color. */ 42 | val IdleColor: Color = Color.LightGray.copy(alpha = 0.65F) 43 | 44 | /** The default values for [ScrollbarsLayerContentType.Default.Colored.IdleActive]. */ 45 | object IdleActive { 46 | 47 | /** The default content active color. */ 48 | val ActiveColor: Color = Color.LightGray 49 | 50 | /** The default content active color in [AnimationSpec]. */ 51 | val InAnimationSpec: AnimationSpec 52 | get() = spring() 53 | 54 | /** The default content active color out [AnimationSpec]. */ 55 | val OutAnimationSpec: AnimationSpec 56 | get() = tween(delayMillis = 750) 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layercontenttype/layercontentstyletype/ScrollbarsLayerContentStyleType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layercontenttype.layercontentstyletype 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 5 | 6 | /** 7 | * The scrollbars layer content style configuration for [ScrollbarsLayerContentType.Default]. 8 | * 9 | * @see ScrollbarsLayerContentType 10 | * @author GIGAMOLE 11 | */ 12 | sealed interface ScrollbarsLayerContentStyleType { 13 | 14 | /** The background layer content style. */ 15 | data object Background : ScrollbarsLayerContentStyleType 16 | 17 | /** 18 | * The outline border layer content style. 19 | * 20 | * @property width The border width. 21 | */ 22 | data class Border( 23 | val width: Dp = ScrollbarsLayerContentStyleTypeDefaults.Border.Width 24 | ) : ScrollbarsLayerContentStyleType 25 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layercontenttype/layercontentstyletype/ScrollbarsLayerContentStyleTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layercontenttype.layercontentstyletype 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 6 | 7 | /** 8 | * The default values for [ScrollbarsLayerContentStyleType]. 9 | * 10 | * @see ScrollbarsLayerContentType.Default 11 | * @author GIGAMOLE 12 | */ 13 | object ScrollbarsLayerContentStyleTypeDefaults { 14 | 15 | /** The default values for [ScrollbarsLayerContentStyleType.Background]. */ 16 | object Border { 17 | 18 | /** The default border width. */ 19 | val Width: Dp = 2.dp 20 | } 21 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/ScrollbarsLayersType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 5 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 6 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerConfig 7 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 8 | 9 | /** 10 | * The scrollbars layers type configuration for [ScrollbarsConfig]. 11 | * 12 | * @see ScrollbarsLayerConfig 13 | * @see ScrollbarsThicknessType 14 | * @see ScrollbarsLayerContentType 15 | * @author GIGAMOLE 16 | */ 17 | sealed interface ScrollbarsLayersType { 18 | 19 | /** 20 | * The scrollbars layers type which wraps a knob layer into a background layer. The layers are centered. 21 | * 22 | * @property thicknessType The knob [ScrollbarsThicknessType]. 23 | * @property paddingValues The background [PaddingValues]. 24 | */ 25 | data class Wrap( 26 | val thicknessType: ScrollbarsThicknessType = ScrollbarsLayersTypeDefaults.Wrap.ThicknessType, 27 | val paddingValues: PaddingValues = ScrollbarsLayersTypeDefaults.Wrap.PaddingValues 28 | ) : ScrollbarsLayersType 29 | 30 | /** 31 | * The scrollbars layers type which splits a knob and a background layer into each own [ScrollbarsLayerConfig]. 32 | * 33 | * @property backgroundLayerConfig The knob [ScrollbarsLayerConfig]. 34 | * @property knobLayerConfig The background [ScrollbarsLayerConfig]. 35 | */ 36 | data class Split( 37 | val backgroundLayerConfig: ScrollbarsLayerConfig = ScrollbarsLayersTypeDefaults.Split.BackgroundLayerConfig, 38 | val knobLayerConfig: ScrollbarsLayerConfig = ScrollbarsLayersTypeDefaults.Split.KnobLayerConfig 39 | ) : ScrollbarsLayersType 40 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/ScrollbarsLayersTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 5 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersTypeDefaults.Wrap.PaddingValues 6 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerConfig 7 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 8 | 9 | /** 10 | * The default values for [ScrollbarsLayersType]. 11 | * 12 | * @see ScrollbarsConfig 13 | * @author GIGAMOLE 14 | */ 15 | class ScrollbarsLayersTypeDefaults { 16 | 17 | /** The default values for [ScrollbarsLayersType.Wrap]. */ 18 | object Wrap { 19 | 20 | /** The default scrollbars layer [ScrollbarsThicknessType]. */ 21 | val ThicknessType: ScrollbarsThicknessType = ScrollbarsThicknessType.Exact() 22 | 23 | /** The default scrollbars layer [PaddingValues]. */ 24 | val PaddingValues: PaddingValues = PaddingValues() 25 | } 26 | 27 | /** The default values for [ScrollbarsLayersType.Split]. */ 28 | object Split { 29 | 30 | /** The default background [ScrollbarsLayerConfig]. */ 31 | val BackgroundLayerConfig: ScrollbarsLayerConfig = ScrollbarsLayerConfig() 32 | 33 | /** The default knob [ScrollbarsLayerConfig]. */ 34 | val KnobLayerConfig: ScrollbarsLayerConfig = ScrollbarsLayerConfig() 35 | } 36 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/layerConfig/ScrollbarsLayerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType.layerConfig 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 5 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 6 | 7 | /** 8 | * The scrollbars layer config inside the layers container for [ScrollbarsLayersType.Split]. 9 | * 10 | * @property thicknessType The scrollbars layer [ScrollbarsThicknessType]. 11 | * @property layerGravity The [ScrollbarsLayerGravity]. 12 | * @property paddingValues The scrollbars layer [PaddingValues]. 13 | * @see ScrollbarsLayersType 14 | * @author GIGAMOLE 15 | */ 16 | data class ScrollbarsLayerConfig( 17 | val thicknessType: ScrollbarsThicknessType = ScrollbarsLayerConfigDefaults.ThicknessType, 18 | val layerGravity: ScrollbarsLayerGravity = ScrollbarsLayerConfigDefaults.Gravity, 19 | val paddingValues: PaddingValues = ScrollbarsLayerConfigDefaults.PaddingValues 20 | ) -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/layerConfig/ScrollbarsLayerConfigDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType.layerConfig 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 5 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 6 | 7 | /** 8 | * The default values for [ScrollbarsLayerConfig]. 9 | * 10 | * @see ScrollbarsLayersType.Split 11 | * @author GIGAMOLE 12 | */ 13 | object ScrollbarsLayerConfigDefaults { 14 | 15 | /** The default scrollbars layer [ScrollbarsThicknessType]. */ 16 | val ThicknessType: ScrollbarsThicknessType = ScrollbarsThicknessType.Exact() 17 | 18 | /** The default [ScrollbarsLayerGravity]. */ 19 | val Gravity: ScrollbarsLayerGravity = ScrollbarsLayerGravity.Center 20 | 21 | /** The default scrollbars layer [PaddingValues]. */ 22 | val PaddingValues: PaddingValues = PaddingValues() 23 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/layerConfig/ScrollbarsLayerGravity.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType.layerConfig 2 | 3 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 4 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 5 | 6 | /** 7 | * The scrollbars layer gravity inside the layers container for [ScrollbarsLayerConfig]. 8 | * 9 | * @see ScrollbarsLayersType.Split 10 | * @author GIGAMOLE 11 | */ 12 | enum class ScrollbarsLayerGravity { 13 | 14 | /** The scrollbars layer container gravity at the start anchor (left or top, depends on [ScrollbarsOrientation]). */ 15 | Start, 16 | 17 | /** The scrollbars layer container gravity at the center anchor. */ 18 | Center, 19 | 20 | /** The scrollbars layer container gravity at the start anchor (right or bottom, depends on [ScrollbarsOrientation]). */ 21 | End 22 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/thicknessType/ScrollbarsThicknessType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType.thicknessType 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 5 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 6 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 7 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerConfig 8 | 9 | /** 10 | * The scrollbars thickness type configuration for knob and/or background [ScrollbarsLayersType]. 11 | * 12 | * @see ScrollbarsLayerConfig 13 | * @see ScrollbarsOrientation 14 | * @author GIGAMOLE 15 | */ 16 | sealed interface ScrollbarsThicknessType { 17 | 18 | /** 19 | * The exact scrollbars thickness size (width or height, depends on [ScrollbarsOrientation]). 20 | * 21 | * @property thickness The exact thickness size. 22 | */ 23 | data class Exact( 24 | val thickness: Dp = ScrollbarsThicknessTypeDefaults.Exact.Thickness 25 | ) : ScrollbarsThicknessType 26 | 27 | /** 28 | * The wrap content scrollbars thickness size (width or height, depends on [ScrollbarsOrientation]). 29 | * 30 | * Basically, it is only used for a [ScrollbarsLayerContentType.Custom], so it is possible to have a dynamic thickness size. 31 | */ 32 | data object Wrap : ScrollbarsThicknessType 33 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/layersType/thicknessType/ScrollbarsThicknessTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.layersType.thicknessType 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 6 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerConfig 7 | 8 | /** 9 | * The default values for [ScrollbarsThicknessType]. 10 | * 11 | * @see ScrollbarsLayersType 12 | * @see ScrollbarsLayerConfig 13 | * @author GIGAMOLE 14 | */ 15 | class ScrollbarsThicknessTypeDefaults { 16 | 17 | /** The default values for [ScrollbarsThicknessType.Exact]. */ 18 | object Exact { 19 | 20 | /** The default exact thickness size. */ 21 | val Thickness: Dp = 8.dp 22 | } 23 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/sizetype/ScrollbarsSizeType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.sizetype 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 5 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 6 | import com.gigamole.composescrollbars.config.sizetype.ScrollbarsSizeType.Full 7 | 8 | /** 9 | * The scrollbars layers container size configuration for [ScrollbarsConfig]. The scrollbars layers container is centered, when the size is not [Full]. 10 | * 11 | * @see ScrollbarsOrientation 12 | * @author GIGAMOLE 13 | */ 14 | sealed interface ScrollbarsSizeType { 15 | 16 | /** The scrollbars layers container size, which occupies all available size (width or height, depends on [ScrollbarsOrientation]). */ 17 | data object Full : ScrollbarsSizeType 18 | 19 | /** 20 | * The scrollbars layers container size, which occupies available size by [fraction] (width or height, depends on [ScrollbarsOrientation]). 21 | * 22 | * @property fraction The layers container size fraction. 23 | */ 24 | data class Fraction( 25 | val fraction: Float = ScrollbarsSizeTypeDefaults.Fraction.Fraction 26 | ) : ScrollbarsSizeType 27 | 28 | /** 29 | * The scrollbars layers container size, which occupies available size by the exact [size] (width or height, depends on [ScrollbarsOrientation]). 30 | * 31 | * @property size The layers container exact size. 32 | */ 33 | data class Exact( 34 | val size: Dp = ScrollbarsSizeTypeDefaults.Exact.Size 35 | ) : ScrollbarsSizeType 36 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/sizetype/ScrollbarsSizeTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.sizetype 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 6 | 7 | /** 8 | * The default values for [ScrollbarsSizeType]. 9 | * 10 | * @see ScrollbarsConfig 11 | * @author GIGAMOLE 12 | */ 13 | object ScrollbarsSizeTypeDefaults { 14 | 15 | /** The default values for [ScrollbarsSizeType.Fraction]. */ 16 | object Fraction { 17 | 18 | /** The default [ScrollbarsSizeType.Fraction.fraction]. */ 19 | const val Fraction = 0.5F 20 | } 21 | 22 | /** The default values for [ScrollbarsSizeType.Exact]. */ 23 | object Exact { 24 | 25 | /** The default [ScrollbarsSizeType.Exact.size]. */ 26 | val Size = 300.dp 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/visibilitytype/ScrollbarsVisibilityType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.visibilitytype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableFloatStateOf 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.unit.IntOffset 12 | import androidx.compose.ui.unit.IntSize 13 | import androidx.compose.ui.util.lerp 14 | import com.gigamole.composescrollbars.ScrollbarsState 15 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 16 | import com.gigamole.composescrollbars.config.ScrollbarsGravity 17 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 18 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 19 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 20 | 21 | 22 | /** 23 | * The scrollbars visibility configuration for [ScrollbarsConfig]. 24 | * 25 | * @see ScrollbarsScrollType 26 | * @author GIGAMOLE 27 | */ 28 | sealed interface ScrollbarsVisibilityType { 29 | 30 | /** The static scrollbars visibility. Always visible. */ 31 | data object Static : ScrollbarsVisibilityType 32 | 33 | /** The dynamic scrollbars visibility. Includes in/out visibility animation, fading and other. */ 34 | sealed class Dynamic : ScrollbarsVisibilityType { 35 | 36 | /** 37 | * In animation spec. Null disables the animation. 38 | * 39 | * @see HandleFraction 40 | */ 41 | abstract val inAnimationSpec: AnimationSpec? 42 | 43 | /** 44 | * Out animation spec. Null disables the animation. 45 | * 46 | * @see HandleFraction 47 | */ 48 | abstract val outAnimationSpec: AnimationSpec? 49 | 50 | /** 51 | * Indicates whether the dynamic visibility is animated with fade. 52 | * 53 | * @see fadeFraction 54 | */ 55 | abstract val isFaded: Boolean 56 | 57 | /** 58 | * Indicates whether scrollbars are visible when the scroll is not possible (short content). 59 | * 60 | * @see ScrollbarsScrollType.isScrollPossible 61 | */ 62 | abstract val isVisibleWhenScrollNotPossible: Boolean 63 | 64 | /** 65 | * Indicates whether scrollbars are visible when any press/touch down event occurred. 66 | * 67 | * @see cancelAwaitTouchDown 68 | * @see handleAwaitTouchDownRelease 69 | */ 70 | abstract val isVisibleOnTouchDown: Boolean 71 | 72 | /** 73 | * Indicates whether scrollbars are statically visible only when the scroll is possible. 74 | * 75 | * @see ScrollbarsScrollType.isScrollPossible 76 | */ 77 | abstract val isStaticWhenScrollPossible: Boolean 78 | 79 | private var fractionState by mutableFloatStateOf(0.0F) 80 | private var isAwaitTouchDownState by mutableStateOf(false) 81 | private var isAwaitTouchDownAnimationFinishedState by mutableStateOf(false) 82 | private var isHighlightingState by mutableStateOf(false) 83 | 84 | /** 85 | * Raw visibility fraction. Range from 0.0F to 1.0F. 86 | * 87 | * @see HandleFraction 88 | */ 89 | val fraction: Float 90 | get() = fractionState 91 | 92 | /** 93 | * Fade fraction. Range from 0.0F to 1.0F. 94 | * 95 | * @see isFaded 96 | */ 97 | val fadeFraction: Float 98 | get() = if (isFaded) { 99 | fraction 100 | } else { 101 | 1.0F 102 | } 103 | 104 | /** 105 | * Cancels the pending auto-hide touch/press down sequence. 106 | * 107 | * @see isVisibleOnTouchDown 108 | */ 109 | internal fun cancelAwaitTouchDown() { 110 | if (inAnimationSpec == null || outAnimationSpec == null) { 111 | return 112 | } 113 | 114 | isAwaitTouchDownState = false 115 | } 116 | 117 | /** 118 | * Handles the pending auto-hide touch/press down sequence. 119 | * 120 | * @see isVisibleOnTouchDown 121 | */ 122 | internal fun handleAwaitTouchDownRelease() { 123 | if (inAnimationSpec == null || outAnimationSpec == null) { 124 | return 125 | } 126 | 127 | if (isAwaitTouchDownAnimationFinishedState.not()) { 128 | isAwaitTouchDownState = true 129 | } 130 | } 131 | 132 | /** 133 | * Handles the visibility [fraction] by calculating all provided fields. 134 | * 135 | * @param state The current [ScrollbarsState]. 136 | * @see ScrollbarsScrollType 137 | */ 138 | @Composable 139 | internal fun HandleFraction(state: ScrollbarsState) { 140 | val localInAnimationSpec = inAnimationSpec 141 | val localOutAnimationSpec = outAnimationSpec 142 | val isVisible = with(state.scrollType) { 143 | val isVisibleWhenScrollNotPossibleFlag = if (isVisibleWhenScrollNotPossible) { 144 | true 145 | } else { 146 | isScrollPossible 147 | } 148 | val isVisibleOnTouchDownFlag = if (isVisibleOnTouchDown) { 149 | state.isTouchDown || isAwaitTouchDownState 150 | } else { 151 | false 152 | } 153 | val isStaticWhenScrollPossibleFlag = if (isStaticWhenScrollPossible) { 154 | isScrollPossible 155 | } else { 156 | isScrollInProgress 157 | } 158 | 159 | (isStaticWhenScrollPossibleFlag || isVisibleOnTouchDownFlag || isHighlightingState) && isVisibleWhenScrollNotPossibleFlag 160 | } 161 | val targetValue = if (isVisible) { 162 | 1.0F 163 | } else { 164 | 0.0F 165 | } 166 | 167 | fractionState = if (localInAnimationSpec != null && localOutAnimationSpec != null) { 168 | animateFloatAsState( 169 | targetValue = targetValue, 170 | animationSpec = if (isVisible) { 171 | localInAnimationSpec 172 | } else { 173 | localOutAnimationSpec 174 | }, 175 | finishedListener = { value -> 176 | isAwaitTouchDownAnimationFinishedState = value == 1.0F 177 | 178 | if (isAwaitTouchDownState) { 179 | isAwaitTouchDownState = false 180 | } 181 | 182 | if (isHighlightingState) { 183 | if (value == 1.0F) { 184 | isHighlightingState = false 185 | } 186 | } 187 | }, 188 | label = "VisibilityFractionState" 189 | ).value.also { 190 | isAwaitTouchDownAnimationFinishedState = it == 1.0F 191 | } 192 | } else { 193 | targetValue 194 | } 195 | } 196 | 197 | /** 198 | * Highlights (shows then hides) the scrollbars, so the [fraction] goes from 0.0F to 1.0F and then back to 0.0F. 199 | * 200 | * @see HandleFraction 201 | */ 202 | fun highlight() { 203 | isHighlightingState = true 204 | 205 | if (fractionState == 1.0F) { 206 | isHighlightingState = false 207 | } 208 | } 209 | 210 | /** 211 | * The dynamic visibility with only fade. 212 | * 213 | * @property inAnimationSpec The in [AnimationSpec]. Null disables the animation. 214 | * @property outAnimationSpec The out [AnimationSpec]. Null disables the animation. 215 | * @property isVisibleWhenScrollNotPossible Indicates whether scrollbars are visible when the scroll is not possible (short content). 216 | * @property isVisibleOnTouchDown Indicates whether scrollbars are visible when any press/touch down event occurred. 217 | * @property isStaticWhenScrollPossible Indicates whether scrollbars are statically visible only when the scroll is possible. 218 | */ 219 | data class Fade( 220 | override val inAnimationSpec: AnimationSpec? = ScrollbarsVisibilityTypeDefaults.Dynamic.InAnimationSpec, 221 | override val outAnimationSpec: AnimationSpec? = ScrollbarsVisibilityTypeDefaults.Dynamic.OutAnimationSpec, 222 | override val isVisibleWhenScrollNotPossible: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsVisibleWhenScrollNotPossible, 223 | override val isVisibleOnTouchDown: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsVisibleOnTouchDown, 224 | override val isStaticWhenScrollPossible: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsStaticWhenScrollPossible 225 | ) : Dynamic() { 226 | 227 | override val isFaded: Boolean 228 | get() = true 229 | } 230 | 231 | /** 232 | * The dynamic visibility with the slide, and optional fade. 233 | * 234 | * @property inAnimationSpec The in [AnimationSpec]. Null disables the animation. 235 | * @property outAnimationSpec The out [AnimationSpec]. Null disables the animation. 236 | * @property isFaded Indicates whether the dynamic visibility is animated with fade. 237 | * @property isVisibleWhenScrollNotPossible Indicates whether scrollbars are visible when the scroll is not possible (short content). 238 | * @property isVisibleOnTouchDown Indicates whether scrollbars are visible when any press/touch down event occurred. 239 | * @property isStaticWhenScrollPossible Indicates whether scrollbars are statically visible only when the scroll is possible. 240 | * @see ScrollbarsLayersType 241 | */ 242 | data class Slide( 243 | override val inAnimationSpec: AnimationSpec? = ScrollbarsVisibilityTypeDefaults.Dynamic.InAnimationSpec, 244 | override val outAnimationSpec: AnimationSpec? = ScrollbarsVisibilityTypeDefaults.Dynamic.OutAnimationSpec, 245 | override val isFaded: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsFaded, 246 | override val isVisibleWhenScrollNotPossible: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsVisibleWhenScrollNotPossible, 247 | override val isVisibleOnTouchDown: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsVisibleOnTouchDown, 248 | override val isStaticWhenScrollPossible: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsStaticWhenScrollPossible 249 | ) : Dynamic() { 250 | 251 | private var targetSlideOffsetState by mutableStateOf(IntOffset.Zero) 252 | 253 | /** The current slide offset. Range (+/- and X/Y) from captured scrollbars layers container size to 0.0F. */ 254 | val slideOffset by derivedStateOf { 255 | targetSlideOffsetState.times(1.0F - fraction) 256 | } 257 | 258 | /** 259 | * Handles target slide offset by capturing the current scrollbars layers container [size] and mapping it to a correct side (left, top, right or bottom, 260 | * depends on [ScrollbarsOrientation] and [ScrollbarsGravity]). 261 | * 262 | * @param state The current [ScrollbarsState]. 263 | * @param size The captured scrollbars layers container [size]. 264 | */ 265 | internal fun handleTargetSlideOffset( 266 | state: ScrollbarsState, 267 | size: IntSize 268 | ) { 269 | with(state) { 270 | targetSlideOffsetState = when (config.orientation) { 271 | ScrollbarsOrientation.Vertical -> { 272 | when (config.gravity) { 273 | ScrollbarsGravity.Start -> { 274 | IntOffset( 275 | x = -size.width, 276 | y = 0 277 | ) 278 | } 279 | ScrollbarsGravity.End -> { 280 | IntOffset( 281 | x = size.width, 282 | y = 0 283 | ) 284 | } 285 | } 286 | } 287 | ScrollbarsOrientation.Horizontal -> { 288 | when (config.gravity) { 289 | ScrollbarsGravity.Start -> { 290 | IntOffset( 291 | x = 0, 292 | y = -size.height 293 | ) 294 | } 295 | ScrollbarsGravity.End -> { 296 | IntOffset( 297 | x = 0, 298 | y = size.height 299 | ) 300 | } 301 | } 302 | } 303 | } 304 | } 305 | } 306 | } 307 | 308 | /** 309 | * The dynamic visibility with the scale, and optional fade. 310 | * 311 | * @property inAnimationSpec The in [AnimationSpec]. Null disables the animation. 312 | * @property outAnimationSpec The out [AnimationSpec]. Null disables the animation. 313 | * @property isFaded Indicates whether the dynamic visibility is animated with fade. 314 | * @property isVisibleWhenScrollNotPossible Indicates whether scrollbars are visible when the scroll is not possible (short content). 315 | * @property isVisibleOnTouchDown Indicates whether scrollbars are visible when any press/touch down event occurred. 316 | * @property isStaticWhenScrollPossible Indicates whether scrollbars are statically visible only when the scroll is possible. 317 | * @property startScale The start (from) scale. The stop scale is at 1.0F. 318 | */ 319 | data class Scale( 320 | override val inAnimationSpec: AnimationSpec? = ScrollbarsVisibilityTypeDefaults.Dynamic.InAnimationSpec, 321 | override val outAnimationSpec: AnimationSpec? = ScrollbarsVisibilityTypeDefaults.Dynamic.OutAnimationSpec, 322 | override val isFaded: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsFaded, 323 | override val isVisibleWhenScrollNotPossible: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsVisibleWhenScrollNotPossible, 324 | override val isVisibleOnTouchDown: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsVisibleOnTouchDown, 325 | override val isStaticWhenScrollPossible: Boolean = ScrollbarsVisibilityTypeDefaults.Dynamic.IsStaticWhenScrollPossible, 326 | val startScale: Float = ScrollbarsVisibilityTypeDefaults.Dynamic.Scale.StartScale 327 | ) : Dynamic() { 328 | 329 | /** The current scale. Range from [startScale] to 1.0F. */ 330 | val scaleValue by derivedStateOf { 331 | lerp( 332 | start = startScale, 333 | stop = 1.0F, 334 | fraction = fraction 335 | ) 336 | } 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/config/visibilitytype/ScrollbarsVisibilityTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.config.visibilitytype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.animation.core.spring 5 | import androidx.compose.animation.core.tween 6 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 7 | 8 | /** 9 | * The default values for [ScrollbarsVisibilityType]. 10 | * 11 | * @see ScrollbarsConfig 12 | * @author GIGAMOLE 13 | */ 14 | object ScrollbarsVisibilityTypeDefaults { 15 | 16 | /** The default values for [ScrollbarsVisibilityType.Dynamic]. */ 17 | object Dynamic { 18 | 19 | /** The default in [AnimationSpec]. */ 20 | val InAnimationSpec: AnimationSpec 21 | get() = spring() 22 | 23 | /** The default out [AnimationSpec]. */ 24 | val OutAnimationSpec: AnimationSpec 25 | get() = tween(delayMillis = 750) 26 | 27 | /** The default is faded indicator. */ 28 | const val IsFaded: Boolean = true 29 | 30 | /** The default is visible when scroll is not possible (short content) indicator. */ 31 | const val IsVisibleWhenScrollNotPossible: Boolean = false 32 | 33 | /** The default is visible on any touch/press down indicator. */ 34 | const val IsVisibleOnTouchDown: Boolean = false 35 | 36 | /** The default are scrollbars statically visible only when the scroll is possible indicator. */ 37 | const val IsStaticWhenScrollPossible: Boolean = true 38 | 39 | /** The default values for [ScrollbarsVisibilityType.Dynamic.Scale]. */ 40 | object Scale { 41 | 42 | /** The default start scale. The stop scale is at 1.0F. */ 43 | const val StartScale = 0.75F 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/scrolltype/ScrollbarsScrollType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.scrolltype 2 | 3 | import androidx.compose.foundation.ScrollState 4 | import androidx.compose.foundation.lazy.LazyListState 5 | import androidx.compose.foundation.lazy.grid.LazyGridState 6 | import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState 7 | import com.gigamole.composescrollbars.ScrollbarsState 8 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsDynamicKnobType 9 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsKnobType 10 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsStaticKnobType 11 | 12 | /** 13 | * The scrollbars scroll configuration for [ScrollbarsState]. 14 | * 15 | * @author GIGAMOLE 16 | */ 17 | sealed interface ScrollbarsScrollType { 18 | 19 | /** The [ScrollbarsKnobType]. */ 20 | val knobType: ScrollbarsKnobType 21 | 22 | /** Indicates whether the scroll is in progress. */ 23 | val isScrollInProgress: Boolean 24 | 25 | /** Indicates whether the scroll is possible. */ 26 | val isScrollPossible: Boolean 27 | 28 | /** 29 | * The scrollbars for a [ScrollState] content. 30 | * 31 | * @property knobType The [ScrollbarsStaticKnobType]. 32 | * @property state The [ScrollState]. 33 | */ 34 | data class Scroll( 35 | override val knobType: ScrollbarsStaticKnobType = ScrollbarsScrollTypeDefaults.StaticKnobType, 36 | val state: ScrollState, 37 | ) : ScrollbarsScrollType { 38 | 39 | /** Indicates whether the scroll is in progress. */ 40 | override val isScrollInProgress: Boolean 41 | get() = state.isScrollInProgress 42 | 43 | /** Indicates whether the scroll is possible. */ 44 | override val isScrollPossible: Boolean 45 | get() = state.canScrollForward || state.canScrollBackward 46 | } 47 | 48 | /** The scrollbars for a lazy scrollable content. */ 49 | sealed interface Lazy : ScrollbarsScrollType { 50 | 51 | /** The scrollbars for a [LazyListState] content. */ 52 | sealed interface List : Lazy { 53 | 54 | /** The [LazyListState]. */ 55 | val state: LazyListState 56 | 57 | /** Indicates whether the scroll is in progress. */ 58 | override val isScrollInProgress: Boolean 59 | get() = state.isScrollInProgress 60 | 61 | /** Indicates whether the scroll is possible. */ 62 | override val isScrollPossible: Boolean 63 | get() = state.canScrollForward || state.canScrollBackward 64 | 65 | /** 66 | * The scrollbars for a [LazyListState] content with a static items heights. 67 | * 68 | * @property knobType The [ScrollbarsStaticKnobType]. 69 | * @property state The [LazyListState]. 70 | */ 71 | data class Static( 72 | override val knobType: ScrollbarsStaticKnobType = ScrollbarsScrollTypeDefaults.StaticKnobType, 73 | override val state: LazyListState 74 | ) : List 75 | 76 | /** 77 | * The scrollbars for a [LazyListState] content with a dynamic items heights. 78 | * 79 | * @property knobType The [ScrollbarsDynamicKnobType]. 80 | * @property state The [LazyListState]. 81 | */ 82 | data class Dynamic( 83 | override val knobType: ScrollbarsDynamicKnobType = ScrollbarsScrollTypeDefaults.DynamicKnobType, 84 | override val state: LazyListState 85 | ) : List 86 | } 87 | 88 | /** The scrollbars for a [LazyGridState] content. */ 89 | sealed interface Grid : Lazy { 90 | 91 | /** The [LazyGridState]. */ 92 | val state: LazyGridState 93 | 94 | /** The grid span count. */ 95 | val spanCount: Int 96 | 97 | /** Indicates whether the scroll is in progress. */ 98 | override val isScrollInProgress: Boolean 99 | get() = state.isScrollInProgress 100 | 101 | /** Indicates whether the scroll is possible. */ 102 | override val isScrollPossible: Boolean 103 | get() = state.canScrollForward || state.canScrollBackward 104 | 105 | /** 106 | * The scrollbars for a [LazyGridState] content with a static items heights. 107 | * 108 | * @property knobType The [ScrollbarsStaticKnobType]. 109 | * @property state The [LazyGridState]. 110 | * @property spanCount The grid span count. 111 | */ 112 | data class Static( 113 | override val knobType: ScrollbarsStaticKnobType = ScrollbarsScrollTypeDefaults.StaticKnobType, 114 | override val state: LazyGridState, 115 | override val spanCount: Int 116 | ) : Grid 117 | 118 | /** 119 | * The scrollbars for a [LazyGridState] content with a dynamic items heights. 120 | * 121 | * @property knobType The [ScrollbarsDynamicKnobType]. 122 | * @property state The [LazyGridState]. 123 | * @property spanCount The grid span count. 124 | */ 125 | data class Dynamic( 126 | override val knobType: ScrollbarsDynamicKnobType = ScrollbarsScrollTypeDefaults.DynamicKnobType, 127 | override val state: LazyGridState, 128 | override val spanCount: Int 129 | ) : Grid 130 | } 131 | 132 | /** 133 | * The scrollbars for a [LazyStaggeredGridState] content. 134 | * 135 | * The ComposeScrollbars library highly recommends to use a [knobType] with a provided [ScrollbarsDynamicKnobType.animationSpec], because staggered layout 136 | * sometimes can be unpredicted and some items can be placed in the way that the interpolation between these items is too short or almost zero, which causes some 137 | * knob jumps. Also, [LazyStaggeredGridState.layoutInfo] item spacing, before and after content paddings, causes some extra calculations, so if it is possible, 138 | * add these padding to the items/cards instead, to improve performance. 139 | * 140 | * @property knobType The [ScrollbarsDynamicKnobType]. 141 | * @property state The [LazyStaggeredGridState]. 142 | * @property spanCount The grid span count. 143 | */ 144 | data class StaggeredGrid( 145 | override val knobType: ScrollbarsDynamicKnobType = ScrollbarsScrollTypeDefaults.DynamicKnobType, 146 | val state: LazyStaggeredGridState, 147 | val spanCount: Int 148 | ) : Lazy { 149 | 150 | /** Indicates whether the scroll is in progress. */ 151 | override val isScrollInProgress: Boolean 152 | get() = state.isScrollInProgress 153 | 154 | /** Indicates whether the scroll is possible. */ 155 | override val isScrollPossible: Boolean 156 | get() = state.canScrollForward || state.canScrollBackward 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/scrolltype/ScrollbarsScrollTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.scrolltype 2 | 3 | import com.gigamole.composescrollbars.ScrollbarsState 4 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsDynamicKnobType 5 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsStaticKnobType 6 | 7 | /** 8 | * The default values for [ScrollbarsScrollType]. 9 | * 10 | * @see ScrollbarsState 11 | * @author GIGAMOLE 12 | */ 13 | object ScrollbarsScrollTypeDefaults { 14 | 15 | /** The default static [ScrollbarsStaticKnobType]. */ 16 | val StaticKnobType: ScrollbarsStaticKnobType = ScrollbarsStaticKnobType.Auto() 17 | 18 | /** The default dynamic [ScrollbarsStaticKnobType]. */ 19 | val DynamicKnobType: ScrollbarsDynamicKnobType = ScrollbarsDynamicKnobType.Auto() 20 | } 21 | -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/scrolltype/knobtype/ScrollbarsDynamicKnobType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.scrolltype.knobtype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.ui.unit.Dp 5 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 6 | 7 | /** 8 | * The scrollbars dynamic knob configuration for [ScrollbarsScrollType]. 9 | * 10 | * @see ScrollbarsStaticKnobType 11 | * @see ScrollbarsKnobType 12 | * @author GIGAMOLE 13 | */ 14 | sealed interface ScrollbarsDynamicKnobType : ScrollbarsKnobType { 15 | 16 | /** The knob [AnimationSpec]. Null disables the animation. */ 17 | override val animationSpec: AnimationSpec? 18 | 19 | /** 20 | * The scrollbars dynamic knob with an automatic size. 21 | * 22 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 23 | */ 24 | data class Auto( 25 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec 26 | ) : ScrollbarsDynamicKnobType 27 | 28 | /** 29 | * The scrollbars dynamic knob with a fraction size. 30 | * 31 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 32 | * @property fraction The knob fraction size. 33 | */ 34 | data class Fraction( 35 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec, 36 | val fraction: Float = ScrollbarsKnobTypeDefaults.Fraction.Fraction 37 | ) : ScrollbarsDynamicKnobType 38 | 39 | /** 40 | * The scrollbars dynamic knob with an exact size. 41 | * 42 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 43 | * @property size The knob exact size. 44 | */ 45 | data class Exact( 46 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec, 47 | val size: Dp = ScrollbarsKnobTypeDefaults.Exact.Size 48 | ) : ScrollbarsDynamicKnobType 49 | 50 | /** 51 | * The scrollbars dynamic knob with a size, which represents current visible items as a section or with sub-interpolation. 52 | * 53 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 54 | * @property isSubLerp Indicates whether the knob size is sub interpolated between items. 55 | */ 56 | data class Worm( 57 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec, 58 | val isSubLerp: Boolean = ScrollbarsKnobTypeDefaults.Worm.IsSubLerp 59 | ) : ScrollbarsDynamicKnobType 60 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/scrolltype/knobtype/ScrollbarsKnobType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.scrolltype.knobtype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 5 | 6 | /** 7 | * The scrollbars generic knob configuration for [ScrollbarsScrollType]. 8 | * 9 | * @see ScrollbarsStaticKnobType 10 | * @see ScrollbarsDynamicKnobType 11 | */ 12 | interface ScrollbarsKnobType { 13 | 14 | /** The knob [AnimationSpec]. Null disables the animation. */ 15 | val animationSpec: AnimationSpec? 16 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/scrolltype/knobtype/ScrollbarsKnobTypeDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.scrolltype.knobtype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.animation.core.spring 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 8 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsKnobTypeDefaults.AnimationSpec 9 | 10 | /** 11 | * The default values for [ScrollbarsKnobType]. 12 | * 13 | * @see ScrollbarsStaticKnobType 14 | * @see ScrollbarsDynamicKnobType 15 | * @see ScrollbarsScrollType 16 | * @author GIGAMOLE 17 | */ 18 | object ScrollbarsKnobTypeDefaults { 19 | 20 | /** The default knob [AnimationSpec]. */ 21 | val AnimationSpec: AnimationSpec 22 | get() = spring() 23 | 24 | /** The default values for static and dynamic fraction [ScrollbarsKnobType]. */ 25 | object Fraction { 26 | 27 | /** The default fraction knob size. */ 28 | const val Fraction: Float = 0.5F 29 | } 30 | 31 | /** The default values for static and dynamic exact [ScrollbarsKnobType]. */ 32 | object Exact { 33 | 34 | /** The default exact knob size. */ 35 | val Size: Dp = 120.dp 36 | } 37 | 38 | /** The default values for [ScrollbarsDynamicKnobType.Worm]. */ 39 | object Worm { 40 | 41 | /** The default is knob size sub interpolated between items indicator. */ 42 | const val IsSubLerp: Boolean = true 43 | } 44 | } -------------------------------------------------------------------------------- /ComposeScrollbars/src/main/kotlin/com/gigamole/composescrollbars/scrolltype/knobtype/ScrollbarsStaticKnobType.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.scrolltype.knobtype 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.ui.unit.Dp 5 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 6 | 7 | /** 8 | * The scrollbars static knob configuration for [ScrollbarsScrollType]. 9 | * 10 | * @see ScrollbarsDynamicKnobType 11 | * @see ScrollbarsKnobType 12 | * @author GIGAMOLE 13 | */ 14 | sealed interface ScrollbarsStaticKnobType : ScrollbarsKnobType { 15 | 16 | /** The knob [AnimationSpec]. Null disables the animation. */ 17 | override val animationSpec: AnimationSpec? 18 | 19 | /** 20 | * The scrollbars static knob with an automatic size. 21 | * 22 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 23 | */ 24 | data class Auto( 25 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec 26 | ) : ScrollbarsStaticKnobType 27 | 28 | /** 29 | * The scrollbars static knob with a fraction size. 30 | * 31 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 32 | * @property fraction The knob fraction size. 33 | */ 34 | data class Fraction( 35 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec, 36 | val fraction: Float = ScrollbarsKnobTypeDefaults.Fraction.Fraction 37 | ) : ScrollbarsStaticKnobType 38 | 39 | /** 40 | * The scrollbars static knob with an exact size. 41 | * 42 | * @property animationSpec The knob [AnimationSpec]. Null disables the animation. 43 | * @property size The knob exact size. 44 | */ 45 | data class Exact( 46 | override val animationSpec: AnimationSpec? = ScrollbarsKnobTypeDefaults.AnimationSpec, 47 | val size: Dp = ScrollbarsKnobTypeDefaults.Exact.Size 48 | ) : ScrollbarsStaticKnobType 49 | } -------------------------------------------------------------------------------- /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.png) 2 | 3 | ![](https://jitpack.io/v/GIGAMOLE/ComposeScrollbars.svg?style=flat-square) | [Setup Guide](#setup) 4 | | [Report new issue](https://github.com/GIGAMOLE/ComposeScrollbars/issues/new) 5 | 6 | # ComposeScrollbars 7 | 8 | The `ComposeScrollbars` is a feature-rich Android Compose UI library that seamlessly incorporates customisable scrollbars, including size, animations, background/knob layer style, and scroll behavior, for a seamless UX. 9 | 10 | ![](/media/demo.gif) 11 | 12 | Features: 13 | 14 | - **Advanced Customization:** Customize scrollbar size, orientation, gravity and visibility animation. 15 | - **Layers Configuration:** Configure background and knob layers with style, appearance and animation. 16 | - **Knob Scroll Behavior:** Choose from static, worm, fraction, or exact scroll behaviors for the knob. 17 | - **Scrolls States:** Ready for `ScrollState`, `LazyListState`, `LazyGridState`, `LazyStaggeredGridState`. 18 | - **Custom Content:** Design custom backgrounds and knobs when required. 19 | - **Sample App:** Explore and experiment with [sample app](#sample-app). 20 | 21 | ## Sample App 22 | 23 | | Sample 1 | Sample 2 | Sample 3 | 24 | |----------------------------------------------|----------------------------------------------|----------------------------------------------| 25 | | | | | 26 | 27 | | Sample 4 | Sample 5 | Sample 6 | 28 | |----------------------------------------------|----------------------------------------------|----------------------------------------------| 29 | | | | | 30 | 31 | Download or clone this repository to discover the sample app. 32 | 33 | ## Setup 34 | 35 | Add to the root `build.gradle.kts`: 36 | 37 | ``` groovy 38 | allprojects { 39 | repositories { 40 | ... 41 | maven("https://jitpack.io") 42 | } 43 | } 44 | ``` 45 | 46 | Add to the package `build.gradle.kts`: 47 | 48 | ``` groovy 49 | dependencies { 50 | implementation("com.github.GIGAMOLE:ComposeScrollbars:{latest-version}") 51 | } 52 | ``` 53 | 54 | Latest version: ![](https://jitpack.io/v/GIGAMOLE/ComposeScrollbars.svg?style=flat-square). 55 | 56 | Also, it's possible to download the latest artifact from the [releases page](https://github.com/GIGAMOLE/ComposeScrollbars/releases). 57 | 58 | ## Guide 59 | 60 | The `ComposeScrollbars` comes with the main component: [`Scrollbars`](#scrollbars). 61 | 62 | For more technical and detailed documentation, read the library `KDoc`. 63 | 64 | ### Scrollbars 65 | 66 | The `Scrollbars` presents scrollbars based on the provided [`ScrollbarsState`](#scrollbarsstate). 67 | 68 | Just place it above (Z-order) of your scrollable content so its visible. 69 | 70 | ### ScrollbarsState 71 | 72 | The `ScrollbarsState` consists of two required components: [`ScrollbarsConfig`](#scrollbarsconfig) and [`ScrollbarsScrollType`](#scrollbarsscrolltype). 73 | 74 | To create a `ScrollbarsState`, use one of the utility functions: `rememberScrollbarsState(...)` or make it on your own. 75 | 76 | #### ScrollbarsConfig 77 | 78 | The `ScrollbarsConfig` setups the scrollbars layouts, styles and appearances: 79 | 80 | | Param | Description | 81 | |------------------------------|-----------------------------------------------------------------------------| 82 | | `orientation` | The scrollbars orientation: `Horizontal` or `Vertical`. | 83 | | `gravity` | The scrollbars gravity: `Start` or `End`. | 84 | | `isReverseLayout` | The scrollbars reverse layout indicator. | 85 | | `paddingValues` | The scrollbars layers container padding values. | 86 | | `sizeType` | The scrollbars layers container size: `Full`, `Fraction` or `Exact`. | 87 | | `layersType` | The [`ScrollbarsLayersType`](#scrollbarslayerstype). | 88 | | `backgroundLayerContentType` | The background [`ScrollbarsLayerContentType`](#scrollbarslayercontenttype). | 89 | | `knobLayerContentType` | The knob [`ScrollbarsLayerContentType`](#scrollbarslayercontenttype). | 90 | | `visibilityType` | The [`ScrollbarsVisibilityType`](#scrollbarsvisibilitytype). | 91 | 92 | ##### ScrollbarsLayersType 93 | 94 | The `ScrollbarsLayersType` can be one of the following: 95 | 96 | - `Wrap`: Wraps a knob layer into a background layer. The layers are centered. 97 | - `Split`: Splits a knob and a background layer into each own configurable layer. 98 | 99 | Each mode can set layers thickness(`Exact` or `Wrap`), padding values, and layer gravity (`Start`, `Center` or `End`). 100 | 101 | ##### ScrollbarsLayerContentType 102 | 103 | The `ScrollbarsLayerContentType` can be one of the following: 104 | 105 | - `None`: The empty (not visible) layer content. 106 | - `Custom`: Provides custom layer content via the `@Composable` lambda. 107 | - `Default`: The default layer content. 108 | 109 | The `Default` mode can set the content layer shape, style (`Backgrond` or `Border`), color (`Idle` or `IdleActive`). 110 | 111 | ##### ScrollbarsVisibilityType 112 | 113 | The `ScrollbarsVisibilityType` can be one of the following: 114 | 115 | - `Static`: The static scrollbars visibility. 116 | - `Dynamic`: The dynamic scrollbars visibility. Includes in/out animation, fading and other. 117 | 118 | The `Dynamic` mode can be one of the following: 119 | 120 | - `Fade`: The dynamic visibility with only fade. 121 | - `Slide`: The dynamic visibility with the slide, and optional fade. 122 | - `Scale`: The dynamic visibility with the scale, and optional fade. 123 | 124 | The `Dynamic` mode can the following UX utility params: 125 | 126 | - `isVisibleWhenScrollNotPossible`: Indicates whether scrollbars are visible when the scroll is not possible (short content). 127 | - `isVisibleOnTouchDown`: Indicates whether scrollbars are visible when any press/touch down event occurred. 128 | - `isStaticWhenScrollPossible`: Indicates whether scrollbars are statically visible only when the scroll is possible. 129 | 130 | #### ScrollbarsScrollType 131 | 132 | The `ScrollbarsScrollType` can be one of the following: 133 | 134 | - `Scroll`: The scrollbars for a `ScrollState` content. 135 | - `Lazy.List`: The scrollbars for a `LazyListState` content. 136 | - `Lazy.Grid`: The scrollbars for a `LazyGridState` content. 137 | - `Lazy.StaggeredGrid`: The scrollbars for a `LazyStaggeredGridState` content. 138 | 139 | The `Lazy.List` and `Lazy.Grid` supports the scrollbars for `Static` or `Dynamic` items heights. 140 | 141 | The `Lazy.StaggeredGrid` only supports the scrollbars for `Dynamic` items heights. 142 | 143 | Each mode can set the knob size type: 144 | 145 | - `Auto`: The scrollbars knob with an automatic size. 146 | - `Exact`: The scrollbars knob with an exact size. 147 | - `Fraction`: The scrollbars knob with a fraction size. 148 | - `Worm`: The scrollbars knob with a size, which represents current visible items as a section or with sub-interpolation. Only available for `Dynamic` item heights. 149 | 150 | ## License 151 | 152 | MIT License. See the [LICENSE](https://github.com/GIGAMOLE/ComposeScrollbars/blob/master/LICENSE) file for more details. 153 | 154 | ## Credits 155 | 156 | Special thanks to the [GoDaddy](https://github.com/godaddy) for the amazing [color picker library](https://github.com/godaddy/compose-color-picker). 157 | 158 | ## Author: 159 | 160 | [Basil Miller](https://www.linkedin.com/in/gigamole/) 161 | [gigamole53@gmail.com](mailto:gigamole53@gmail.com) 162 | -------------------------------------------------------------------------------- /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("composescrollbars.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.composeScrollbars) 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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composescrollbars/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.systemBarsPadding 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.core.view.WindowCompat 12 | 13 | class MainActivity : ComponentActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | WindowCompat.setDecorFitsSystemWindows(window, false) 19 | 20 | setContent { 21 | Box( 22 | modifier = Modifier 23 | .fillMaxSize() 24 | .systemBarsPadding() 25 | ) { 26 | MainScreen() 27 | } 28 | } 29 | } 30 | } 31 | 32 | @Composable 33 | private fun MainScreen() { 34 | MainTheme { 35 | MainScreenContent() 36 | // MainScreenDemoContent() 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composescrollbars/sample/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.sample 2 | 3 | import android.app.Application 4 | 5 | class MainApplication : Application() -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composescrollbars/sample/MainScreenDemoContent.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.sample 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.animateColorAsState 5 | import androidx.compose.animation.core.Spring 6 | import androidx.compose.animation.core.animateFloatAsState 7 | import androidx.compose.animation.core.spring 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.animation.scaleIn 12 | import androidx.compose.animation.scaleOut 13 | import androidx.compose.animation.togetherWith 14 | import androidx.compose.foundation.Canvas 15 | import androidx.compose.foundation.Image 16 | import androidx.compose.foundation.background 17 | import androidx.compose.foundation.border 18 | import androidx.compose.foundation.clickable 19 | import androidx.compose.foundation.horizontalScroll 20 | import androidx.compose.foundation.interaction.MutableInteractionSource 21 | import androidx.compose.foundation.layout.Arrangement 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.PaddingValues 25 | import androidx.compose.foundation.layout.Row 26 | import androidx.compose.foundation.layout.fillMaxHeight 27 | import androidx.compose.foundation.layout.fillMaxSize 28 | import androidx.compose.foundation.layout.fillMaxWidth 29 | import androidx.compose.foundation.layout.height 30 | import androidx.compose.foundation.layout.padding 31 | import androidx.compose.foundation.layout.size 32 | import androidx.compose.foundation.layout.width 33 | import androidx.compose.foundation.rememberScrollState 34 | import androidx.compose.foundation.shape.CircleShape 35 | import androidx.compose.foundation.shape.CutCornerShape 36 | import androidx.compose.foundation.shape.RoundedCornerShape 37 | import androidx.compose.foundation.verticalScroll 38 | import androidx.compose.material3.MaterialTheme 39 | import androidx.compose.material3.Text 40 | import androidx.compose.runtime.Composable 41 | import androidx.compose.runtime.LaunchedEffect 42 | import androidx.compose.runtime.derivedStateOf 43 | import androidx.compose.runtime.getValue 44 | import androidx.compose.runtime.mutableFloatStateOf 45 | import androidx.compose.runtime.mutableStateOf 46 | import androidx.compose.runtime.remember 47 | import androidx.compose.runtime.rememberCoroutineScope 48 | import androidx.compose.runtime.setValue 49 | import androidx.compose.ui.Alignment 50 | import androidx.compose.ui.Modifier 51 | import androidx.compose.ui.draw.alpha 52 | import androidx.compose.ui.draw.shadow 53 | import androidx.compose.ui.geometry.center 54 | import androidx.compose.ui.graphics.Color 55 | import androidx.compose.ui.graphics.ColorFilter 56 | import androidx.compose.ui.graphics.Outline 57 | import androidx.compose.ui.graphics.Paint 58 | import androidx.compose.ui.graphics.PaintingStyle 59 | import androidx.compose.ui.graphics.Path 60 | import androidx.compose.ui.graphics.PathEffect 61 | import androidx.compose.ui.graphics.RectangleShape 62 | import androidx.compose.ui.graphics.TransformOrigin 63 | import androidx.compose.ui.graphics.drawOutline 64 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 65 | import androidx.compose.ui.graphics.graphicsLayer 66 | import androidx.compose.ui.layout.ContentScale 67 | import androidx.compose.ui.platform.LocalDensity 68 | import androidx.compose.ui.res.painterResource 69 | import androidx.compose.ui.text.font.FontWeight 70 | import androidx.compose.ui.unit.TextUnit 71 | import androidx.compose.ui.unit.TextUnitType 72 | import androidx.compose.ui.unit.dp 73 | import androidx.compose.ui.unit.lerp 74 | import com.gigamole.composescrollbars.Scrollbars 75 | import com.gigamole.composescrollbars.config.ScrollbarsConfig 76 | import com.gigamole.composescrollbars.config.ScrollbarsGravity 77 | import com.gigamole.composescrollbars.config.ScrollbarsOrientation 78 | import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType 79 | import com.gigamole.composescrollbars.config.layercontenttype.layercontentstyletype.ScrollbarsLayerContentStyleType 80 | import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType 81 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerConfig 82 | import com.gigamole.composescrollbars.config.layersType.layerConfig.ScrollbarsLayerGravity 83 | import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType 84 | import com.gigamole.composescrollbars.rememberScrollbarsState 85 | import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType 86 | import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsStaticKnobType 87 | import kotlinx.coroutines.delay 88 | import kotlinx.coroutines.launch 89 | import java.util.UUID 90 | import kotlin.math.roundToInt 91 | 92 | private val sampleTitleColor = Color(0xFF007AC9) 93 | private val sampleTitleBgColor = sampleTitleColor.copy(alpha = 0.05F) 94 | private val sampleDescriptionColor = Color(0xFFE21776) 95 | private val sampleDescriptionBgColor = sampleDescriptionColor.copy(alpha = 0.05F) 96 | private val sampleStarColor = Color.White 97 | private val sampleStarBgColor = Color(0xFFFFC107) 98 | 99 | @Suppress("unused") 100 | @Composable 101 | fun MainScreenDemoContent() { 102 | val verticalScrollState = rememberScrollState() 103 | val horizontalScrollState = rememberScrollState() 104 | val coroutineScope = rememberCoroutineScope() 105 | 106 | val verticalScrollAmount = with(LocalDensity.current) { 30.dp.toPx() }.roundToInt() 107 | val horizontalScrollAmount = with(LocalDensity.current) { 128.dp.toPx() }.roundToInt() 108 | var triggerId by remember { mutableStateOf(null) } 109 | 110 | LaunchedEffect(triggerId) { 111 | if (triggerId == null) { 112 | return@LaunchedEffect 113 | } 114 | 115 | coroutineScope.launch { 116 | coroutineScope.launch { 117 | verticalScrollState.animateScrollTo( 118 | value = verticalScrollAmount, 119 | animationSpec = tween(durationMillis = 1000) 120 | ) 121 | } 122 | coroutineScope.launch { 123 | delay(600) 124 | horizontalScrollState.animateScrollTo( 125 | value = horizontalScrollAmount, 126 | animationSpec = tween(durationMillis = 1300) 127 | ) 128 | } 129 | coroutineScope.launch { 130 | delay(1300) 131 | verticalScrollState.animateScrollTo( 132 | value = -verticalScrollAmount, 133 | animationSpec = tween(durationMillis = 1000) 134 | ) 135 | } 136 | coroutineScope.launch { 137 | delay(2500) 138 | horizontalScrollState.animateScrollTo( 139 | value = -horizontalScrollAmount, 140 | animationSpec = tween(durationMillis = 600) 141 | ) 142 | } 143 | } 144 | } 145 | 146 | Box( 147 | modifier = Modifier 148 | .fillMaxSize() 149 | .background(color = Color.White) 150 | .clickable( 151 | interactionSource = remember { MutableInteractionSource() }, 152 | indication = null, 153 | onClick = { 154 | triggerId = UUID 155 | .randomUUID() 156 | .toString() 157 | } 158 | ), 159 | contentAlignment = Alignment.Center 160 | ) { 161 | Column( 162 | verticalArrangement = Arrangement.spacedBy( 163 | space = 16.dp, 164 | alignment = Alignment.CenterVertically, 165 | ), 166 | horizontalAlignment = Alignment.CenterHorizontally 167 | ) { 168 | Row( 169 | modifier = Modifier.height(height = 160.dp), 170 | horizontalArrangement = Arrangement.spacedBy(space = 32.dp) 171 | ) { 172 | Scrollbars( 173 | modifier = Modifier.width(width = 16.dp), 174 | state = rememberScrollbarsState( 175 | config = ScrollbarsConfig( 176 | orientation = ScrollbarsOrientation.Vertical, 177 | paddingValues = PaddingValues(), 178 | layersType = ScrollbarsLayersType.Wrap(paddingValues = PaddingValues(all = 2.dp)), 179 | backgroundLayerContentType = ScrollbarsLayerContentType.Custom { 180 | // Set shadowed background. 181 | Box( 182 | modifier = Modifier 183 | .fillMaxSize() 184 | .shadow( 185 | elevation = 4.dp, 186 | shape = RectangleShape, 187 | clip = true, 188 | spotColor = Color.DarkGray.copy(alpha = 0.5F), 189 | ambientColor = Color.DarkGray.copy(alpha = 0.5F) 190 | ) 191 | .background( 192 | color = Color.White, 193 | shape = RectangleShape 194 | ) 195 | ) 196 | }, 197 | knobLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle( 198 | shape = RectangleShape, 199 | idleColor = Color.LightGray.copy(alpha = 0.25F) 200 | ) 201 | ), 202 | scrollType = ScrollbarsScrollType.Scroll( 203 | state = verticalScrollState, 204 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.3F) 205 | ) 206 | ) 207 | ) 208 | 209 | Scrollbars( 210 | modifier = Modifier.width(width = 16.dp), 211 | state = rememberScrollbarsState( 212 | config = ScrollbarsConfig( 213 | orientation = ScrollbarsOrientation.Vertical, 214 | paddingValues = PaddingValues(), 215 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle(idleColor = Color.LightGray.copy(alpha = 0.2F)), 216 | knobLayerContentType = ScrollbarsLayerContentType.Custom { 217 | // Set shadowed knob. 218 | Box( 219 | modifier = Modifier 220 | .fillMaxSize() 221 | .shadow( 222 | elevation = 4.dp, 223 | shape = CircleShape, 224 | clip = true 225 | ) 226 | .background( 227 | color = Color.White, 228 | shape = CircleShape 229 | ) 230 | ) 231 | } 232 | ), 233 | scrollType = ScrollbarsScrollType.Scroll( 234 | state = verticalScrollState, 235 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.4F) 236 | ) 237 | ) 238 | ) 239 | 240 | Scrollbars( 241 | modifier = Modifier.width(width = 16.dp), 242 | state = rememberScrollbarsState( 243 | config = ScrollbarsConfig( 244 | orientation = ScrollbarsOrientation.Vertical, 245 | paddingValues = PaddingValues(), 246 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle(idleColor = Color.LightGray.copy(alpha = 0.25F)), 247 | knobLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive( 248 | idleColor = Color.LightGray.copy(alpha = 0.4F), 249 | activeColor = Color.LightGray, 250 | outAnimationSpec = tween( 251 | durationMillis = 200, 252 | delayMillis = 300 253 | ) 254 | ) 255 | ), 256 | scrollType = ScrollbarsScrollType.Scroll( 257 | state = verticalScrollState, 258 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.5F) 259 | ) 260 | ) 261 | ) 262 | 263 | Scrollbars( 264 | modifier = Modifier.width(width = 16.dp), 265 | state = rememberScrollbarsState( 266 | config = ScrollbarsConfig( 267 | orientation = ScrollbarsOrientation.Vertical, 268 | paddingValues = PaddingValues(), 269 | layersType = ScrollbarsLayersType.Wrap( 270 | paddingValues = PaddingValues( 271 | horizontal = 4.dp, 272 | vertical = 6.dp 273 | ) 274 | ), 275 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive( 276 | idleColor = Color.LightGray.copy(alpha = 0.4F), 277 | activeColor = Color.LightGray.copy(alpha = 0.8F), 278 | shape = CutCornerShape(percent = 100), 279 | styleType = ScrollbarsLayerContentStyleType.Border(width = 2.dp), 280 | outAnimationSpec = tween( 281 | durationMillis = 200, 282 | delayMillis = 300 283 | ) 284 | ), 285 | knobLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive( 286 | idleColor = Color.LightGray.copy(alpha = 0.4F), 287 | activeColor = Color.LightGray.copy(alpha = 0.8F), 288 | shape = CutCornerShape(percent = 100), 289 | outAnimationSpec = tween( 290 | durationMillis = 200, 291 | delayMillis = 300 292 | ) 293 | ) 294 | ), 295 | scrollType = ScrollbarsScrollType.Scroll( 296 | state = verticalScrollState, 297 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.6F) 298 | ) 299 | ) 300 | ) 301 | 302 | Box( 303 | modifier = Modifier 304 | .height(height = 160.dp) 305 | .width(width = 250.dp) 306 | .background(color = sampleTitleBgColor) 307 | .verticalScroll(state = verticalScrollState) 308 | ) { 309 | Text( 310 | modifier = Modifier 311 | .fillMaxWidth() 312 | .padding( 313 | horizontal = 32.dp, 314 | vertical = 48.dp 315 | ), 316 | text = "Compose\nScrollbars", 317 | color = sampleTitleColor, 318 | style = MaterialTheme.typography.displaySmall, 319 | fontWeight = FontWeight.Bold, 320 | fontFamily = FontFamilySpaceGrotesk 321 | ) 322 | } 323 | 324 | Scrollbars( 325 | modifier = Modifier.width(width = 16.dp), 326 | state = rememberScrollbarsState( 327 | config = ScrollbarsConfig( 328 | orientation = ScrollbarsOrientation.Vertical, 329 | gravity = ScrollbarsGravity.Start, 330 | paddingValues = PaddingValues(), 331 | layersType = ScrollbarsLayersType.Split( 332 | backgroundLayerConfig = ScrollbarsLayerConfig( 333 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 2.dp), 334 | layerGravity = ScrollbarsLayerGravity.Start 335 | ), 336 | knobLayerConfig = ScrollbarsLayerConfig( 337 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 6.dp), 338 | layerGravity = ScrollbarsLayerGravity.Start, 339 | paddingValues = PaddingValues(start = 2.dp) 340 | ) 341 | ), 342 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive( 343 | idleColor = Color.LightGray.copy(alpha = 0.4F), 344 | activeColor = Color.LightGray.copy(alpha = 0.8F), 345 | shape = RectangleShape, 346 | outAnimationSpec = tween( 347 | durationMillis = 200, 348 | delayMillis = 300 349 | ) 350 | ), 351 | knobLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive( 352 | idleColor = Color.LightGray.copy(alpha = 0.4F), 353 | activeColor = Color.LightGray.copy(alpha = 0.8F), 354 | shape = RectangleShape, 355 | outAnimationSpec = tween( 356 | durationMillis = 200, 357 | delayMillis = 300 358 | ) 359 | ) 360 | ), 361 | scrollType = ScrollbarsScrollType.Scroll( 362 | state = verticalScrollState, 363 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.6F) 364 | ) 365 | ) 366 | ) 367 | 368 | Scrollbars( 369 | modifier = Modifier.width(width = 16.dp), 370 | state = rememberScrollbarsState( 371 | config = ScrollbarsConfig( 372 | orientation = ScrollbarsOrientation.Vertical, 373 | gravity = ScrollbarsGravity.Start, 374 | paddingValues = PaddingValues(), 375 | layersType = ScrollbarsLayersType.Split( 376 | backgroundLayerConfig = ScrollbarsLayerConfig( 377 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 2.dp), 378 | layerGravity = ScrollbarsLayerGravity.Center 379 | ), 380 | knobLayerConfig = ScrollbarsLayerConfig( 381 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 10.dp), 382 | layerGravity = ScrollbarsLayerGravity.Center 383 | ) 384 | ), 385 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.IdleActive( 386 | idleColor = Color.LightGray.copy(alpha = 0.3F), 387 | activeColor = Color.LightGray.copy(alpha = 0.6F), 388 | shape = RectangleShape, 389 | outAnimationSpec = tween( 390 | durationMillis = 200, 391 | delayMillis = 300 392 | ) 393 | ), 394 | knobLayerContentType = ScrollbarsLayerContentType.Custom { state -> 395 | // Take the idle active color from the background to match it on knob. 396 | val idleActiveColor = (state.config.backgroundLayerContentType as? 397 | ScrollbarsLayerContentType.Default.Colored.IdleActive)?.idleActiveColor ?: Color.LightGray.copy(alpha = 0.3F) 398 | 399 | Box( 400 | modifier = Modifier 401 | .fillMaxSize() 402 | .background( 403 | color = Color.White, 404 | shape = RectangleShape 405 | ) 406 | .border( 407 | color = idleActiveColor, 408 | width = 2.dp 409 | ) 410 | ) 411 | } 412 | ), 413 | scrollType = ScrollbarsScrollType.Scroll( 414 | state = verticalScrollState, 415 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.5F) 416 | ) 417 | ) 418 | ) 419 | 420 | Scrollbars( 421 | modifier = Modifier 422 | .width(width = 16.dp) 423 | .alpha(alpha = 0.3F), 424 | state = rememberScrollbarsState( 425 | config = ScrollbarsConfig( 426 | orientation = ScrollbarsOrientation.Vertical, 427 | paddingValues = PaddingValues(), 428 | layersType = ScrollbarsLayersType.Split( 429 | backgroundLayerConfig = ScrollbarsLayerConfig( 430 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 4.dp), 431 | layerGravity = ScrollbarsLayerGravity.Center 432 | ), 433 | knobLayerConfig = ScrollbarsLayerConfig( 434 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 16.dp), 435 | layerGravity = ScrollbarsLayerGravity.Center 436 | ) 437 | ), 438 | backgroundLayerContentType = ScrollbarsLayerContentType.Custom { 439 | // Custom line with bulbs background. 440 | Box( 441 | modifier = Modifier 442 | .fillMaxHeight() 443 | .align(alignment = Alignment.Center) 444 | ) { 445 | Box( 446 | modifier = Modifier 447 | .fillMaxHeight() 448 | .width(width = 2.dp) 449 | .padding(vertical = 8.dp) 450 | .align(alignment = Alignment.Center) 451 | .background(color = Color.LightGray) 452 | ) 453 | Box( 454 | modifier = Modifier 455 | .padding(top = 6.dp) 456 | .size(size = 4.dp) 457 | .background( 458 | color = Color.LightGray, 459 | shape = CircleShape 460 | ) 461 | ) 462 | Box( 463 | modifier = Modifier 464 | .padding(bottom = 6.dp) 465 | .size(size = 4.dp) 466 | .align(alignment = Alignment.BottomCenter) 467 | .background( 468 | color = Color.LightGray, 469 | shape = CircleShape 470 | ) 471 | ) 472 | } 473 | }, 474 | knobLayerContentType = ScrollbarsLayerContentType.Custom { 475 | // Custom dot with increasing size custom knob. 476 | val knobFraction = 1.0F - if (it.scrollFraction <= 0.5F) { 477 | 1.0F - (it.scrollFraction * 2.0F) 478 | } else { 479 | (it.scrollFraction - 0.5F) * 2.0F 480 | } 481 | val knobSize = lerp( 482 | start = 6.dp, 483 | stop = 16.dp, 484 | fraction = knobFraction 485 | ) 486 | val knobColor = androidx.compose.ui.graphics.lerp( 487 | start = Color.LightGray, 488 | stop = Color.DarkGray, 489 | fraction = knobFraction 490 | ) 491 | 492 | Box( 493 | modifier = Modifier 494 | .fillMaxSize() 495 | .align(alignment = Alignment.Center), 496 | contentAlignment = Alignment.Center 497 | ) { 498 | Box( 499 | modifier = Modifier 500 | .size(size = knobSize) 501 | .background( 502 | color = knobColor, 503 | shape = CircleShape 504 | ) 505 | ) 506 | } 507 | } 508 | ), 509 | scrollType = ScrollbarsScrollType.Scroll( 510 | state = verticalScrollState, 511 | knobType = ScrollbarsStaticKnobType.Exact(size = 16.dp) 512 | ) 513 | ) 514 | ) 515 | 516 | Scrollbars( 517 | modifier = Modifier.width(width = 16.dp), 518 | state = rememberScrollbarsState( 519 | config = ScrollbarsConfig( 520 | orientation = ScrollbarsOrientation.Vertical, 521 | paddingValues = PaddingValues(), 522 | layersType = ScrollbarsLayersType.Split( 523 | backgroundLayerConfig = ScrollbarsLayerConfig( 524 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 2.dp), 525 | layerGravity = ScrollbarsLayerGravity.Center, 526 | paddingValues = PaddingValues(vertical = 6.dp) 527 | ), 528 | knobLayerConfig = ScrollbarsLayerConfig( 529 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 12.dp), 530 | layerGravity = ScrollbarsLayerGravity.Center 531 | ) 532 | ), 533 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle( 534 | idleColor = Color.LightGray.copy(alpha = 0.2F), 535 | shape = CircleShape 536 | ), 537 | knobLayerContentType = ScrollbarsLayerContentType.Custom { 538 | // Custom circle shadow knob. 539 | Box( 540 | modifier = Modifier 541 | .fillMaxSize() 542 | .shadow( 543 | elevation = 4.dp, 544 | shape = CircleShape, 545 | clip = true 546 | ) 547 | .background( 548 | color = Color.White, 549 | shape = CircleShape 550 | ) 551 | ) 552 | } 553 | ), 554 | scrollType = ScrollbarsScrollType.Scroll( 555 | state = verticalScrollState, 556 | knobType = ScrollbarsStaticKnobType.Exact(size = 12.dp) 557 | ) 558 | ) 559 | ) 560 | } 561 | 562 | Row( 563 | modifier = Modifier.fillMaxWidth(), 564 | horizontalArrangement = Arrangement.spacedBy( 565 | space = 32.dp, 566 | alignment = Alignment.CenterHorizontally 567 | ), 568 | verticalAlignment = Alignment.CenterVertically 569 | ) { 570 | Column( 571 | modifier = Modifier.width(width = 160.dp), 572 | verticalArrangement = Arrangement.spacedBy(space = 16.dp), 573 | horizontalAlignment = Alignment.CenterHorizontally 574 | ) { 575 | Scrollbars( 576 | modifier = Modifier.height(height = 10.dp), 577 | state = rememberScrollbarsState( 578 | config = ScrollbarsConfig( 579 | orientation = ScrollbarsOrientation.Horizontal, 580 | gravity = ScrollbarsGravity.Start, 581 | paddingValues = PaddingValues(), 582 | layersType = ScrollbarsLayersType.Split( 583 | backgroundLayerConfig = ScrollbarsLayerConfig( 584 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 5.dp), 585 | layerGravity = ScrollbarsLayerGravity.Center 586 | ), 587 | knobLayerConfig = ScrollbarsLayerConfig( 588 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 10.dp), 589 | layerGravity = ScrollbarsLayerGravity.Center 590 | ) 591 | ), 592 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle( 593 | idleColor = Color.LightGray.copy(alpha = 0.45F), 594 | shape = CircleShape 595 | ), 596 | knobLayerContentType = ScrollbarsLayerContentType.Custom { 597 | // Custom circle long knob. 598 | Box( 599 | modifier = Modifier 600 | .fillMaxSize() 601 | .background( 602 | color = Color.White, 603 | shape = CircleShape 604 | ) 605 | .border( 606 | color = Color.LightGray.copy(alpha = 0.45F), 607 | width = 2.dp, 608 | shape = CircleShape 609 | ) 610 | ) 611 | } 612 | ), 613 | scrollType = ScrollbarsScrollType.Scroll( 614 | state = horizontalScrollState, 615 | knobType = ScrollbarsStaticKnobType.Fraction(fraction = 0.5F) 616 | ) 617 | ) 618 | ) 619 | 620 | Scrollbars( 621 | modifier = Modifier.height(height = 16.dp), 622 | state = rememberScrollbarsState( 623 | config = ScrollbarsConfig( 624 | orientation = ScrollbarsOrientation.Horizontal, 625 | gravity = ScrollbarsGravity.End, 626 | paddingValues = PaddingValues(), 627 | layersType = ScrollbarsLayersType.Split( 628 | backgroundLayerConfig = ScrollbarsLayerConfig( 629 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 2.dp), 630 | layerGravity = ScrollbarsLayerGravity.End 631 | ), 632 | knobLayerConfig = ScrollbarsLayerConfig( 633 | thicknessType = ScrollbarsThicknessType.Exact(thickness = 10.dp), 634 | layerGravity = ScrollbarsLayerGravity.End, 635 | paddingValues = PaddingValues(bottom = 6.dp) 636 | ) 637 | ), 638 | backgroundLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle( 639 | idleColor = Color.LightGray.copy(alpha = 0.55F), 640 | shape = RectangleShape 641 | ), 642 | knobLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle( 643 | idleColor = Color.LightGray.copy(alpha = 0.55F), 644 | shape = RectangleShape 645 | ), 646 | ), 647 | scrollType = ScrollbarsScrollType.Scroll( 648 | state = horizontalScrollState, 649 | knobType = ScrollbarsStaticKnobType.Exact(size = 10.dp) 650 | ) 651 | ) 652 | ) 653 | } 654 | 655 | Box( 656 | modifier = Modifier 657 | .width(width = 250.dp) 658 | .height(height = 48.dp) 659 | .background(color = sampleDescriptionBgColor) 660 | .horizontalScroll(state = horizontalScrollState), 661 | contentAlignment = Alignment.CenterStart 662 | ) { 663 | Text( 664 | modifier = Modifier.padding(horizontal = 16.dp), 665 | text = "Polish Android Compose UI with advanced scrollbars", 666 | color = sampleDescriptionColor, 667 | style = MaterialTheme.typography.labelLarge, 668 | fontWeight = FontWeight.Normal, 669 | fontFamily = FontFamilyOpenSans 670 | ) 671 | } 672 | 673 | Column( 674 | modifier = Modifier 675 | .width(width = 160.dp) 676 | .height(height = 48.dp), 677 | verticalArrangement = Arrangement.Bottom, 678 | horizontalAlignment = Alignment.CenterHorizontally 679 | ) { 680 | Scrollbars( 681 | state = rememberScrollbarsState( 682 | config = ScrollbarsConfig( 683 | orientation = ScrollbarsOrientation.Horizontal, 684 | gravity = ScrollbarsGravity.End, 685 | paddingValues = PaddingValues(), 686 | layersType = ScrollbarsLayersType.Split( 687 | backgroundLayerConfig = ScrollbarsLayerConfig( 688 | thicknessType = ScrollbarsThicknessType.Wrap, 689 | layerGravity = ScrollbarsLayerGravity.End, 690 | paddingValues = PaddingValues(horizontal = 12.dp) 691 | ), 692 | knobLayerConfig = ScrollbarsLayerConfig( 693 | thicknessType = ScrollbarsThicknessType.Wrap, 694 | layerGravity = ScrollbarsLayerGravity.End 695 | ), 696 | ), 697 | backgroundLayerContentType = ScrollbarsLayerContentType.Custom { state -> 698 | // Custom increasing triangle background. 699 | val bgColor = androidx.compose.ui.graphics.lerp( 700 | start = Color.LightGray.copy(alpha = 0.25F), 701 | stop = Color.LightGray.copy(alpha = 0.55F), 702 | fraction = state.scrollFraction 703 | ) 704 | 705 | Canvas( 706 | modifier = Modifier 707 | .fillMaxWidth() 708 | .height(height = 6.dp) 709 | ) { 710 | drawPath( 711 | path = Path().apply { 712 | moveTo( 713 | x = 0.0F, 714 | y = size.height 715 | ) 716 | lineTo( 717 | x = size.width, 718 | y = 0.0F 719 | ) 720 | lineTo( 721 | x = size.width, 722 | y = size.height 723 | ) 724 | close() 725 | }, 726 | color = bgColor 727 | ) 728 | } 729 | }, 730 | knobLayerContentType = ScrollbarsLayerContentType.Custom { state -> 731 | // Custom sliding popup with a 100% special star knob. 732 | val maxReached by remember(state.scrollFraction) { 733 | derivedStateOf { 734 | state.scrollFraction >= 0.99F 735 | } 736 | } 737 | var prevScrollFraction by remember { mutableFloatStateOf(state.scrollFraction) } 738 | val isForward = when { 739 | prevScrollFraction < state.scrollFraction -> { 740 | true 741 | } 742 | prevScrollFraction > state.scrollFraction -> { 743 | false 744 | } 745 | else -> { 746 | null 747 | } 748 | } 749 | prevScrollFraction = state.scrollFraction 750 | 751 | val progressKnobColor = androidx.compose.ui.graphics.lerp( 752 | start = Color.LightGray.copy(alpha = 0.25F), 753 | stop = Color.LightGray.copy(alpha = 0.55F), 754 | fraction = state.scrollFraction 755 | ) 756 | val knobColor by animateColorAsState( 757 | targetValue = if (maxReached) { 758 | sampleStarBgColor 759 | } else { 760 | progressKnobColor 761 | }, 762 | label = "KnobColor" 763 | ) 764 | val progressTextColor = androidx.compose.ui.graphics.lerp( 765 | start = Color.DarkGray.copy(alpha = 0.5F), 766 | stop = Color.DarkGray.copy(alpha = 0.8F), 767 | fraction = state.scrollFraction 768 | ) 769 | val textColor by animateColorAsState( 770 | targetValue = if (maxReached) { 771 | sampleStarColor 772 | } else { 773 | progressTextColor 774 | }, 775 | label = "TextColor" 776 | ) 777 | val progressKnobScale = androidx.compose.ui.util.lerp( 778 | start = 0.85F, 779 | stop = 1.25F, 780 | fraction = state.scrollFraction 781 | ) 782 | val knobScale by animateFloatAsState( 783 | targetValue = if (maxReached) { 784 | 1.85F 785 | } else { 786 | progressKnobScale 787 | }, 788 | animationSpec = spring( 789 | dampingRatio = Spring.DampingRatioLowBouncy, 790 | stiffness = 7_000.0F 791 | ), 792 | label = "KnobScale" 793 | ) 794 | val knobOffset = lerp( 795 | start = 0.dp, 796 | stop = (-6).dp, 797 | fraction = state.scrollFraction 798 | ) 799 | val knobOffsetPx = with(LocalDensity.current) { knobOffset.toPx() } 800 | val knobRotation by animateFloatAsState( 801 | targetValue = if (maxReached) { 802 | 0.0F 803 | } else { 804 | if (state.scrollType.isScrollInProgress && isForward != null) { 805 | if (isForward) { 806 | -12.0F 807 | } else { 808 | 12.0F 809 | } 810 | } else { 811 | 0.0F 812 | } 813 | }, 814 | animationSpec = if (maxReached) { 815 | spring( 816 | dampingRatio = 0.18F, 817 | stiffness = Spring.StiffnessMedium 818 | ) 819 | } else { 820 | spring( 821 | dampingRatio = 0.35F, 822 | stiffness = Spring.StiffnessMediumLow 823 | ) 824 | }, 825 | label = "KnobRotation" 826 | ) 827 | val knobText = (state.scrollFraction * 100.0F).roundToInt().toString() 828 | 829 | Column( 830 | modifier = Modifier 831 | .fillMaxWidth() 832 | .padding(bottom = 4.dp) 833 | .graphicsLayer( 834 | scaleX = knobScale, 835 | scaleY = knobScale, 836 | translationY = knobOffsetPx, 837 | rotationZ = knobRotation, 838 | transformOrigin = TransformOrigin( 839 | pivotFractionX = 0.5F, 840 | pivotFractionY = 1.0F 841 | ) 842 | ), 843 | verticalArrangement = Arrangement.Center, 844 | horizontalAlignment = Alignment.CenterHorizontally 845 | ) { 846 | Box( 847 | modifier = Modifier 848 | .fillMaxWidth() 849 | .height(height = 22.dp) 850 | .background( 851 | color = knobColor, 852 | shape = RoundedCornerShape(size = 6.dp) 853 | ), 854 | contentAlignment = Alignment.Center 855 | ) { 856 | AnimatedContent( 857 | modifier = Modifier.fillMaxSize(), 858 | contentAlignment = Alignment.Center, 859 | targetState = maxReached, 860 | transitionSpec = { 861 | (fadeIn(animationSpec = spring()) + 862 | scaleIn(animationSpec = spring())).togetherWith( 863 | fadeOut(animationSpec = spring()) + 864 | scaleOut(animationSpec = spring()) 865 | ) 866 | }, 867 | label = "KnobTextIconContent" 868 | ) { maxReachedState -> 869 | Box( 870 | modifier = Modifier.fillMaxSize(), 871 | contentAlignment = Alignment.Center 872 | ) { 873 | if (maxReachedState) { 874 | Image( 875 | modifier = Modifier.size(size = 14.dp), 876 | painter = painterResource(id = R.drawable.ic_star), 877 | contentScale = ContentScale.FillBounds, 878 | colorFilter = ColorFilter.tint(color = sampleStarColor), 879 | contentDescription = "" 880 | ) 881 | } else { 882 | Text( 883 | text = knobText, 884 | color = textColor, 885 | fontWeight = FontWeight.Bold, 886 | fontSize = TextUnit( 887 | value = 10.0F, 888 | type = TextUnitType.Sp 889 | ) 890 | ) 891 | } 892 | } 893 | } 894 | } 895 | Canvas( 896 | modifier = Modifier 897 | .height(height = 6.dp) 898 | .width(width = 12.dp) 899 | ) { 900 | val path = Path().apply { 901 | moveTo( 902 | x = 0.0F, 903 | y = 0.0F 904 | ) 905 | lineTo( 906 | x = size.width * 0.25F, 907 | y = 0.0F 908 | ) 909 | lineTo( 910 | x = size.center.x, 911 | y = size.height 912 | ) 913 | lineTo( 914 | x = size.width * 0.75F, 915 | y = 0.0F 916 | ) 917 | lineTo( 918 | x = size.width, 919 | y = 0.0F 920 | ) 921 | lineTo( 922 | x = size.width, 923 | y = 0.0F 924 | ) 925 | lineTo( 926 | x = 0.0F, 927 | y = 0.0F 928 | ) 929 | close() 930 | } 931 | 932 | drawIntoCanvas { canvas -> 933 | canvas.drawOutline( 934 | outline = Outline.Generic(path = path), 935 | paint = Paint().apply { 936 | color = knobColor 937 | style = PaintingStyle.Fill 938 | 939 | pathEffect = PathEffect.cornerPathEffect(radius = 8.dp.toPx()) 940 | } 941 | ) 942 | } 943 | } 944 | } 945 | } 946 | ), 947 | scrollType = ScrollbarsScrollType.Scroll( 948 | state = horizontalScrollState, 949 | knobType = ScrollbarsStaticKnobType.Exact(size = 24.dp) 950 | ) 951 | ) 952 | ) 953 | } 954 | } 955 | } 956 | } 957 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/gigamole/composescrollbars/sample/MainTheme.kt: -------------------------------------------------------------------------------- 1 | package com.gigamole.composescrollbars.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 | 27 | @Composable 28 | fun MainTheme(content: @Composable () -> Unit) { 29 | MaterialTheme( 30 | colorScheme = lightColorScheme( 31 | primary = Color.Black, 32 | primaryContainer = Color.White, 33 | onPrimary = Color.White, 34 | secondary = Color.Black, 35 | secondaryContainer = Color.White, 36 | onSecondary = Color.White, 37 | tertiary = Color.Black, 38 | tertiaryContainer = Color.White, 39 | onTertiary = Color.White, 40 | surface = Color.White, 41 | onSurface = Color.Black, 42 | surfaceVariant = Color.LightGray, 43 | onSurfaceVariant = Color.DarkGray, 44 | outline = Color.DarkGray, 45 | background = Color.White, 46 | onBackground = Color.Black 47 | ), 48 | content = content 49 | ) 50 | } -------------------------------------------------------------------------------- /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/ic_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/font/opensans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/space_grotesk_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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 | ComposeScrollbars 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.2.0" 5 | androidx_ktx_version = "1.12.0" 6 | kotlin_version = "1.9.20" 7 | 8 | # Plugins 9 | ksp_version = "1.9.20-1.0.14" 10 | 11 | # Compose 12 | compose_compiler_version = "1.5.4" 13 | compose_bom_version = "2023.10.01" 14 | compose_activity_version = "1.8.1" 15 | compose_color_picker_version = "0.7.0" 16 | 17 | [plugins] 18 | 19 | ksp_gradle_plugin = { id = "com.google.devtools.ksp", version.ref = "ksp_version" } 20 | 21 | [libraries] 22 | 23 | # Android/Kotlin 24 | android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin_version" } 25 | androidx_core = { module = "androidx.core:core", version.ref = "androidx_ktx_version" } 26 | androidx_ktx = { module = "androidx.core:core-ktx", version.ref = "androidx_ktx_version" } 27 | kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" } 28 | 29 | # Compose 30 | compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom_version" } 31 | compose_material3 = { module = "androidx.compose.material3:material3" } 32 | compose_preview = { module = "androidx.compose.ui:ui-tooling-preview" } 33 | compose_runtime = { module = "androidx.compose.runtime:runtime" } 34 | compose_ui_util = { module = "androidx.compose.ui:ui-util" } 35 | compose_activity = { module = "androidx.activity:activity-compose", version.ref = "compose_activity_version" } 36 | compose_color_picker = { module = "com.godaddy.android.colorpicker:compose-color-picker", version.ref = "compose_color_picker_version" } 37 | debug_compose_tooling = { module = "androidx.compose.ui:ui-tooling" } 38 | 39 | [bundles] 40 | 41 | compose = [ 42 | "compose.material3", 43 | "compose.preview", 44 | "compose.runtime", 45 | "compose.activity", 46 | "compose.ui.util", 47 | ] 48 | debug_compose = [ 49 | "debug.compose.tooling" 50 | ] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/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/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/credits.png -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/demo.gif -------------------------------------------------------------------------------- /media/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/footer.png -------------------------------------------------------------------------------- /media/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/header.png -------------------------------------------------------------------------------- /media/sample-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/sample-1.gif -------------------------------------------------------------------------------- /media/sample-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/sample-2.gif -------------------------------------------------------------------------------- /media/sample-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/sample-3.gif -------------------------------------------------------------------------------- /media/sample-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/sample-4.gif -------------------------------------------------------------------------------- /media/sample-5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/sample-5.gif -------------------------------------------------------------------------------- /media/sample-6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIGAMOLE/ComposeScrollbars/2d72e0bb7d674c6316c2e1a709dda1242dbeefc0/media/sample-6.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.4" 9 | 10 | const val namespace = "com.gigamole.composescrollbars" 11 | const val group = "com.github.GIGAMOLE" 12 | const val artifact = "ComposeScrollbars" 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/composescrollbars.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/composescrollbars.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(":ComposeScrollbars") 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 = "ComposeScrollbarsProject" 18 | --------------------------------------------------------------------------------