Repository: Crdzbird/floaty_chathead Branch: master Commit: 28dfa9c02145 Files: 96 Total size: 200.6 KB Directory structure: gitextract_z7wqmmkf/ ├── .gitignore ├── .idea/ │ ├── codeStyles/ │ │ └── Project.xml │ ├── kotlinc.xml │ ├── libraries/ │ │ ├── Dart_SDK.xml │ │ ├── Flutter_Plugins.xml │ │ └── KotlinJavaRuntime.xml │ ├── modules.xml │ ├── runConfigurations/ │ │ └── example_lib_main_dart.xml │ └── workspace.xml ├── .metadata ├── .vscode/ │ └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── android/ │ ├── .gitignore │ ├── .idea/ │ │ ├── .name │ │ ├── assetWizardSettings.xml │ │ ├── caches/ │ │ │ └── build_file_checksums.ser │ │ ├── codeStyles/ │ │ │ └── Project.xml │ │ ├── gradle.xml │ │ ├── jarRepositories.xml │ │ ├── misc.xml │ │ ├── modules.xml │ │ ├── runConfigurations.xml │ │ └── vcs.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── ni/ │ │ └── devotion/ │ │ └── floaty_head/ │ │ └── FloatFragment.kt │ ├── kotlin/ │ │ └── ni/ │ │ └── devotion/ │ │ └── floaty_head/ │ │ ├── FloatyHeadPlugin.kt │ │ ├── MainActivity.kt │ │ ├── floating_chathead/ │ │ │ ├── ChatHead.kt │ │ │ ├── ChatHeads.kt │ │ │ ├── Close.kt │ │ │ ├── SpringConfig.kt │ │ │ └── WindowManagerHelper.kt │ │ ├── models/ │ │ │ ├── Decoration.kt │ │ │ ├── Margin.kt │ │ │ └── Padding.kt │ │ ├── services/ │ │ │ ├── FloatyContentJobService.kt │ │ │ └── FloatyIconService.kt │ │ ├── utils/ │ │ │ ├── Commons.kt │ │ │ ├── Constants.kt │ │ │ ├── ImageHelper.kt │ │ │ ├── Managment.kt │ │ │ ├── NumberUtils.kt │ │ │ └── UiBuilder.kt │ │ └── views/ │ │ ├── BodyView.kt │ │ ├── FooterView.kt │ │ ├── HeaderView.kt │ │ └── RowView.kt │ └── res/ │ ├── drawable/ │ │ ├── gradient_bg.xml │ │ └── ic_chathead.xml │ ├── layout/ │ │ └── fragment_float.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── example/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── android/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── ni/ │ │ │ │ │ └── devotion/ │ │ │ │ │ └── floaty_head_example/ │ │ │ │ │ ├── Application.kt │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values/ │ │ │ │ └── styles.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── settings.gradle │ │ └── settings_aar.gradle │ ├── lib/ │ │ └── main.dart │ ├── pubspec.yaml │ └── test/ │ └── widget_test.dart ├── floaty_head.iml ├── lib/ │ ├── floaty_head.dart │ ├── models/ │ │ ├── floaty_head_body.dart │ │ ├── floaty_head_button.dart │ │ ├── floaty_head_decoration.dart │ │ ├── floaty_head_footer.dart │ │ ├── floaty_head_header.dart │ │ ├── floaty_head_margin.dart │ │ ├── floaty_head_padding.dart │ │ └── floaty_head_text.dart │ └── utils/ │ └── commons.dart ├── pubspec.yaml └── test/ └── floaty_head_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store .dart_tool/ .packages .pub/ build/ ================================================ FILE: .idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: .idea/kotlinc.xml ================================================ ================================================ FILE: .idea/libraries/Dart_SDK.xml ================================================ ================================================ FILE: .idea/libraries/Flutter_Plugins.xml ================================================ ================================================ FILE: .idea/libraries/KotlinJavaRuntime.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations/example_lib_main_dart.xml ================================================ ================================================ FILE: .idea/workspace.xml ================================================ 1598576921664 ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 216dee60c0cc9449f0b29bcf922974d612263e24 channel: stable project_type: plugin ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Flutter", "program": "lib/main.dart", "request": "launch", "type": "dart" } ] } ================================================ FILE: CHANGELOG.md ================================================ ## [2.0.0-nullsafety.0] - 2021.03.06 * mirgrate to nullsafety ## [1.1.0] - 2020.09.19 * Added documentation. * Refactored all the code. * Added independent functionality chathead works even if the app is killed. ## [1.0.0] - 2020.09.05 * Initial Release ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Floaty_Chathead :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: The following is a set of guidelines for contributing to Floaty_Chathead. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ## How Can I Contribute? ### Reporting Bugs > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. #### How Do I Submit A (Good) Bug Report? Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your bug is related to, create an issue on that repository and provide the following information by filling in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). Explain the problem and include additional details to help maintainers reproduce the problem: * **Use a clear and descriptive title** for the issue to identify the problem. * **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how the plugin was intended to work on your app, e.g. which command exactly you used in the terminal. When listing steps, **don't just say what you did, but explain how you did it**. * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. * **Explain which behavior you expected to see instead and why.** * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. * **If you're reporting that Floaty_Chathead crashed**, include a crash report with a stack trace from the project console. Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. ### Suggesting Enhancements This section guides you through submitting an enhancement suggestion for Floaty_Chathead, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the flutter community understand your suggestion :pencil: and find related suggestions :mag_right:. Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed. #### How Do I Submit A (Good) Enhancement Suggestion? Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your enhancement suggestion is related to, create an issue on that repository and provide the following information: * **Use a clear and descriptive title** for the issue to identify the suggestion. * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). * **Describe the current behavior** and **explain which behavior you expected to see instead** and why. * **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Atom which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. * **Explain why this enhancement would be useful** to most Flutter users. ### Pull Requests The process described here has several goals: - Fix problems that are important to user [search-atom-repo-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aenhancement [search-atom-org-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aenhancement [search-atom-repo-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abug [search-atom-org-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abug [search-atom-repo-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aquestion [search-atom-org-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aquestion [search-atom-repo-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Afeedback [search-atom-org-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Afeedback [search-atom-repo-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ahelp-wanted [search-atom-org-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ahelp-wanted [search-atom-repo-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abeginner [search-atom-org-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abeginner [search-atom-repo-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Amore-information-needed [search-atom-org-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Amore-information-needed [search-atom-repo-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aneeds-reproduction [search-atom-org-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aneeds-reproduction [search-atom-repo-label-triage-help-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Atriage-help-needed [search-atom-org-label-triage-help-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Atriage-help-needed [search-atom-repo-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awindows [search-atom-org-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awindows [search-atom-repo-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Alinux [search-atom-org-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Alinux [search-atom-repo-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Amac [search-atom-org-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Amac [search-atom-repo-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Adocumentation [search-atom-org-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adocumentation [search-atom-repo-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aperformance [search-atom-org-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aperformance [search-atom-repo-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Asecurity [search-atom-org-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Asecurity [search-atom-repo-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aui [search-atom-org-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aui [search-atom-repo-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aapi [search-atom-org-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aapi [search-atom-repo-label-crash]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Acrash [search-atom-org-label-crash]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Acrash [search-atom-repo-label-auto-indent]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aauto-indent [search-atom-org-label-auto-indent]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aauto-indent [search-atom-repo-label-encoding]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aencoding [search-atom-org-label-encoding]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aencoding [search-atom-repo-label-network]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Anetwork [search-atom-org-label-network]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Anetwork [search-atom-repo-label-uncaught-exception]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Auncaught-exception [search-atom-org-label-uncaught-exception]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Auncaught-exception [search-atom-repo-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Agit [search-atom-org-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Agit [search-atom-repo-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ablocked [search-atom-org-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ablocked [search-atom-repo-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aduplicate [search-atom-org-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aduplicate [search-atom-repo-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awontfix [search-atom-org-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awontfix [search-atom-repo-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ainvalid [search-atom-org-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ainvalid [search-atom-repo-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Apackage-idea [search-atom-org-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Apackage-idea [search-atom-repo-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awrong-repo [search-atom-org-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awrong-repo [search-atom-repo-label-editor-rendering]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aeditor-rendering [search-atom-org-label-editor-rendering]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aeditor-rendering [search-atom-repo-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abuild-error [search-atom-org-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abuild-error [search-atom-repo-label-error-from-pathwatcher]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-pathwatcher [search-atom-org-label-error-from-pathwatcher]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-pathwatcher [search-atom-repo-label-error-from-save]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-save [search-atom-org-label-error-from-save]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-save [search-atom-repo-label-error-from-open]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-open [search-atom-org-label-error-from-open]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-open [search-atom-repo-label-installer]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ainstaller [search-atom-org-label-installer]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ainstaller [search-atom-repo-label-auto-updater]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aauto-updater [search-atom-org-label-auto-updater]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aauto-updater [search-atom-repo-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Adeprecation-help [search-atom-org-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adeprecation-help [search-atom-repo-label-electron]: https://github.com/search?q=is%3Aissue+repo%3Aatom%2Fatom+is%3Aopen+label%3Aelectron [search-atom-org-label-electron]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aelectron [search-atom-repo-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Awork-in-progress [search-atom-org-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Awork-in-progress [search-atom-repo-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-review [search-atom-org-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-review [search-atom-repo-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aunder-review [search-atom-org-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aunder-review [search-atom-repo-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Arequires-changes [search-atom-org-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Arequires-changes [search-atom-repo-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-testing [search-atom-org-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-testing [beginner]:https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc [help-wanted]:https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner [contributing-to-official-atom-packages]:https://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/ [hacking-on-atom-core]: https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Luis Cardoza Bird Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Floaty Chathead (Deprecated) [![Deprecated](https://img.shields.io/badge/status-deprecated-red.svg)](https://pub.dev/packages/floaty_chatheads) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) > **This package has been deprecated.** Please use [`floaty_chatheads`](https://pub.dev/packages/floaty_chatheads) instead. --- ## Migration Replace your dependency: ```yaml # Before dependencies: floaty_chathead: ^3.0.0 # After dependencies: floaty_chatheads: ^1.0.0 ``` Then run: ```bash flutter pub get ``` ## What changed? `floaty_chatheads` is a full rewrite of `floaty_chathead` using a **federated plugin architecture**, bringing: - **iOS support** alongside Android - **Type-safe platform channels** via Pigeon (no more manual `MethodChannel` strings) - **Typed messaging** with `FloatyMessenger` for serialized communication between main app and overlay - **Lifecycle-aware controller** (`FloatyController`) with `ChangeNotifier` integration - **Widget-level integration** via `FloatyScope` (InheritedWidget) and `FloatyPermissionGate` - **Multi-chathead management** (`addChatHead`, `removeChatHead`, `expandChatHead`, `collapseChatHead`) - **Built-in UI components** (`FloatyMiniPlayer`, `FloatyNotificationCard`) - **100% test coverage** on handwritten code with 132 tests - **Testing utilities** (`FakeFloatyPlatform`, `FakeOverlayDataSource`) for easy widget testing ## Links - [`floaty_chatheads` on pub.dev](https://pub.dev/packages/floaty_chatheads) - [`floaty_chatheads` repository](https://github.com/Crdzbird/floaty_chatheads) --- ## License This project is licensed under the MIT License. See [LICENSE](LICENSE) for details. ================================================ FILE: android/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ================================================ FILE: android/.idea/.name ================================================ floaty_head ================================================ FILE: android/.idea/assetWizardSettings.xml ================================================ ================================================ FILE: android/.idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: android/.idea/gradle.xml ================================================ ================================================ FILE: android/.idea/jarRepositories.xml ================================================ ================================================ FILE: android/.idea/misc.xml ================================================ ================================================ FILE: android/.idea/modules.xml ================================================ ================================================ FILE: android/.idea/runConfigurations.xml ================================================ ================================================ FILE: android/.idea/vcs.xml ================================================ ================================================ FILE: android/build.gradle ================================================ group 'ni.devotion.floaty_head' version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.4.0' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } rootProject.allprojects { repositories { google() jcenter() } } apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { compileSdkVersion 30 defaultConfig { minSdkVersion 16 targetSdkVersion 30 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { disable 'InvalidPackage' } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'com.google.android.material:material:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.facebook.rebound:rebound:0.3.8' implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ #Thu Aug 27 19:59:47 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true android.enableR8=true ================================================ FILE: android/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: android/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: android/settings.gradle ================================================ rootProject.name = 'floaty_head' ================================================ FILE: android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/src/main/java/ni/devotion/floaty_head/FloatFragment.kt ================================================ package ni.devotion.floaty_head import android.content.Context import android.view.View import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.widget.FrameLayout import android.widget.LinearLayout import com.facebook.rebound.SimpleSpringListener import com.facebook.rebound.Spring import com.facebook.rebound.SpringSystem import ni.devotion.floaty_head.floating_chathead.SpringConfigs import ni.devotion.floaty_head.R import ni.devotion.floaty_head.utils.Managment.bodyView import ni.devotion.floaty_head.utils.Managment.footerView import ni.devotion.floaty_head.utils.Managment.headerView class FloatFragment(context: Context) : LinearLayout(context) { private val springSystem = SpringSystem.create() private val scaleSpring = springSystem.createSpring() private lateinit var content: LinearLayout init { setupView() } private fun setupView() { context.setTheme(R.style.Theme_MaterialComponents_Light) inflate(context, R.layout.fragment_float, this) scaleSpring.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { scaleX = spring.currentValue.toFloat() scaleY = spring.currentValue.toFloat() } }) scaleSpring.springConfig = SpringConfigs.CONTENT_SCALE scaleSpring.currentValue = 0.0 content = findViewById(R.id.contentLayout) headerView?.let { content.addView(it) } bodyView?.let { content.addView(it) } footerView?.let { content.addView(it) } } override fun onViewRemoved(child: View?) { super.onViewRemoved(child) content.removeAllViews() } fun hideContent() { scaleSpring.endValue = 0.0 val anim = AlphaAnimation(1.0f, 0.0f) anim.duration = 200 anim.repeatMode = Animation.RELATIVE_TO_SELF startAnimation(anim) } fun showContent() { scaleSpring.endValue = 1.0 val anim = AlphaAnimation(0.0f, 1.0f) anim.duration = 100 anim.repeatMode = Animation.RELATIVE_TO_SELF startAnimation(anim) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/FloatyHeadPlugin.kt ================================================ package ni.devotion.floaty_head import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.graphics.BitmapFactory import android.net.Uri import android.os.Build import android.os.IBinder import android.provider.Settings import android.util.Log import android.widget.FrameLayout import androidx.annotation.NonNull import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry.Registrar import io.flutter.view.FlutterCallbackInformation import io.flutter.view.FlutterMain import io.flutter.view.FlutterNativeView import io.flutter.view.FlutterRunArguments import ni.devotion.floaty_head.services.FloatyContentJobService.Companion.INTENT_EXTRA_IS_UPDATE_WINDOW import ni.devotion.floaty_head.services.FloatyIconService import ni.devotion.floaty_head.services.FloatyContentJobService import ni.devotion.floaty_head.utils.Commons.getMapFromObject import ni.devotion.floaty_head.utils.Constants.SHARED_PREF_FLOATY_HEAD import ni.devotion.floaty_head.utils.Constants.CALLBACK_HANDLE_KEY import ni.devotion.floaty_head.utils.Constants.CODE_CALLBACK_HANDLE_KEY import ni.devotion.floaty_head.utils.Constants.BACKGROUND_CHANNEL import ni.devotion.floaty_head.utils.Constants.INTENT_EXTRA_PARAMS_MAP import ni.devotion.floaty_head.utils.Constants.METHOD_CHANNEL import ni.devotion.floaty_head.utils.Constants.KEY_BODY import ni.devotion.floaty_head.utils.Constants.KEY_FOOTER import ni.devotion.floaty_head.utils.Constants.KEY_HEADER import ni.devotion.floaty_head.utils.Managment import ni.devotion.floaty_head.utils.Managment.bodyMap import ni.devotion.floaty_head.utils.Managment.bodyView import ni.devotion.floaty_head.utils.Managment.footerMap import ni.devotion.floaty_head.utils.Managment.footerView import ni.devotion.floaty_head.utils.Managment.headerView import ni.devotion.floaty_head.utils.Managment.headersMap import ni.devotion.floaty_head.utils.Managment.layoutParams import ni.devotion.floaty_head.utils.Managment.sIsIsolateRunning import ni.devotion.floaty_head.views.BodyView import ni.devotion.floaty_head.views.FooterView import ni.devotion.floaty_head.views.HeaderView import java.io.IOException import java.util.ArrayList import java.util.HashMap import kotlin.collections.List import kotlin.collections.Map class FloatyHeadPlugin : ActivityAware, FlutterPlugin, MethodChannel.MethodCallHandler { companion object { var mBound: Boolean = false lateinit var instance: FloatyHeadPlugin var activity: Activity? = null var context: Context? = null var sBackgroundFlutterView: FlutterNativeView? = null private var channel: MethodChannel? = null private var backgroundChannel: MethodChannel? = null } var sPluginRegistrantCallback: PluginRegistry.PluginRegistrantCallback? = null private val CODE_DRAW_OVER_OTHER_APP_PERMISSION = 2084 fun setPluginRegistrant(callback: PluginRegistry.PluginRegistrantCallback) { Managment.pluginRegistrantC = callback sPluginRegistrantCallback = callback } fun registerWith(pluginRegistrar: Registrar) { context = pluginRegistrar.context() channel = MethodChannel(pluginRegistrar.messenger(), METHOD_CHANNEL) channel?.setMethodCallHandler(FloatyHeadPlugin()) } fun startCallBackHandler(context: Context) { var preferences = context.getSharedPreferences(SHARED_PREF_FLOATY_HEAD, 0) val callBackHandle: Long = preferences.getLong(CALLBACK_HANDLE_KEY, -1) if (callBackHandle != -1L) { FlutterMain.ensureInitializationComplete(context, null) val mAppBundlePath: String = FlutterMain.findAppBundlePath() val flutterCallback: FlutterCallbackInformation = FlutterCallbackInformation.lookupCallbackInformation(callBackHandle) sBackgroundFlutterView?.let { sbfv -> backgroundChannel ?: run { backgroundChannel = MethodChannel(sbfv, BACKGROUND_CHANNEL) } Managment.sIsIsolateRunning.set(true) } ?: run { sBackgroundFlutterView = FlutterNativeView(context, true) if(mAppBundlePath != null && !Managment.sIsIsolateRunning.get()) { Managment.pluginRegistrantC ?: run { Log.i("TAG", "Unable to start callBackHandle... as plugin is not registered") return } val args = FlutterRunArguments() args.bundlePath = mAppBundlePath args.entrypoint = flutterCallback.callbackName args.libraryPath = flutterCallback.callbackLibraryPath sBackgroundFlutterView!!.runFromBundle(args) Managment.pluginRegistrantC?.registerWith(sBackgroundFlutterView!!.getPluginRegistry()) backgroundChannel = MethodChannel(sBackgroundFlutterView!!, BACKGROUND_CHANNEL) Managment.sIsIsolateRunning.set(true) } Managment.sIsIsolateRunning.set(true) } } } fun invokeCallBack(context: Context, type: String, params: Any) { val argumentsList: MutableList = ArrayList() val preferences = activity!!.applicationContext.getSharedPreferences(SHARED_PREF_FLOATY_HEAD, 0) val codeCallBackHandle = preferences.getLong(CODE_CALLBACK_HANDLE_KEY, -1) if (codeCallBackHandle == -1L) { Log.e("TAG", "Back failed, as codeCallBackHandle is null") } else { argumentsList.clear() argumentsList.add(codeCallBackHandle) argumentsList.add(type) argumentsList.add(params) if(Managment.sIsIsolateRunning.get()) { backgroundChannel ?: run{ backgroundChannel = MethodChannel(sBackgroundFlutterView, BACKGROUND_CHANNEL) } try { val retries = intArrayOf(2) invokeCallBackToFlutter(backgroundChannel!!, "callBack", argumentsList, retries) //channel!!.invokeMethod("callBack", argumentsList); }catch (ex: Exception) { Log.e("TAG", "Exception in invoking callback $ex") } } else { Log.e("TAG", "invokeCallBack failed, as isolate is not running") } } } private fun invokeCallBackToFlutter(channel: MethodChannel, method: String, arguments: List, retries: IntArray) { channel.invokeMethod(method, arguments, object : MethodChannel.Result { override fun success(o: Any?) { Log.i("TAG", "Invoke call back success") } override fun error(s: String?, s1: String?, o: Any?) { Log.e("TAG", "Error $s$s1") } override fun notImplemented() { if (retries[0] > 0) { Log.d("TAG", "Not Implemented method $method. Trying again to check if it works") invokeCallBackToFlutter(channel, method, arguments, retries) } else { Log.e("TAG", "Not Implemented method $method") } retries[0]-- } }) } private fun FloatyHeadPlugin(_context: Context, _activity: Activity, _methodChannel: MethodChannel) { activity = _activity context = _context channel = _methodChannel channel?.let { it.setMethodCallHandler(this) } } override fun onMethodCall(call: MethodCall, @NonNull result: Result) { when (call.method) { "start" -> { Managment.globalContext = activity?.applicationContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) { val packageName = activity?.packageName activity?.startActivityForResult( Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), CODE_DRAW_OVER_OTHER_APP_PERMISSION) } else { if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { val subIntent = Intent(activity?.applicationContext, FloatyContentJobService::class.java) subIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) subIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) subIntent.putExtra(INTENT_EXTRA_IS_UPDATE_WINDOW, true) activity?.startService(subIntent) mBound = true } else { val subIntent = Intent(activity?.applicationContext, FloatyContentJobService::class.java) activity?.startForegroundService(subIntent) mBound = true } } } "isOpen" -> result.success(mBound) "close" -> { if(mBound){ FloatyContentJobService.instance!!.closeWindow(true) if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q){ activity?.stopService(Intent(activity?.applicationContext, FloatyContentJobService::class.java)) }else{ activity?.startForegroundService(Intent(activity?.applicationContext, FloatyContentJobService::class.java)) } mBound = false } } "setIcon" -> result.success(setIconFromAsset(call.arguments as String)) "setBackgroundCloseIcon" -> result.success(setBackgroundCloseIconFromAsset(call.arguments as String)) "setCloseIcon" -> result.success(setCloseIconFromAsset(call.arguments as String)) "setNotificationTitle" -> result.success(setNotificationTitle(call.arguments as String)) "setNotificationIcon" -> result.success(setNotificationIcon(call.arguments as String)) "setFloatyHeadContent" -> { assert((call.arguments != null)) val updateParams = call.arguments as HashMap headersMap = getMapFromObject(updateParams, KEY_HEADER) bodyMap = getMapFromObject(updateParams, KEY_BODY) footerMap = getMapFromObject(updateParams, KEY_FOOTER) layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT) try { headersMap?.let { headerView = HeaderView(activity!!.applicationContext, it).view } bodyMap?.let { bodyView = BodyView(activity!!.applicationContext, it).view } footerMap?.let { footerView = FooterView(activity!!.applicationContext, it).view } } catch (except: Exception) { except.printStackTrace() } result.success(true) } "registerCallBackHandler" -> { try { val arguments = call.arguments as List<*> arguments ?: result.success(false) arguments?.let { val callBackHandle = (it[0]).toString().toLong() val onClickHandle = (it[1]).toString().toLong() val preferences = activity?.applicationContext!!.getSharedPreferences(SHARED_PREF_FLOATY_HEAD, 0) preferences?.edit()?.putLong(CALLBACK_HANDLE_KEY, callBackHandle)!!.commit() preferences?.edit()?.putLong(CODE_CALLBACK_HANDLE_KEY, onClickHandle)!!.commit() startCallBackHandler(activity!!.applicationContext) result.success(true) } } catch (ex: Exception) { Log.e("TAG", "Exception in registerOnClickHandler " + ex.toString()) result.success(false) } } else -> result.notImplemented() } } private fun setNotificationTitle(title: String):Int { var result = -1 try { Managment.notificationTitle = title result = 1 }catch (e: IOException) { e.printStackTrace() } return result } private fun setNotificationIcon(assetPath: String):Int { var result = -1 try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val inputStream = activity!!.applicationContext.assets.open("flutter_assets/" + assetPath) val bitmap = BitmapFactory.decodeStream(inputStream) Managment.notificationIcon = bitmap result = 1 } else { val assetLookupKey = FlutterLoader.getInstance().getLookupKeyForAsset(assetPath) val assetManager = activity!!.applicationContext.assets val assetFileDescriptor = assetManager.openFd(assetLookupKey) val inputStream = assetFileDescriptor.createInputStream() Managment.notificationIcon = BitmapFactory.decodeStream(inputStream) result = 1 } }catch (e: IOException) { e.printStackTrace() } return result } private fun setBackgroundCloseIconFromAsset(assetPath: String):Int { var result = -1 try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val inputStream = activity!!.applicationContext.assets.open("flutter_assets/" + assetPath) val bitmap = BitmapFactory.decodeStream(inputStream) Managment.backgroundCloseIcon = bitmap result = 1 } else { val assetLookupKey = FlutterLoader.getInstance().getLookupKeyForAsset(assetPath) val assetManager = activity!!.applicationContext.assets val assetFileDescriptor = assetManager.openFd(assetLookupKey) val inputStream = assetFileDescriptor.createInputStream() Managment.backgroundCloseIcon = BitmapFactory.decodeStream(inputStream) result = 1 } }catch (e: IOException) { e.printStackTrace() } return result } private fun setCloseIconFromAsset(assetPath: String):Int { var result = -1 try { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ val inputStream = activity!!.applicationContext.assets.open("flutter_assets/" + assetPath) val bitmap = BitmapFactory.decodeStream(inputStream) Managment.closeIcon = bitmap result = 1 } else { val assetLookupKey = FlutterLoader.getInstance().getLookupKeyForAsset(assetPath) val assetManager = activity!!.applicationContext.assets val assetFileDescriptor = assetManager.openFd(assetLookupKey) val inputStream = assetFileDescriptor.createInputStream() Managment.closeIcon = BitmapFactory.decodeStream(inputStream) result = 1 } }catch (e: IOException) { e.printStackTrace() } return result } private fun setIconFromAsset(assetPath: String):Int { var result = -1 try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val inputStream = activity!!.applicationContext.assets.open("flutter_assets/" + assetPath) val bitmap = BitmapFactory.decodeStream(inputStream) Managment.floatingIcon = bitmap result = 1 } else { val assetLookupKey = FlutterLoader.getInstance().getLookupKeyForAsset(assetPath) val assetManager = activity!!.applicationContext.assets val assetFileDescriptor = assetManager.openFd(assetLookupKey) val inputStream = assetFileDescriptor.createInputStream() Managment.floatingIcon = BitmapFactory.decodeStream(inputStream) result = 1 } }catch (e: IOException) { e.printStackTrace() } return result } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel?.setMethodCallHandler(null) //release() } override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL) channel?.setMethodCallHandler(this) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity Managment.activity = binding.activity instance = this@FloatyHeadPlugin } override fun onDetachedFromActivity() { //release() } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity Managment.activity = binding.activity instance = this@FloatyHeadPlugin } override fun onDetachedFromActivityForConfigChanges() { //release() } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/MainActivity.kt ================================================ package ni.devotion.floaty_head import android.content.Context import android.graphics.Color import android.os.Bundle import android.widget.FrameLayout import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import ni.devotion.floaty_head.utils.Commons.getMapFromObject import ni.devotion.floaty_head.utils.Constants.INTENT_EXTRA_PARAMS_MAP import ni.devotion.floaty_head.utils.Constants.KEY_BODY import ni.devotion.floaty_head.utils.Constants.KEY_FOOTER import ni.devotion.floaty_head.utils.Constants.KEY_HEADER import ni.devotion.floaty_head.utils.Managment.bodyMap import ni.devotion.floaty_head.utils.Managment.footerMap import ni.devotion.floaty_head.utils.Managment.headerView import ni.devotion.floaty_head.utils.Managment.headersMap import ni.devotion.floaty_head.utils.Managment.layoutParams import ni.devotion.floaty_head.utils.Managment.paramsMap import ni.devotion.floaty_head.views.HeaderView import java.util.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/floating_chathead/ChatHead.kt ================================================ package ni.devotion.floaty_head.floating_chathead import android.graphics.* import android.graphics.BitmapFactory.* import android.view.* import com.facebook.rebound.* import ni.devotion.floaty_head.R import ni.devotion.floaty_head.services.FloatyIconService import ni.devotion.floaty_head.utils.ImageHelper import ni.devotion.floaty_head.utils.Managment import kotlin.math.hypot import kotlin.math.pow class ChatHead(var chatHeads: ChatHeads): View(chatHeads.context), View.OnTouchListener, SpringListener { var isTop: Boolean = false var isActive: Boolean = false var params: WindowManager.LayoutParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManagerHelper.getLayoutFlag(), 0, PixelFormat.TRANSLUCENT ) var springSystem = SpringSystem.create() var springX = springSystem.createSpring() var springY = springSystem.createSpring() val paint = Paint() private var initialX = 0.0f private var initialY = 0.0f private var initialTouchX = 0.0f private var initialTouchY = 0.0f private var moving = false override fun onSpringEndStateChange(spring: Spring?) {} override fun onSpringAtRest(spring: Spring?) {} override fun onSpringActivate(spring: Spring?) {} init { params.gravity = Gravity.TOP or Gravity.START params.x = 0 params.y = 0 params.width = ChatHeads.CHAT_HEAD_SIZE + 15 params.height = ChatHeads.CHAT_HEAD_SIZE + 30 springX.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { x = spring.currentValue.toFloat() } }) springX.springConfig = SpringConfigs.NOT_DRAGGING springX.addListener(this) springY.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { y = spring.currentValue.toFloat() } }) springY.springConfig = SpringConfigs.NOT_DRAGGING springY.addListener(this) this.setLayerType(LAYER_TYPE_HARDWARE, paint) chatHeads.addView(this, params) this.setOnTouchListener(this) } override fun onSpringUpdate(spring: Spring) { if (spring !== this.springX && spring !== this.springY) return val totalVelocity = hypot(springX.velocity, springY.velocity).toInt() chatHeads.onSpringUpdate(this, spring, totalVelocity) } override fun onDraw(canvas: Canvas?) { Managment.floatingIcon ?: canvas?.drawBitmap(ImageHelper.addShadow((ImageHelper.getCircularBitmap(decodeResource(Managment.globalContext!!.resources, R.drawable.bot)))), 0f, 0f, paint) Managment.floatingIcon?.let { canvas?.drawBitmap(ImageHelper.addShadow(ImageHelper.getCircularBitmap(it)), 0f, 0f, paint) } } override fun onTouch(v: View?, event: MotionEvent?): Boolean { val currentChatHead = chatHeads.chatHeads.find { it == v }!! val metrics = WindowManagerHelper.getScreenSize() when (event!!.action) { MotionEvent.ACTION_DOWN -> { initialX = x initialY = y initialTouchX = event.rawX initialTouchY = event.rawY scaleX = 0.9f scaleY = 0.9f } MotionEvent.ACTION_UP -> { if (!moving) { if (currentChatHead.isActive) { chatHeads.collapse() } else { val selectedChatHead = chatHeads.chatHeads.find { it.isActive } selectedChatHead?.isActive = false currentChatHead.isActive = true chatHeads.changeContent() } } else { springX.endValue = metrics.widthPixels - width - (chatHeads.chatHeads.size - 1 - chatHeads.chatHeads.indexOf(this)) * (width + ChatHeads.CHAT_HEAD_EXPANDED_PADDING).toDouble() springY.endValue = ChatHeads.CHAT_HEAD_EXPANDED_MARGIN_TOP.toDouble() if (isActive) { chatHeads.content.showContent() } } scaleX = 1f scaleY = 1f moving = false } MotionEvent.ACTION_MOVE -> { if (ChatHeads.distance(initialTouchX, event.rawX, initialTouchY, event.rawY) > ChatHeads.CHAT_HEAD_DRAG_TOLERANCE.pow(2) && !moving) { moving = true if (isActive) { chatHeads.content.hideContent() } } if (moving) { springX.currentValue = initialX + (event.rawX - initialTouchX).toDouble() springY.currentValue = initialY + (event.rawY - initialTouchY).toDouble() } } } return true } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/floating_chathead/ChatHeads.kt ================================================ package ni.devotion.floaty_head.floating_chathead import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat import android.view.* import android.widget.FrameLayout import android.widget.LinearLayout import android.view.VelocityTracker import com.facebook.rebound.Spring import com.facebook.rebound.SimpleSpringListener import com.facebook.rebound.SpringChain import java.util.* import kotlin.math.* import android.app.ActivityManager import ni.devotion.floaty_head.FloatFragment import ni.devotion.floaty_head.R import ni.devotion.floaty_head.services.FloatyContentJobService import ni.devotion.floaty_head.services.FloatyIconService import ni.devotion.floaty_head.utils.Managment class ChatHeads(context: Context) : View.OnTouchListener, FrameLayout(context) { companion object { val CHAT_HEAD_OUT_OF_SCREEN_X: Int = WindowManagerHelper.dpToPx(10f) val CHAT_HEAD_SIZE: Int = WindowManagerHelper.dpToPx(64f) val CHAT_HEAD_PADDING: Int = WindowManagerHelper.dpToPx(6f) val CHAT_HEAD_EXPANDED_PADDING: Int = WindowManagerHelper.dpToPx(4f) val CHAT_HEAD_EXPANDED_MARGIN_TOP: Float = WindowManagerHelper.dpToPx(4f).toFloat() val CLOSE_SIZE = WindowManagerHelper.dpToPx(64f) val CLOSE_CAPTURE_DISTANCE = WindowManagerHelper.dpToPx(100f) val CLOSE_ADDITIONAL_SIZE = WindowManagerHelper.dpToPx(24f) const val CHAT_HEAD_DRAG_TOLERANCE: Float = 20f fun distance(x1: Float, x2: Float, y1: Float, y2: Float): Float { return ((x1 - x2).pow(2) + (y1-y2).pow(2)) } } var wasMoving = false var captured = false var movingOutOfClose = false private var initialX = 0.0f private var initialY = 0.0f private var initialTouchX = 0.0f private var initialTouchY = 0.0f private var initialVelocityX = 0.0 private var initialVelocityY = 0.0 private var lastY = 0.0 private var moving = false private var toggled = false private var motionTrackerUpdated = false private var collapsing = false private var blockAnim = false private var horizontalSpringChain: SpringChain? = null private var verticalSpringChain: SpringChain? = null private var isOnRight = false private var velocityTracker: VelocityTracker? = null private var motionTracker = LinearLayout(context) var topChatHead: ChatHead? = null var content = FloatFragment(context) private var close = Close(this) var chatHeads = ArrayList() private var motionTrackerParams = WindowManager.LayoutParams( CHAT_HEAD_SIZE, CHAT_HEAD_SIZE + 16, WindowManagerHelper.getLayoutFlag(), WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT ) private var params = WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, WindowManagerHelper.getLayoutFlag(), WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT ) init { context.setTheme(R.style.Theme_MaterialComponents_Light) params.gravity = Gravity.START or Gravity.TOP params.dimAmount = 0.7f motionTrackerParams.gravity = Gravity.START or Gravity.TOP FloatyContentJobService.instance?.windowManager?.addView(motionTracker, motionTrackerParams) FloatyContentJobService.instance?.windowManager?.addView(this, params) this.addView(content) motionTracker.setOnTouchListener(this) this.setOnTouchListener{ v, event -> v.performClick() when (event.action) { MotionEvent.ACTION_UP -> { if (v == this) { collapse() } } } return@setOnTouchListener false } } fun setTop(chatHead: ChatHead) { topChatHead?.isTop = false chatHead.isTop = true topChatHead = chatHead } fun fixPositions(animation: Boolean = true) { if (topChatHead == null) return val metrics = WindowManagerHelper.getScreenSize() val newX = if (isOnRight) metrics.widthPixels - topChatHead!!.width + CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() else -CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() val newY = initialY.toDouble() if (animation) { topChatHead!!.springX.endValue = newX topChatHead!!.springY.endValue = newY } else { topChatHead!!.springX.currentValue = newX topChatHead!!.springY.currentValue = newY } } private fun destroySpringChains() { horizontalSpringChain?.let { for (spring in it.allSprings) { spring.destroy() } } verticalSpringChain?.let { for (spring in it.allSprings) { spring.destroy() } } verticalSpringChain = null horizontalSpringChain = null } @SuppressLint("NewApi") private fun resetSpringChains() { destroySpringChains() horizontalSpringChain = SpringChain.create(0, 0, 200, 15) verticalSpringChain = SpringChain.create(0, 0, 200, 15) chatHeads.forEachIndexed { index, element -> element.z = index.toFloat() if (element.isTop) { horizontalSpringChain!!.addSpring(object : SimpleSpringListener() { }) verticalSpringChain!!.addSpring(object : SimpleSpringListener() { }) element.z = chatHeads.size.toFloat() horizontalSpringChain!!.setControlSpringIndex(index) verticalSpringChain!!.setControlSpringIndex(index) } else { horizontalSpringChain!!.addSpring(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring?) { if (!toggled && !blockAnim) { if (collapsing) { element.springX.endValue = spring!!.endValue + (chatHeads.size - 1 - index) * CHAT_HEAD_PADDING * if (isOnRight) 1 else -1 } else { element.springX.currentValue = spring!!.currentValue + (chatHeads.size - 1 - index) * CHAT_HEAD_PADDING * if (isOnRight) 1 else -1 } } } }) verticalSpringChain!!.addSpring(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring?) { if (!toggled && !blockAnim) { element.springY.currentValue = spring!!.currentValue } } }) } } } fun add(): ChatHead { chatHeads.forEach { it.visibility = View.VISIBLE } val chatHead = ChatHead(this) chatHeads.add(chatHead) var lx = -CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() var ly = 0.0 if (topChatHead != null) { lx = topChatHead!!.springX.currentValue ly = topChatHead!!.springY.currentValue } setTop(chatHead) destroySpringChains() resetSpringChains() blockAnim = true chatHeads.forEachIndexed { index, element -> element.springX.currentValue = lx + (chatHeads.size - 1 - index) * CHAT_HEAD_PADDING * if (isOnRight) 1 else -1 element.springY.currentValue = ly } motionTrackerParams.x = chatHead.springX.currentValue.toInt() motionTrackerParams.y = chatHead.springY.currentValue.toInt() motionTrackerParams.flags = motionTrackerParams.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() FloatyContentJobService.instance?.windowManager?.updateViewLayout(motionTracker, motionTrackerParams) return chatHead } fun collapse() { toggled = false collapsing = true fixPositions() chatHeads.forEach { it.isActive = false } content.hideContent() motionTrackerParams.flags = motionTrackerParams.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() FloatyContentJobService.instance?.windowManager?.updateViewLayout(motionTracker, motionTrackerParams) params.flags = ((params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()) and WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL.inv() or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE FloatyContentJobService.instance?.windowManager?.updateViewLayout(this, params) } fun changeContent() { val chatHead = chatHeads.find { it.isActive }!! //content.messagesView.removeAllViews() // for (message in chatHead.messages) { // content.addMessage(message) // } } fun getRunningServiceInfo(serviceClass: Class<*>, context: Context): ActivityManager.RunningServiceInfo? { val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager for (service in manager.getRunningServices(Integer.MAX_VALUE)) { if (serviceClass.name == service.service.className) { return service } } return null } fun hideChatHeads(isClosed:Boolean = false) { if(isClosed){ close.hide() postDelayed({ topChatHead?.let { it.springY.currentValue = 0.0 it.springX.currentValue = 0.0 } FloatyContentJobService.instance!!.closeWindow(true) }, 300) }else{ close.hide() postDelayed({ topChatHead?.let { it.springY.currentValue = 0.0 it.springX.currentValue = 0.0 } }, 300) } } fun onSpringUpdate(chatHead: ChatHead, spring: Spring, totalVelocity: Int) { val metrics = WindowManagerHelper.getScreenSize() if (topChatHead != null && chatHead == topChatHead!!) { if (horizontalSpringChain != null && spring == chatHead.springX) { horizontalSpringChain!!.controlSpring.currentValue = spring.currentValue } if (verticalSpringChain != null && spring == chatHead.springY) { verticalSpringChain!!.controlSpring.currentValue = spring.currentValue } } var tmpChatHead: ChatHead? = null if (collapsing) tmpChatHead = topChatHead!! else if (chatHead.isActive) tmpChatHead = chatHead if (tmpChatHead != null) { content.x = tmpChatHead.springX.currentValue.toFloat() - metrics.widthPixels.toFloat() + ((chatHeads.size - 1 - chatHeads.indexOf(tmpChatHead)) * (tmpChatHead.width + CHAT_HEAD_EXPANDED_PADDING)) + tmpChatHead.width content.y = tmpChatHead.springY.currentValue.toFloat() - CHAT_HEAD_EXPANDED_MARGIN_TOP content.pivotX = metrics.widthPixels.toFloat() - chatHead.width / 2 - ((chatHeads.size - 1 - chatHeads.indexOf(tmpChatHead)) * (tmpChatHead.width + CHAT_HEAD_EXPANDED_PADDING)) } content.pivotY = chatHead.height.toFloat() if (!moving && distance(close.x, topChatHead!!.springX.currentValue.toFloat(), close.y, topChatHead!!.springY.currentValue.toFloat()) < CLOSE_CAPTURE_DISTANCE * CLOSE_CAPTURE_DISTANCE && !captured && close.visibility == View.VISIBLE) { topChatHead!!.springX.springConfig = SpringConfigs.CAPTURING topChatHead!!.springY.springConfig = SpringConfigs.CAPTURING topChatHead!!.springX.endValue = close.springX.endValue topChatHead!!.springY.endValue = close.springY.endValue postDelayed({ hideChatHeads(false) }, 300) captured = true } if (wasMoving) { motionTrackerParams.x = if (isOnRight) metrics.widthPixels - chatHead.width else 0 lastY = chatHead.springY.currentValue if (abs(chatHead.springY.velocity) > 3000 && (chatHead.springX.currentValue > metrics.widthPixels - chatHead.width + CHAT_HEAD_OUT_OF_SCREEN_X / 2 || chatHead.springX.currentValue < -CHAT_HEAD_OUT_OF_SCREEN_X / 2) && abs(initialVelocityX) > 3000) { chatHead.springY.velocity = 3000.0 * if (initialVelocityY < 0) -1 else 1 } if ((chatHead.springX.currentValue < -CHAT_HEAD_OUT_OF_SCREEN_X / 2 && initialVelocityX < -3000 || chatHead.springX.currentValue > metrics.widthPixels - chatHead.width + CHAT_HEAD_OUT_OF_SCREEN_X / 2) && abs(initialVelocityY) < abs(initialVelocityX)) { chatHead.springY.velocity = 0.0 } if (abs(chatHead.springY.velocity) > 500) { if (chatHead.springY.currentValue < 0) { chatHead.springY.velocity = -500.0 } else if (chatHead.springY.currentValue > metrics.heightPixels) { chatHead.springY.velocity = 500.0 } } if (!moving) { if (spring === chatHead.springX) { val xPosition = chatHead.springX.currentValue if (xPosition + chatHead.width > metrics.widthPixels && chatHead.springX.velocity > 0) { val newPos = metrics.widthPixels - chatHead.width + CHAT_HEAD_OUT_OF_SCREEN_X chatHead.springX.springConfig = SpringConfigs.NOT_DRAGGING chatHead.springX.endValue = newPos.toDouble() isOnRight = true } else if (xPosition < 0 && chatHead.springX.velocity < 0) { chatHead.springX.springConfig = SpringConfigs.NOT_DRAGGING chatHead.springX.endValue = -CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() isOnRight = false } } else if (spring === chatHead.springY) { val yPosition = chatHead.springY.currentValue if (yPosition + chatHead.height > metrics.heightPixels && chatHead.springY.velocity > 0) { chatHead.springY.springConfig = SpringConfigs.NOT_DRAGGING chatHead.springY.endValue = metrics.heightPixels - chatHead.height.toDouble() - WindowManagerHelper.dpToPx(25f) } else if (yPosition < 0 && chatHead.springY.velocity < 0) { chatHead.springY.springConfig = SpringConfigs.NOT_DRAGGING chatHead.springY.endValue = 0.0 } } } if (abs(totalVelocity) % 10 == 0 && !moving) { motionTrackerParams.y = topChatHead!!.springY.currentValue.toInt() FloatyContentJobService.instance?.windowManager?.updateViewLayout(motionTracker, motionTrackerParams) } } } override fun onTouch(v: View?, event: MotionEvent?): Boolean { val metrics = WindowManagerHelper.getScreenSize() if (topChatHead == null) return true when (event!!.action) { MotionEvent.ACTION_DOWN -> { topChatHead?.let { initialX = it.springX.currentValue.toFloat() initialY = it.springY.currentValue.toFloat() initialTouchX = event.rawX initialTouchY = event.rawY wasMoving = false collapsing = false blockAnim = false close.show() it.scaleX = 0.9f it.scaleY = 0.9f it.springX.springConfig = SpringConfigs.DRAGGING it.springY.springConfig = SpringConfigs.DRAGGING it.springX.setAtRest() it.springY.setAtRest() } motionTrackerUpdated = false when (velocityTracker) { null -> velocityTracker = VelocityTracker.obtain() else -> velocityTracker?.clear() } velocityTracker?.addMovement(event) } MotionEvent.ACTION_UP -> { if (moving) wasMoving = true postDelayed({ close.hide() if (captured) { content.removeAllViews() hideChatHeads(true) } }, 200) if (captured) return true if (!moving) { if (!toggled) { toggled = true chatHeads.forEachIndexed { index, it -> it.springX.springConfig = SpringConfigs.NOT_DRAGGING it.springY.springConfig = SpringConfigs.NOT_DRAGGING it.springY.endValue = CHAT_HEAD_EXPANDED_MARGIN_TOP.toDouble() it.springX.endValue = metrics.widthPixels - topChatHead!!.width.toDouble() - (chatHeads.size - 1 - index) * (it.width + CHAT_HEAD_EXPANDED_PADDING).toDouble() } motionTrackerParams.flags = motionTrackerParams.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE FloatyContentJobService.instance?.windowManager?.updateViewLayout(motionTracker, motionTrackerParams) params.flags = (params.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv()) or WindowManager.LayoutParams.FLAG_DIM_BEHIND or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() FloatyContentJobService.instance?.windowManager?.updateViewLayout(this, params) topChatHead!!.isActive = true changeContent() android.os.Handler().postDelayed( { content.showContent() }, 200 ) } } else if (!toggled) { moving = false var xVelocity = velocityTracker!!.xVelocity.toDouble() val yVelocity = velocityTracker!!.yVelocity.toDouble() var maxVelocityX = 0.0 velocityTracker?.recycle() velocityTracker = null if (xVelocity < -3500) { val newVelocity = ((-topChatHead!!.springX.currentValue - CHAT_HEAD_OUT_OF_SCREEN_X) * SpringConfigs.DRAGGING.friction) maxVelocityX = newVelocity - 5000 if (xVelocity > maxVelocityX) xVelocity = newVelocity - 500 } else if (xVelocity > 3500) { val newVelocity = ((metrics.widthPixels - topChatHead!!.springX.currentValue - topChatHead!!.width + CHAT_HEAD_OUT_OF_SCREEN_X) * SpringConfigs.DRAGGING.friction) maxVelocityX = newVelocity + 5000 if (maxVelocityX > xVelocity) xVelocity = newVelocity + 500 } else if (yVelocity > 20 || yVelocity < -20) { topChatHead!!.springX.springConfig = SpringConfigs.NOT_DRAGGING if (topChatHead!!.x >= metrics.widthPixels / 2) { topChatHead!!.springX.endValue = metrics.widthPixels - topChatHead!!.width + CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() isOnRight = true } else { topChatHead!!.springX.endValue = -CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() isOnRight = false } } else { topChatHead!!.springX.springConfig = SpringConfigs.NOT_DRAGGING topChatHead!!.springY.springConfig = SpringConfigs.NOT_DRAGGING if (topChatHead!!.x >= metrics.widthPixels / 2) { topChatHead!!.springX.endValue = metrics.widthPixels - topChatHead!!.width + CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() topChatHead!!.springY.endValue = topChatHead!!.y.toDouble() isOnRight = true } else { topChatHead!!.springX.endValue = -CHAT_HEAD_OUT_OF_SCREEN_X.toDouble() topChatHead!!.springY.endValue = topChatHead!!.y.toDouble() isOnRight = false } } if (xVelocity < 0) { topChatHead!!.springX.velocity = max(xVelocity, maxVelocityX) } else { topChatHead!!.springX.velocity = min(xVelocity, maxVelocityX) } initialVelocityX = topChatHead!!.springX.velocity initialVelocityY = topChatHead!!.springY.velocity topChatHead!!.springY.velocity = yVelocity } topChatHead!!.scaleX = 1f topChatHead!!.scaleY = 1f } MotionEvent.ACTION_MOVE -> { if (distance(initialTouchX, event.rawX, initialTouchY, event.rawY) > CHAT_HEAD_DRAG_TOLERANCE.pow(2)) { moving = true } velocityTracker?.addMovement(event) if (moving) { close.springX.endValue = (metrics.widthPixels / 2) + (((event.rawX + topChatHead!!.width / 2) / 7) - metrics.widthPixels / 2 / 7) - close.width.toDouble() / 2 close.springY.endValue = (metrics.heightPixels - CLOSE_SIZE) + max(((event.rawY + close.height / 2) / 10) - metrics.heightPixels / 10, -WindowManagerHelper.dpToPx(30f).toFloat()) - WindowManagerHelper.dpToPx(60f).toDouble() if (distance(close.x + close.width / 2, event.rawX, close.y + close.height / 2, event.rawY) < CLOSE_CAPTURE_DISTANCE * CLOSE_CAPTURE_DISTANCE) { topChatHead!!.springX.springConfig = SpringConfigs.CAPTURING topChatHead!!.springY.springConfig = SpringConfigs.CAPTURING close.springScale.endValue = CLOSE_ADDITIONAL_SIZE.toDouble() captured = true } else if (captured) { topChatHead!!.springX.springConfig = SpringConfigs.CAPTURING topChatHead!!.springY.springConfig = SpringConfigs.CAPTURING close.springScale.endValue = 0.0 topChatHead!!.springX.endValue = initialX + (event.rawX - initialTouchX).toDouble() topChatHead!!.springY.endValue = initialY + (event.rawY - initialTouchY).toDouble() captured = false movingOutOfClose = true postDelayed({ movingOutOfClose = false }, 100) } else if (!movingOutOfClose) { topChatHead!!.springX.springConfig = SpringConfigs.DRAGGING topChatHead!!.springY.springConfig = SpringConfigs.DRAGGING topChatHead!!.springX.currentValue = initialX + (event.rawX - initialTouchX).toDouble() topChatHead!!.springY.currentValue = initialY + (event.rawY - initialTouchY).toDouble() velocityTracker?.computeCurrentVelocity(2000) } } } } return true } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/floating_chathead/Close.kt ================================================ package ni.devotion.floaty_head.floating_chathead import android.graphics.* import android.os.Build import android.view.* import android.widget.FrameLayout import android.widget.RelativeLayout import androidx.core.content.ContextCompat import com.facebook.rebound.* import ni.devotion.floaty_head.R import ni.devotion.floaty_head.services.FloatyIconService import ni.devotion.floaty_head.utils.Managment class Close(var chatHeads: ChatHeads): View(chatHeads.context) { private var params = WindowManager.LayoutParams( ChatHeads.CLOSE_SIZE + ChatHeads.CLOSE_ADDITIONAL_SIZE, ChatHeads.CLOSE_SIZE + ChatHeads.CLOSE_ADDITIONAL_SIZE, WindowManagerHelper.getLayoutFlag(), WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT ) private var gradientParams = FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, WindowManagerHelper.dpToPx(150f)) var springSystem = SpringSystem.create() var springY = springSystem.createSpring() var springX = springSystem.createSpring() var springAlpha = springSystem.createSpring() var springScale = springSystem.createSpring() val paint = Paint() val gradient = FrameLayout(context) private var bitmapBg: Bitmap? = null private var bitmapClose: Bitmap? = null fun hide() { val metrics = WindowManagerHelper.getScreenSize() springY.endValue = metrics.heightPixels.toDouble() + height springX.endValue = metrics.widthPixels.toDouble() / 2 - width / 2 springAlpha.endValue = 0.0 } fun show() { visibility = View.VISIBLE springAlpha.endValue = 1.0 } private fun onPositionUpdate() { if (chatHeads.captured) { chatHeads.topChatHead!!.springX.endValue = springX.currentValue + width / 2 - chatHeads.topChatHead!!.width / 2 + 2 chatHeads.topChatHead!!.springY.endValue = springY.currentValue + height / 2 - chatHeads.topChatHead!!.height / 2 + 2 } } init { bitmapBg = Managment.backgroundCloseIcon ?: Bitmap.createScaledBitmap(BitmapFactory.decodeResource(Managment.globalContext!!.resources, R.drawable.close_bg), ChatHeads.CLOSE_SIZE, ChatHeads.CLOSE_SIZE, false) Managment.backgroundCloseIcon?.let { bitmapBg = Bitmap.createScaledBitmap(it, ChatHeads.CLOSE_SIZE, ChatHeads.CLOSE_SIZE, false) } bitmapClose = Managment.closeIcon ?: Bitmap.createScaledBitmap(BitmapFactory.decodeResource(Managment.globalContext!!.resources, R.drawable.close), WindowManagerHelper.dpToPx(28f), WindowManagerHelper.dpToPx(28f), false) Managment.closeIcon?.let { bitmapClose = Bitmap.createScaledBitmap(it, WindowManagerHelper.dpToPx(28f), WindowManagerHelper.dpToPx(28f), false) } this.setLayerType(View.LAYER_TYPE_HARDWARE, paint) visibility = View.INVISIBLE hide() springY.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { y = spring.currentValue.toFloat() if (chatHeads.captured && chatHeads.wasMoving) { chatHeads.topChatHead!!.springY.currentValue = spring.currentValue } onPositionUpdate() } }) springX.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { x = spring.currentValue.toFloat() onPositionUpdate() } }) springScale.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { bitmapBg = Managment.backgroundCloseIcon ?: Bitmap.createScaledBitmap(BitmapFactory.decodeResource(Managment.globalContext!!.resources, R.drawable.close_bg), (spring.currentValue + ChatHeads.CLOSE_SIZE).toInt(), (spring.currentValue + ChatHeads.CLOSE_SIZE).toInt(), false) Managment.backgroundCloseIcon?.let { bitmapBg = Bitmap.createScaledBitmap(it, (spring.currentValue + ChatHeads.CLOSE_SIZE).toInt(), (spring.currentValue + ChatHeads.CLOSE_SIZE).toInt(), false) } invalidate() } }) springAlpha.addListener(object : SimpleSpringListener() { override fun onSpringUpdate(spring: Spring) { gradient.alpha = spring.currentValue.toFloat() } }) springScale.springConfig = SpringConfigs.CLOSE_SCALE springY.springConfig = SpringConfigs.CLOSE_Y params.gravity = Gravity.START or Gravity.TOP gradientParams.gravity = Gravity.BOTTOM gradient.background = ContextCompat.getDrawable(context, R.drawable.gradient_bg) springAlpha.currentValue = 0.0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) z = 100f chatHeads.addView(this, params) chatHeads.addView(gradient, gradientParams) } override fun onDraw(canvas: Canvas?) { bitmapBg?.let { canvas?.drawBitmap(it, width / 2 - it.width.toFloat() / 2, height / 2 - it.height.toFloat() / 2, paint) } bitmapClose?.let { canvas?.drawBitmap(it, width / 2 - it.width.toFloat() / 2, height / 2 - it.height.toFloat() / 2, paint) } } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/floating_chathead/SpringConfig.kt ================================================ package ni.devotion.floaty_head.floating_chathead import com.facebook.rebound.SpringConfig object SpringConfigs { val NOT_DRAGGING = SpringConfig.fromOrigamiTensionAndFriction(60.0, 7.5) val CAPTURING = SpringConfig.fromBouncinessAndSpeed(8.0, 40.0) val CLOSE_SCALE = SpringConfig.fromBouncinessAndSpeed(7.0, 25.0) val CLOSE_Y = SpringConfig.fromBouncinessAndSpeed(3.0, 3.0) val DRAGGING = SpringConfig.fromOrigamiTensionAndFriction(0.0, 5.0) val CONTENT_SCALE = SpringConfig.fromBouncinessAndSpeed(5.0, 40.0) } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/floating_chathead/WindowManagerHelper.kt ================================================ package ni.devotion.floaty_head.floating_chathead import android.content.res.Resources import android.os.Build import android.view.WindowManager import android.util.TypedValue class WindowManagerHelper { companion object { fun getLayoutFlag(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { WindowManager.LayoutParams.TYPE_PHONE } fun getScreenSize() = Resources.getSystem().displayMetrics fun dpToPx(dp: Float) = (dp * Resources.getSystem().displayMetrics.density).toInt() fun spToPx(sp: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, Resources.getSystem().displayMetrics) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/models/Decoration.kt ================================================ package ni.devotion.floaty_head.models import android.content.Context import ni.devotion.floaty_head.utils.Commons import ni.devotion.floaty_head.utils.NumberUtils class Decoration(startColor: Any?, endColor: Any?, borderWidth: Any?, borderRadius: Any?, borderColor: Any?, context: Context?) { val startColor: Int var endColor = 0 val borderWidth: Int val borderRadius: Float val borderColor: Int var isGradient = false init { this.startColor = NumberUtils.getInt(startColor) if (endColor != null) { this.endColor = NumberUtils.getInt(endColor) isGradient = true } else { isGradient = false } this.borderWidth = Commons.getPixelsFromDp(context!!, NumberUtils.getInt(borderWidth)) this.borderRadius = Commons.getPixelsFromDp(context, NumberUtils.getFloat(borderRadius)) this.borderColor = NumberUtils.getInt(borderColor) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/models/Margin.kt ================================================ package ni.devotion.floaty_head.models import ni.devotion.floaty_head.utils.NumberUtils import android.content.Context import ni.devotion.floaty_head.utils.Commons class Margin(left: Any?, top: Any?, right: Any?, bottom: Any?, context: Context?) { val left: Int val top: Int val right: Int val bottom: Int init { this.left = Commons.getPixelsFromDp(context!!, NumberUtils.getInt(left)) this.top = Commons.getPixelsFromDp(context, NumberUtils.getInt(top)) this.right = Commons.getPixelsFromDp(context, NumberUtils.getInt(right)) this.bottom = Commons.getPixelsFromDp(context, NumberUtils.getInt(bottom)) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/models/Padding.kt ================================================ package ni.devotion.floaty_head.models import ni.devotion.floaty_head.utils.NumberUtils import android.content.Context import ni.devotion.floaty_head.utils.Commons class Padding(left: Any?, top: Any?, right: Any?, bottom: Any?, context: Context?) { val left: Int val top: Int val right: Int val bottom: Int init { this.left = Commons.getPixelsFromDp(context!!, NumberUtils.getInt(left)) this.top = Commons.getPixelsFromDp(context, NumberUtils.getInt(top)) this.right = Commons.getPixelsFromDp(context, NumberUtils.getInt(right)) this.bottom = Commons.getPixelsFromDp(context, NumberUtils.getInt(bottom)) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/services/FloatyContentJobService.kt ================================================ package ni.devotion.floaty_head.services import android.app.* import android.content.Context import android.content.Intent import android.os.Build import android.os.IBinder import android.util.Log import android.view.WindowManager import androidx.core.app.NotificationCompat import android.graphics.PixelFormat import android.view.WindowManager.LayoutParams import ni.devotion.floaty_head.FloatyHeadPlugin import ni.devotion.floaty_head.R import ni.devotion.floaty_head.floating_chathead.ChatHeads import ni.devotion.floaty_head.utils.Constants.INTENT_EXTRA_PARAMS_MAP import ni.devotion.floaty_head.utils.Managment import java.lang.Exception import java.util.* class FloatyContentJobService : Service() { companion object { var instance: FloatyContentJobService?= null val CHANNEL_ID = "ForegroundServiceChannel" val NOTIFICATION_ID = 1 val INTENT_EXTRA_IS_UPDATE_WINDOW = "IsUpdateWindow" val INTENT_EXTRA_IS_CLOSE_WINDOW = "IsCloseWindow" } var windowManager: WindowManager? = null var context: Context? = null var notification: Notification? = null var chatHeads: ChatHeads? = null override fun onCreate() { instance = this createNotificationChannel() showNotificationManager() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if(null != intent && intent.extras != null) { val paramsMap = (intent.getSerializableExtra(INTENT_EXTRA_PARAMS_MAP) as HashMap?) assert(paramsMap != null) context = this val isCloseWindow = intent.getBooleanExtra(INTENT_EXTRA_IS_CLOSE_WINDOW, false) //createWindow() if(!isCloseWindow){ val isUpdateWindow = intent.getBooleanExtra(INTENT_EXTRA_IS_CLOSE_WINDOW, false) if(isUpdateWindow){ //updateWindow() }else{ createWindow() } }else{ closeWindow(true) } } return START_STICKY } fun closeWindow(isEverythingDone: Boolean){ try { windowManager?.let { wm -> chatHeads?.let { ch -> ch.removeAllViews() wm.removeView(ch) chatHeads = null } } windowManager = null if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q){ Managment.activity?.stopService(Intent(Managment.activity?.applicationContext, this@FloatyContentJobService::class.java)) }else{ Managment.activity?.startForegroundService(Intent(Managment.activity?.applicationContext, this@FloatyContentJobService::class.java)) } }catch(ex: Exception){ Log.e("TAG", "View not found") } if(isEverythingDone) stopSelf() } fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val serviceChannel = NotificationChannel( CHANNEL_ID, "Foreground Service Channel", NotificationManager.IMPORTANCE_DEFAULT ) val manager = getSystemService(NotificationManager::class.java) assert(manager != null) manager.createNotificationChannel(serviceChannel) } } fun createWindow() { setWindowManager() val params:WindowManager.LayoutParams params = LayoutParams() params.width = LayoutParams.MATCH_PARENT params.height = LayoutParams.WRAP_CONTENT params.format = PixelFormat.TRANSLUCENT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { params.type = LayoutParams.TYPE_APPLICATION_OVERLAY params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL or LayoutParams.FLAG_SHOW_WHEN_LOCKED or LayoutParams.FLAG_NOT_FOCUSABLE } else { params.type = LayoutParams.TYPE_SYSTEM_ALERT or LayoutParams.TYPE_SYSTEM_OVERLAY params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL or LayoutParams.FLAG_NOT_FOCUSABLE } chatHeads = ChatHeads(this) chatHeads?.add() } fun showNotificationManager() { val notificationIntent = Intent(this, FloatyHeadPlugin::class.java) val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) notification = if(Managment.notificationIcon == null) { NotificationCompat.Builder(this, "ForegroundServiceChannel") .setContentTitle("${Managment.notificationTitle} is Currently Running") .setSmallIcon(R.drawable.ic_chathead) .setContentIntent(pendingIntent) .build() }else{ NotificationCompat.Builder(this, "ForegroundServiceChannel") .setContentTitle("${Managment.notificationTitle} is Currently Running") .setLargeIcon(Managment.notificationIcon) .setContentIntent(pendingIntent) .build() } startForeground(NOTIFICATION_ID, notification) } override fun onBind(p0: Intent?): IBinder? { return null } override fun onDestroy() { val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager assert(notificationManager != null) notificationManager.cancel(NOTIFICATION_ID) super.onDestroy() } private fun setWindowManager() = windowManager ?: run { windowManager = getSystemService(WINDOW_SERVICE) as WindowManager } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/services/FloatyIconService.kt ================================================ package ni.devotion.floaty_head.services import android.annotation.SuppressLint import android.app.* import android.content.Intent import android.os.Build import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import ni.devotion.floaty_head.FloatyHeadPlugin import ni.devotion.floaty_head.FloatyHeadPlugin.Companion.context import ni.devotion.floaty_head.MainActivity import ni.devotion.floaty_head.R import ni.devotion.floaty_head.utils.Managment class FloatyIconService: Service() { companion object { lateinit var instance: FloatyIconService var notificationManager: NotificationManager? = null var notification: Notification? = null } val channel_id = "2208" val floaty_notification_id = 2208 override fun onCreate() { instance = this super.onCreate() } @SuppressLint("NewApi") private fun initNotificationManager() { notificationManager ?: run { context?.let { notificationManager = it.getSystemService(NotificationManager::class.java) } ?: run { Log.e("TAG", "Context is null. Can't show the FloatyNotification") return } } } fun createNotificationChannel() { initNotificationManager() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val serviceChannel = NotificationChannel( channel_id, "Foreground Service Channel", NotificationManager.IMPORTANCE_DEFAULT ) notificationManager?.createNotificationChannel(serviceChannel) } } fun showNotificationManager() { val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) notification = if(Managment.notificationIcon == null) { NotificationCompat.Builder(this, "ForegroundServiceChannel") .setContentTitle("${Managment.notificationTitle} is Currently Running") .setSmallIcon(R.drawable.ic_chathead) .setContentIntent(pendingIntent) .build() }else{ NotificationCompat.Builder(this, "ForegroundServiceChannel") .setContentTitle("${Managment.notificationTitle} is Currently Running") .setLargeIcon(Managment.notificationIcon) .setContentIntent(pendingIntent) .build() } startForeground(floaty_notification_id, notification) } override fun onDestroy() { super.onDestroy() } override fun onBind(intent: Intent): IBinder? { return null } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { createNotificationChannel() showNotificationManager() return START_NOT_STICKY } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/utils/Commons.kt ================================================ package ni.devotion.floaty_head.utils import android.content.Context import android.graphics.Typeface import android.util.TypedValue import android.view.Gravity import android.widget.LinearLayout import androidx.annotation.Nullable import ni.devotion.floaty_head.models.Margin import ni.devotion.floaty_head.utils.Constants.KEY_MARGIN object Commons { fun getMapFromObject(map: Map, key: String?): Map? { return map[key] as Map? } fun getMapListFromObject(map: Map, key: String?): List>? { return map[key] as List>? } fun getSpFromPixels(context: Context, px: Float): Float { val scaledDensity = context.resources.displayMetrics.scaledDensity return px / scaledDensity } fun getPixelsFromDp(context: Context, dp: Int): Int { return if (dp == -1) -1 else TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics).toInt() } fun getPixelsFromDp(context: Context, dp: Float): Float { return if (dp == -1f) (-1).toFloat() else TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics) } fun getGravity(@Nullable gravityStr: String?, defVal: Int): Int { var gravity = defVal if (gravityStr != null) { when (gravityStr) { "top" -> gravity = Gravity.TOP "center" -> gravity = Gravity.CENTER "bottom" -> gravity = Gravity.BOTTOM "leading" -> gravity = Gravity.START "trailing" -> gravity = Gravity.END } } return gravity } fun getFontWeight(@Nullable fontWeightStr: String?, defVal: Int): Int { var fontWeight = defVal if (fontWeightStr != null) { fontWeight = when (fontWeightStr) { "normal" -> Typeface.NORMAL "bold" -> Typeface.BOLD "italic" -> Typeface.ITALIC "bold_italic" -> Typeface.BOLD_ITALIC else -> Typeface.NORMAL } } return fontWeight } fun setMargin(context: Context?, params: LinearLayout.LayoutParams, map: Map) { val margin: Margin = UiBuilder.getMargin(context, map[KEY_MARGIN]) params.setMargins(margin.left, margin.top, margin.right, margin.bottom) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/utils/Constants.kt ================================================ package ni.devotion.floaty_head.utils object Constants { val CHANNEL = "ni.devotion.floaty_head" val METHOD_CHANNEL = "ni.devotion/floaty_head" val BACKGROUND_CHANNEL = "ni.devotion.floaty_head/background" val SHARED_PREF_FLOATY_HEAD = "ni.devotion.floaty_head" val CALLBACK_HANDLE_KEY = "callback_handler" val CODE_CALLBACK_HANDLE_KEY = "code_callback_handler" val INTENT_EXTRA_PARAMS_MAP = "intent_params_map" val CALLBACK_TYPE_ONCLICK = "onClick" //Internal plugin param map keys val KEY_HEADER = "header" val KEY_BODY = "body" val KEY_FOOTER = "footer" val KEY_IS_SHOW_FOOTER = "isShowFooter" val KEY_TITLE = "title" val KEY_SUBTITLE = "subTitle" val KEY_TAG = "tag" val KEY_TEXT = "text" val KEY_FONT_SIZE = "fontSize" val KEY_FONT_WEIGHT = "fontWeight" val KEY_TEXT_COLOR = "textColor" val KEY_BUTTON = "button" val KEY_BUTTONS_LIST = "buttons" val KEY_BUTTON_POSITION = "buttonPosition" val KEY_BUTTONS_LIST_POSITION = "buttonsPosition" val KEY_DECORATION = "decoration" val KEY_START_COLOR = "startColor" val KEY_END_COLOR = "endColor" val KEY_BORDER_WIDTH = "borderWidth" val KEY_BORDER_COLOR = "borderColor" val KEY_BORDER_RADIUS = "borderRadius" val KEY_GRAVITY = "gravity" val KEY_PADDING = "padding" val KEY_MARGIN = "margin" val KEY_LEFT = "left" val KEY_TOP = "top" val KEY_RIGHT = "right" val KEY_BOTTOM = "bottom" val KEY_WIDTH = "width" val KEY_HEIGHT = "height" val KEY_ROWS = "rows" val KEY_COLUMNS = "columns" } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/utils/ImageHelper.kt ================================================ package ni.devotion.floaty_head.utils import android.annotation.SuppressLint import android.content.Context import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.Build import androidx.core.content.ContextCompat import ni.devotion.floaty_head.floating_chathead.ChatHeads class ImageHelper { companion object { fun getCircularBitmap(bitmap: Bitmap): Bitmap { val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) val canvas = Canvas(output) val paint = Paint() val rect = Rect(0, 0, bitmap.width, bitmap.height) paint.isAntiAlias = true canvas.drawARGB(0, 0, 0, 0) paint.color = -0xbdbdbe canvas.drawCircle(output.width.toFloat() / 2, output.height.toFloat() / 2, output.width.toFloat() / 2, paint) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) canvas.drawBitmap(bitmap, rect, rect, paint) return Bitmap.createScaledBitmap(output, ChatHeads.CHAT_HEAD_SIZE, ChatHeads.CHAT_HEAD_SIZE, true) } fun addShadow(src: Bitmap): Bitmap { val bmOut = Bitmap.createBitmap(src.width + 10, src.height + 20, Bitmap.Config.ARGB_8888) val centerX = (bmOut.width / 2 - src.width / 2).toFloat() val centerY = (bmOut.height / 2 - src.height / 2).toFloat() val canvas = Canvas(bmOut) canvas.drawColor(0, PorterDuff.Mode.CLEAR) val ptBlur = Paint() ptBlur.maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL) val offsetXY = IntArray(2) val bmAlpha = src.extractAlpha(ptBlur, offsetXY) val ptAlphaColor = Paint() ptAlphaColor.color = Color.argb(80, 0, 0, 0) canvas.drawBitmap(bmAlpha, centerX + offsetXY[0], centerY + offsetXY[1] + 4f, ptAlphaColor) bmAlpha.recycle() canvas.drawBitmap(src, centerX, centerY,null) return bmOut } } @SuppressLint("UseCompatLoadingForDrawables") fun drawableFromVector(context: Context, drawableId: Int): Drawable { val drawable = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> context.getDrawable(drawableId) else -> ContextCompat.getDrawable(context, drawableId) } val bitmap = Bitmap.createBitmap(drawable!!.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return BitmapDrawable(context.resources, Bitmap.createScaledBitmap(bitmap, drawable.intrinsicWidth, drawable.intrinsicHeight, false)) } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/utils/Managment.kt ================================================ package ni.devotion.floaty_head.utils import android.app.ActionBar import android.app.Activity import android.content.Context import android.graphics.Bitmap import android.view.View import android.widget.FrameLayout import java.util.HashMap import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry.Registrar import ni.devotion.floaty_head.services.FloatyIconService import java.util.concurrent.atomic.AtomicBoolean /** * Handle all the states of the project, and all the custom icons including the body that is gonna be displayed inside the chathead. */ object Managment { var floatingIcon: Bitmap? = null var closeIcon: Bitmap? = null var backgroundCloseIcon: Bitmap? = null var notificationTitle: String = "Floaty_head" var notificationIcon: Bitmap? = null var paramsMap: HashMap? = null var headersMap: Map? = null var bodyMap: Map? = null var footerMap: Map? = null var headerView: View? = null var bodyView: View? = null var footerView: View? = null var layoutParams: FrameLayout.LayoutParams? = null var pluginRegistrantC: PluginRegistry.PluginRegistrantCallback? = null var floatyIconService: FloatyIconService? = null var globalContext: Context? = null var activity: Activity? = null var sIsIsolateRunning = AtomicBoolean(false) } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/utils/NumberUtils.kt ================================================ package ni.devotion.floaty_head.utils import android.util.Log /** * Class used for convert any number to [float] or [int] and retrieve any number from an [any] object. */ object NumberUtils { private const val TAG = "NumberUtils" fun getFloat(`object`: Any?) = getNumber(`object`).toFloat() fun getInt(`object`: Any?) = getNumber(`object`).toInt() private fun getNumber(`object`: Any?): Number { var `val`: Number = 0 if (`object` != null) { try { `val` = `object` as Number } catch (ex: Exception) { Log.d(TAG, ex.toString()) } } return `val` } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/utils/UiBuilder.kt ================================================ package ni.devotion.floaty_head.utils import android.content.Context import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.os.Build import android.util.TypedValue import android.view.View import android.widget.Button import android.widget.LinearLayout import android.widget.TextView import ni.devotion.floaty_head.FloatyHeadPlugin import ni.devotion.floaty_head.models.Decoration import ni.devotion.floaty_head.models.Margin import ni.devotion.floaty_head.models.Padding import ni.devotion.floaty_head.utils.Constants.CALLBACK_TYPE_ONCLICK import ni.devotion.floaty_head.utils.Constants.KEY_BORDER_COLOR import ni.devotion.floaty_head.utils.Constants.KEY_BORDER_RADIUS import ni.devotion.floaty_head.utils.Constants.KEY_BORDER_WIDTH import ni.devotion.floaty_head.utils.Constants.KEY_BOTTOM import ni.devotion.floaty_head.utils.Constants.KEY_DECORATION import ni.devotion.floaty_head.utils.Constants.KEY_END_COLOR import ni.devotion.floaty_head.utils.Constants.KEY_FONT_SIZE import ni.devotion.floaty_head.utils.Constants.KEY_FONT_WEIGHT import ni.devotion.floaty_head.utils.Constants.KEY_HEIGHT import ni.devotion.floaty_head.utils.Constants.KEY_LEFT import ni.devotion.floaty_head.utils.Constants.KEY_MARGIN import ni.devotion.floaty_head.utils.Constants.KEY_PADDING import ni.devotion.floaty_head.utils.Constants.KEY_RIGHT import ni.devotion.floaty_head.utils.Constants.KEY_START_COLOR import ni.devotion.floaty_head.utils.Constants.KEY_TAG import ni.devotion.floaty_head.utils.Constants.KEY_TEXT import ni.devotion.floaty_head.utils.Constants.KEY_TEXT_COLOR import ni.devotion.floaty_head.utils.Constants.KEY_TOP import ni.devotion.floaty_head.utils.Constants.KEY_WIDTH /** * This class is responsible to create all the content that is displayed inside the chathead. * if you wanna add your own widget, please be sure to create your [function], also remember to * create your class with the styles and components needed for that widget to be displayed. */ object UiBuilder { fun getTextView(context: Context?, textMap: Map?): TextView? { if (textMap == null) return null val textView = TextView(context) textView.text = textMap[KEY_TEXT] as String? textView.setTypeface(textView.typeface, Commons.getFontWeight(textMap[KEY_FONT_WEIGHT] as String?, Typeface.NORMAL)) textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, NumberUtils.getFloat(textMap[KEY_FONT_SIZE])) textView.setTextColor(NumberUtils.getInt(textMap[KEY_TEXT_COLOR])) val padding: Padding = getPadding(context, textMap[KEY_PADDING]) textView.setPadding(padding.left, padding.top, padding.right, padding.bottom) return textView } fun getPadding(context: Context?, `object`: Any?): Padding { val paddingMap = `object` as Map? ?: return Padding(0, 0, 0, 0, context) return Padding(paddingMap[KEY_LEFT], paddingMap[KEY_TOP], paddingMap[KEY_RIGHT], paddingMap[KEY_BOTTOM], context) } fun getMargin(context: Context?, `object`: Any?): Margin { val marginMap = `object` as Map? ?: return Margin(0, 0, 0, 0, context) return Margin(marginMap[KEY_LEFT], marginMap[KEY_TOP], marginMap[KEY_RIGHT], marginMap[KEY_BOTTOM], context) } fun getDecoration(context: Context?, `object`: Any?): Decoration? { val decorationMap = `object` as Map? ?: return null return Decoration(decorationMap[KEY_START_COLOR], decorationMap[KEY_END_COLOR], decorationMap[KEY_BORDER_WIDTH], decorationMap[KEY_BORDER_RADIUS], decorationMap[KEY_BORDER_COLOR], context) } fun getButtonView(context: Context?, buttonMap: Map?): Button? { buttonMap ?: return null val button = Button(context) val buttonText = getTextView(context, Commons.getMapFromObject(buttonMap, KEY_TEXT))!! button.text = buttonText.text val tag = buttonMap[KEY_TAG] button.tag = tag button.textSize = Commons.getSpFromPixels(context!!, buttonText.textSize) button.setTextColor(buttonText.textColors) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) button.elevation = 10f val params = LinearLayout.LayoutParams( Commons.getPixelsFromDp(context, buttonMap[KEY_WIDTH] as Int), Commons.getPixelsFromDp(context, buttonMap[KEY_HEIGHT] as Int), 1.0f) val buttonMargin: Margin = getMargin(context, buttonMap[KEY_MARGIN]) params.setMargins(buttonMargin.left, buttonMargin.top, buttonMargin.right, buttonMargin.bottom.coerceAtMost(4)) button.layoutParams = params val padding: Padding = getPadding(context, buttonMap[KEY_PADDING]) button.setPadding(padding.left, padding.top, padding.right, padding.bottom) val decoration: Decoration? = getDecoration(context, buttonMap[KEY_DECORATION]) decoration?.let{ val gd = getGradientDrawable(it) button.background = gd } button.setOnClickListener { if(!Managment.sIsIsolateRunning.get()){ FloatyHeadPlugin.instance.startCallBackHandler(context) } FloatyHeadPlugin.instance.invokeCallBack(context, CALLBACK_TYPE_ONCLICK, tag!!) } return button } fun getGradientDrawable(decoration: Decoration?): GradientDrawable { val gd = GradientDrawable() if (decoration!!.isGradient) { val colors = intArrayOf(decoration.startColor, decoration.endColor) gd.colors = colors gd.orientation = GradientDrawable.Orientation.LEFT_RIGHT } else { gd.setColor(decoration.startColor) } gd.cornerRadius = decoration.borderRadius gd.setStroke(decoration.borderWidth, decoration.borderColor) return gd } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/views/BodyView.kt ================================================ package ni.devotion.floaty_head.views import android.content.Context import android.graphics.Color import android.view.Gravity import android.view.View import android.widget.LinearLayout import ni.devotion.floaty_head.utils.Commons.getGravity import ni.devotion.floaty_head.utils.Commons.getMapFromObject import ni.devotion.floaty_head.utils.Commons.setMargin import ni.devotion.floaty_head.utils.Constants.KEY_COLUMNS import ni.devotion.floaty_head.utils.Constants.KEY_DECORATION import ni.devotion.floaty_head.utils.Constants.KEY_GRAVITY import ni.devotion.floaty_head.utils.Constants.KEY_PADDING import ni.devotion.floaty_head.utils.Constants.KEY_ROWS import ni.devotion.floaty_head.utils.Constants.KEY_TEXT import ni.devotion.floaty_head.utils.UiBuilder.getDecoration import ni.devotion.floaty_head.utils.UiBuilder.getGradientDrawable import ni.devotion.floaty_head.utils.UiBuilder.getPadding import ni.devotion.floaty_head.utils.UiBuilder.getTextView class BodyView(private val context: Context, private val bodyMap: Map) { val view: LinearLayout get() { val linearLayout = LinearLayout(context) linearLayout.orientation = LinearLayout.VERTICAL val decoration = getDecoration(context, bodyMap[KEY_DECORATION]) if (decoration != null) { val gd = getGradientDrawable(decoration) linearLayout.background = gd } else { linearLayout.setBackgroundColor(Color.WHITE) } val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) setMargin(context, params, bodyMap) linearLayout.layoutParams = params val padding = getPadding(context, bodyMap[KEY_PADDING]) linearLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom) val rowsMap = bodyMap[KEY_ROWS] as List>? if (rowsMap != null) { for (i in rowsMap.indices) { val row = rowsMap[i] linearLayout.addView(createRow(row)) } } return linearLayout } private fun createRow(rowMap: Map): View { val linearLayout = LinearLayout(context) linearLayout.orientation = LinearLayout.HORIZONTAL val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) setMargin(context, params, rowMap) linearLayout.layoutParams = params linearLayout.gravity = getGravity(rowMap[KEY_GRAVITY] as String?, Gravity.START) val padding = getPadding(context, rowMap[KEY_PADDING]) linearLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom) val decoration = getDecoration(context, rowMap[KEY_DECORATION]) if (decoration != null) { val gd = getGradientDrawable(decoration) linearLayout.background = gd } val columnsMap = rowMap[KEY_COLUMNS] as List>? if (columnsMap != null) { for (j in columnsMap.indices) { val column = columnsMap[j] linearLayout.addView(createColumn(column)) } } return linearLayout } private fun createColumn(columnMap: Map): View { val columnLayout = LinearLayout(context) columnLayout.orientation = LinearLayout.HORIZONTAL val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT) setMargin(context, params, columnMap) columnLayout.layoutParams = params val padding = getPadding(context, columnMap[KEY_PADDING]) columnLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom) val decoration = getDecoration(context, columnMap[KEY_DECORATION]) if (decoration != null) { val gd = getGradientDrawable(decoration) columnLayout.background = gd } val textView = getTextView(context, getMapFromObject(columnMap, KEY_TEXT)) columnLayout.addView(textView) return columnLayout } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/views/FooterView.kt ================================================ package ni.devotion.floaty_head.views import android.content.Context import android.view.Gravity import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout import ni.devotion.floaty_head.utils.Commons.getGravity import ni.devotion.floaty_head.utils.Commons.getMapFromObject import ni.devotion.floaty_head.utils.Commons.getMapListFromObject import ni.devotion.floaty_head.utils.Constants.KEY_BUTTONS_LIST import ni.devotion.floaty_head.utils.Constants.KEY_BUTTONS_LIST_POSITION import ni.devotion.floaty_head.utils.Constants.KEY_DECORATION import ni.devotion.floaty_head.utils.Constants.KEY_IS_SHOW_FOOTER import ni.devotion.floaty_head.utils.Constants.KEY_PADDING import ni.devotion.floaty_head.utils.Constants.KEY_TEXT import ni.devotion.floaty_head.utils.UiBuilder.getButtonView import ni.devotion.floaty_head.utils.UiBuilder.getDecoration import ni.devotion.floaty_head.utils.UiBuilder.getGradientDrawable import ni.devotion.floaty_head.utils.UiBuilder.getPadding import ni.devotion.floaty_head.utils.UiBuilder.getTextView class FooterView(private val context: Context, private val footerMap: Map) { val view: LinearLayout get() { val linearLayout = LinearLayout(context) linearLayout.orientation = LinearLayout.HORIZONTAL val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) val footerPadding = getPadding(context, footerMap[KEY_PADDING]) linearLayout.setPadding(footerPadding.left, footerPadding.top, footerPadding.right, footerPadding.bottom) linearLayout.layoutParams = params val decoration = getDecoration(context, footerMap[KEY_DECORATION]) if (decoration != null) { val gd = getGradientDrawable(decoration) linearLayout.background = gd } if (footerMap[KEY_IS_SHOW_FOOTER] as Boolean) { val textMap = getMapFromObject(footerMap, KEY_TEXT) val buttonsMap: List>? = getMapListFromObject(footerMap, KEY_BUTTONS_LIST) val textView = getTextView(context, textMap) val buttonsView: MutableList = ArrayList() for (buttonMap in buttonsMap!!) { buttonsView.add(getButtonView(context, buttonMap)) } val buttonsPosition = footerMap[KEY_BUTTONS_LIST_POSITION] as String? if (textView != null) { if (buttonsView.size > 0) { if ("leading" == buttonsPosition) { for (buttonView in buttonsView) { linearLayout.addView(buttonView) } linearLayout.addView(textView) } else { val param = LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f ) textView.layoutParams = param linearLayout.addView(textView) for (buttonView in buttonsView) { linearLayout.addView(buttonView) } } } else { linearLayout.addView(textView) } } else { for (buttonView in buttonsView) { linearLayout.addView(buttonView) } linearLayout.gravity = getGravity(buttonsPosition, Gravity.FILL) } } return linearLayout } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/views/HeaderView.kt ================================================ package ni.devotion.floaty_head.views import android.content.Context import android.graphics.Color import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.RelativeLayout import ni.devotion.floaty_head.utils.Commons.getMapFromObject import ni.devotion.floaty_head.utils.Constants.KEY_BUTTON import ni.devotion.floaty_head.utils.Constants.KEY_BUTTON_POSITION import ni.devotion.floaty_head.utils.Constants.KEY_DECORATION import ni.devotion.floaty_head.utils.Constants.KEY_PADDING import ni.devotion.floaty_head.utils.Constants.KEY_SUBTITLE import ni.devotion.floaty_head.utils.Constants.KEY_TITLE import ni.devotion.floaty_head.utils.UiBuilder.getButtonView import ni.devotion.floaty_head.utils.UiBuilder.getDecoration import ni.devotion.floaty_head.utils.UiBuilder.getGradientDrawable import ni.devotion.floaty_head.utils.UiBuilder.getPadding import ni.devotion.floaty_head.utils.UiBuilder.getTextView class HeaderView(private val context: Context, private val headerMap: Map) { val relativeView: RelativeLayout get() { val relativeLayout = RelativeLayout(context) relativeLayout.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT) val decoration = getDecoration(context, headerMap[KEY_DECORATION]) if (decoration != null) { val gd = getGradientDrawable(decoration) relativeLayout.background = gd } else { relativeLayout.setBackgroundColor(Color.WHITE) } val titleMap = getMapFromObject(headerMap, KEY_TITLE) val subTitleMap = getMapFromObject(headerMap, KEY_SUBTITLE) val buttonMap = getMapFromObject(headerMap, KEY_BUTTON) val padding = getPadding(context, headerMap[KEY_PADDING]) relativeLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom) val isShowButton = buttonMap != null assert(titleMap != null) val textColumn = createTextColumn(titleMap, subTitleMap) if (isShowButton) { val buttonPosition = headerMap[KEY_BUTTON_POSITION] as String? val button = getButtonView(context, buttonMap) if ("leading" == buttonPosition) { relativeLayout.addView(button) relativeLayout.addView(textColumn) } else { relativeLayout.addView(textColumn) relativeLayout.addView(button) } } else { relativeLayout.addView(textColumn) } return relativeLayout } //assert titleMap != null; val view: LinearLayout get() { val linearLayout = LinearLayout(context) linearLayout.orientation = LinearLayout.HORIZONTAL val decoration = getDecoration(context, headerMap[KEY_DECORATION]) if (decoration != null) { val gd = getGradientDrawable(decoration) linearLayout.background = gd } else { linearLayout.setBackgroundColor(Color.WHITE) } linearLayout.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) val titleMap = getMapFromObject(headerMap, KEY_TITLE) val subTitleMap = getMapFromObject(headerMap, KEY_SUBTITLE) val buttonMap = getMapFromObject(headerMap, KEY_BUTTON) val padding = getPadding(context, headerMap[KEY_PADDING]) linearLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom) val isShowButton = buttonMap != null assert(titleMap != null) val textColumn = createTextColumn(titleMap, subTitleMap) if (isShowButton) { val buttonPosition = headerMap[KEY_BUTTON_POSITION] as String? val button = getButtonView(context, buttonMap) if ("leading" == buttonPosition) { linearLayout.addView(button) textColumn?.let{ linearLayout.addView(it) } } else { textColumn?.let{ val param = LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f ) it.layoutParams = param linearLayout.addView(it) } linearLayout.addView(button) } } else { linearLayout.addView(textColumn) } return linearLayout } fun createTextColumn(titleMap: Map?, subTitleMap: Map?): View? { val titleView = getTextView(context, titleMap) if (subTitleMap != null) { val linearLayout = LinearLayout(context) linearLayout.orientation = LinearLayout.VERTICAL linearLayout.addView(titleView) linearLayout.addView(getTextView(context, subTitleMap)) return linearLayout } return titleView } } ================================================ FILE: android/src/main/kotlin/ni/devotion/floaty_head/views/RowView.kt ================================================ package ni.devotion.floaty_head.views import android.content.Context import android.widget.LinearLayout import ni.devotion.floaty_head.utils.Commons.getMapFromObject import ni.devotion.floaty_head.utils.UiBuilder.getPadding import ni.devotion.floaty_head.utils.UiBuilder.getTextView class RowView(private val context: Context, private val rowMap: Map) { val view: LinearLayout get() { val linearLayout = LinearLayout(context) linearLayout.orientation = LinearLayout.HORIZONTAL linearLayout.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT) val columnsMap = rowMap["columns"] as List>? val padding = getPadding(context, getMapFromObject(rowMap, "padding")) linearLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom) if (columnsMap != null) { for (i in columnsMap.indices) { val eachColumn = columnsMap[i] val textView = getTextView(context, getMapFromObject(eachColumn, "text")) linearLayout.addView(textView) } } return linearLayout } } ================================================ FILE: android/src/main/res/drawable/gradient_bg.xml ================================================ ================================================ FILE: android/src/main/res/drawable/ic_chathead.xml ================================================ ================================================ FILE: android/src/main/res/layout/fragment_float.xml ================================================ ================================================ FILE: android/src/main/res/values/colors.xml ================================================ #71b5bd #4A777C #2e4f61 #8e4a42 #b58883 #d1b09e #00ffffff #47d500f9 #6A2ACADF #6B8BC34A #1f000000 #FFEBEE #FFCDD2 #EF9A9A #E57373 #EF5350 #F44336 #E53935 #D32F2F #C62828 #B71C1C #FF8A80 #FF5252 #FF1744 #D50000 #FCE4EC #F8BBD0 #F48FB1 #F06292 #EC407A #E91E63 #D81B60 #C2185B #AD1457 #880E4F #FF80AB #FF4081 #F50057 #C51162 #F3E5F5 #E1BEE7 #CE93D8 #BA68C8 #AB47BC #9C27B0 #8E24AA #7B1FA2 #6A1B9A #4A148C #EA80FC #E040FB #D500F9 #AA00FF #EDE7F6 #D1C4E9 #B39DDB #9575CD #7E57C2 #673AB7 #5E35B1 #512DA8 #4527A0 #311B92 #B388FF #7C4DFF #651FFF #6200EA #E8EAF6 #C5CAE9 #9FA8DA #7986CB #5C6BC0 #3F51B5 #3949AB #303F9F #283593 #1A237E #8C9EFF #536DFE #3D5AFE #304FFE #E3F2FD #BBDEFB #90CAF9 #64B5F6 #42A5F5 #2196F3 #1E88E5 #1976D2 #1565C0 #0D47A1 #82B1FF #448AFF #2979FF #2962FF #E1F5FE #B3E5FC #81D4fA #4fC3F7 #29B6FC #03A9F4 #039BE5 #0288D1 #0277BD #01579B #80D8FF #40C4FF #00B0FF #0091EA #E0F7FA #B2EBF2 #80DEEA #4DD0E1 #26C6DA #00BCD4 #00ACC1 #0097A7 #00838F #006064 #84FFFF #18FFFF #00E5FF #00B8D4 #E0F2F1 #B2DFDB #80CBC4 #4DB6AC #26A69A #009688 #00897B #00796B #00695C #004D40 #A7FFEB #64FFDA #1DE9B6 #00BFA5 #E8F5E9 #C8E6C9 #A5D6A7 #81C784 #66BB6A #4CAF50 #43A047 #388E3C #2E7D32 #1B5E20 #B9F6CA #69F0AE #00E676 #00C853 #F1F8E9 #DCEDC8 #C5E1A5 #AED581 #9CCC65 #8BC34A #7CB342 #689F38 #558B2F #33691E #CCFF90 #B2FF59 #76FF03 #64DD17 #F9FBE7 #F0F4C3 #E6EE9C #DCE775 #D4E157 #CDDC39 #C0CA33 #A4B42B #9E9D24 #827717 #F4FF81 #EEFF41 #C6FF00 #AEEA00 #FFFDE7 #FFF9C4 #FFF590 #FFF176 #FFEE58 #FFEB3B #FDD835 #FBC02D #F9A825 #F57F17 #FFFF82 #FFFF00 #FFEA00 #FFD600 #FFF8E1 #FFECB3 #FFE082 #FFD54F #FFCA28 #FFC107 #FFB300 #FFA000 #FF8F00 #FF6F00 #FFE57F #FFD740 #FFC400 #FFAB00 #FFF3E0 #FFE0B2 #FFCC80 #FFB74D #FFA726 #FF9800 #FB8C00 #F57C00 #EF6C00 #E65100 #FFD180 #FFAB40 #FF9100 #FF6D00 #FBE9A7 #FFCCBC #FFAB91 #FF8A65 #FF7043 #FF5722 #F4511E #E64A19 #D84315 #BF360C #FF9E80 #FF6E40 #FF3D00 #DD2600 #EFEBE9 #D7CCC8 #BCAAA4 #A1887F #8D6E63 #795548 #6D4C41 #5D4037 #4E342E #3E2723 #FAFAFA #F5F5F5 #EEEEEE #E0E0E0 #BDBDBD #9E9E9E #757575 #616161 #424242 #212121 #000000 #ffffff #F8F3F3 #ECEFF1 #CFD8DC #B0BBC5 #90A4AE #78909C #607D8B #546E7A #455A64 #37474F #263238 #F98866 #FF420E #80BD9E #89DA59 #66000000 ================================================ FILE: android/src/main/res/values/strings.xml ================================================ Hello blank fragment ================================================ FILE: android/src/main/res/values/styles.xml ================================================ ================================================ FILE: example/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: example/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.6.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: example/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip ================================================ FILE: example/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true android.enableR8=true ================================================ FILE: example/android/settings.gradle ================================================ include ':app' def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() def plugins = new Properties() def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } } plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() include ":$name" project(":$name").projectDir = pluginDirectory } ================================================ FILE: example/android/settings_aar.gradle ================================================ include ':app' ================================================ FILE: example/lib/main.dart ================================================ import 'dart:async'; import 'package:floaty_head/floaty_head.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; Future main() async { runApp(MaterialApp(home: Home())); } class Home extends StatefulWidget { _Home createState() => _Home(); } class _Home extends State { final FloatyHead floatyHead = FloatyHead(); final header = FloatyHeadHeader( title: FloatyHeadText( text: "Outgoing Call", fontSize: 10, textColor: Colors.black45, fontWeight: FontWeight.normal, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), ), padding: FloatyHeadPadding.setSymmetricPadding(12, 12), subTitle: FloatyHeadText( text: "8989898989", fontSize: 14, fontWeight: FontWeight.bold, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), textColor: Colors.black87, ), decoration: FloatyHeadDecoration(startColor: Colors.grey[100]), button: FloatyHeadButton( text: FloatyHeadText( fontWeight: FontWeight.bold, text: "Personal", fontSize: 10, textColor: Colors.black45, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), ), tag: "personal_btn"), ); final body = FloatyHeadBody( rows: [ EachRow( columns: [ EachColumn( text: FloatyHeadText( fontWeight: FontWeight.bold, text: "Updated body", fontSize: 12, textColor: Colors.black45, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), ), ), ], gravity: ContentGravity.center, ), EachRow(columns: [ EachColumn( text: FloatyHeadText( text: "Updated long data of the body", fontSize: 12, textColor: Colors.black87, fontWeight: FontWeight.bold, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), ), padding: FloatyHeadPadding.setSymmetricPadding(6, 8), decoration: FloatyHeadDecoration( startColor: Colors.black12, borderRadius: 25.0), margin: FloatyHeadMargin(top: 4), ), ], gravity: ContentGravity.center), EachRow( columns: [ EachColumn( text: FloatyHeadText( text: "Notes", fontSize: 10, textColor: Colors.black45, fontWeight: FontWeight.normal, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), ), ), ], gravity: ContentGravity.left, margin: FloatyHeadMargin(top: 8), ), EachRow( columns: [ EachColumn( text: FloatyHeadText( text: "Updated random notes.", fontSize: 13, textColor: Colors.black54, fontWeight: FontWeight.bold, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), ), ), ], gravity: ContentGravity.left, ), ], padding: FloatyHeadPadding(left: 16, right: 16, bottom: 12, top: 12), ); final footer = FloatyHeadFooter( buttons: [ FloatyHeadButton( text: FloatyHeadText( text: "Simple button", fontSize: 12, textColor: Color.fromRGBO(250, 139, 97, 1), padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), fontWeight: FontWeight.normal, ), tag: "simple_button", padding: FloatyHeadPadding(left: 10, right: 10, bottom: 10, top: 10), width: 0, height: FloatyHeadButton.WRAP_CONTENT, decoration: FloatyHeadDecoration( startColor: Colors.white, endColor: Colors.white, borderWidth: 0, borderRadius: 0.0), ), FloatyHeadButton( text: FloatyHeadText( fontWeight: FontWeight.normal, padding: FloatyHeadPadding( bottom: 4, left: 5, right: 5, top: 5, ), text: "Focus button", fontSize: 12, textColor: Colors.white, ), tag: "focus_button", width: 0, padding: FloatyHeadPadding(left: 10, right: 10, bottom: 10, top: 10), height: FloatyHeadButton.WRAP_CONTENT, decoration: FloatyHeadDecoration( startColor: Color.fromRGBO(250, 139, 97, 1), endColor: Color.fromRGBO(247, 28, 88, 1), borderWidth: 0, borderRadius: 30.0), ) ], padding: FloatyHeadPadding(left: 16, right: 16, bottom: 12), decoration: FloatyHeadDecoration(startColor: Colors.white), buttonsPosition: ButtonPosition.center, ); bool alternateColor = false; @override void initState() { super.initState(); FloatyHead.registerOnClickListener(callBack); } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: Text('Floaty Chathead')), body: SingleChildScrollView( padding: EdgeInsets.all(50), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ElevatedButton( child: Text('Open Floaty Chathead'), onPressed: () => floatyHead.openBubble()), ElevatedButton( child: Text('Close Floaty Chathead'), onPressed: () => closeFloatyHead()), ElevatedButton( child: Text('Set icon Floaty Chathead'), onPressed: () => setIcon()), ElevatedButton( child: Text('Set close icon Floaty Chathead'), onPressed: () => setCloseIcon()), ElevatedButton( child: Text('Set close background Icon Floaty Chathead'), onPressed: () => setCloseIconBackground()), ElevatedButton( child: Text( 'Set notification title to: OH MY GOD! THEY KILL KENNY!!! Floaty Chathead'), onPressed: () => setNotificationTitle()), ElevatedButton( child: Text('Set notification Icon Floaty Chathead'), onPressed: () => setNotificationIcon()), ElevatedButton( child: Text('Set Custom Header into Floaty Chathead'), onPressed: () => setCustomHeader()), ], ), ), ); void setCustomHeader() { floatyHead.updateFloatyHeadContent( header: header, body: body, footer: footer, ); } void closeFloatyHead() { if (floatyHead.isOpen) { floatyHead.closeHead(); } } Future setNotificationTitle() async { String result; try { result = await floatyHead .setNotificationTitle("OH MY GOD! THEY KILL KENNY!!!"); } on PlatformException { result = 'Failed to get icon.'; } print('result: $result'); if (!mounted) return; } Future setNotificationIcon() async { String result; String assetPath = "assets/notificationIcon.png"; try { result = await floatyHead.setNotificationIcon(assetPath); print(result); } on PlatformException { result = 'Failed to get icon.'; print("failed: $result"); } if (!mounted) return; } Future setIcon() async { String result; String assetPath = "assets/chatheadIcon.png"; try { result = await floatyHead.setIcon(assetPath); print('result: $result'); } on PlatformException { result = 'Failed to get icon.'; } if (!mounted) return; } Future setCloseIcon() async { String assetPath = "assets/close.png"; try { await floatyHead.setCloseIcon(assetPath); } on PlatformException { return; } if (!mounted) return; } Future setCloseIconBackground() async { String assetPath = "assets/closeBg.png"; try { await floatyHead.setCloseBackgroundIcon(assetPath); } on PlatformException { return; } if (!mounted) return; } } void callBack(String tag) { print('CALLBACK FROM FRAGMENT BUILDED: $tag'); switch (tag) { case "simple_button": case "updated_simple_button": break; case "focus_button": print("Focus button has been called"); break; default: print("OnClick event of $tag"); } } ================================================ FILE: example/pubspec.yaml ================================================ name: floaty_head_example description: Demonstrates how to use the floaty_head plugin. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: sdk: ">=2.1.0 <3.0.0" dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.3 dev_dependencies: flutter_test: sdk: flutter floaty_head: path: ../ # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg assets: - assets/chatheadIcon.png - assets/close.png - assets/closeBg.png - assets/notificationIcon.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: example/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:floaty_head_example/main.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(Home()); // Verify that platform version is retrieved. expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data.startsWith('Running on:'), ), findsOneWidget, ); }); } ================================================ FILE: floaty_head.iml ================================================ ================================================ FILE: lib/floaty_head.dart ================================================ import 'dart:async'; import 'dart:io'; import 'dart:ui'; export 'models/floaty_head_body.dart'; export 'models/floaty_head_button.dart'; export 'models/floaty_head_decoration.dart'; export 'models/floaty_head_footer.dart'; export 'models/floaty_head_header.dart'; export 'models/floaty_head_margin.dart'; export 'models/floaty_head_padding.dart'; export 'models/floaty_head_text.dart'; export 'utils/commons.dart'; import 'package:floaty_head/models/floaty_head_body.dart'; import 'package:floaty_head/models/floaty_head_footer.dart'; import 'package:floaty_head/models/floaty_head_header.dart'; import 'package:floaty_head/models/floaty_head_margin.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; /// Set the [gravity] orientation for the header of the chathead /// /// use [top] to position the content of the header /// to the upper side of the container. /// /// use [bottom] to position the content of the header /// to the bottom side of the container. /// /// use [center] to position the content of the header /// to the bottom side of the container. enum FloatyHeadGravity { top, bottom, center, } /// Set the [gravity] orientation for the body of the chathead /// /// use [left] to position the content of the body /// to the Start side of the container. /// /// use [right] to position the content of the body /// to the End side of the container. /// /// use [center] to position the content of the body /// to the center of the container. enum ContentGravity { left, right, center, } /// Set the [position] for the buttons of the chathead /// /// use [trailing] to position the button /// at the End of the container. /// /// use [leading] to position the button /// at the Start of the container. /// /// use [center] to position the button /// at the center of the container. enum ButtonPosition { trailing, leading, center, } /// Set the [Weight] for the text inside the chathead /// /// use [normal] for w500 font. /// /// use [bold] for w900 font. /// /// use [italic] for a stylished font. /// /// use [bold_italic] for a w900 font with stylished. enum FontWeight { normal, bold, italic, bold_italic, } /// This is called when a button is tapped, the return is gonna be. /// ```dart /// OnClickListener(String tag) => 'btn_ok'; /// ``` typedef void OnClickListener(String tag); class FloatyHead { bool _isOpen = false; Timer? _callback; late Timer _timer; /// Return the [state] of the chathead /// ```dart /// bool get isOpen => true or false; /// ``` bool get isOpen => _isOpen; /// The timer is used when an action is needed to perform after x time has passed. Timer? get callback => _callback; static const _platform = const MethodChannel('ni.devotion/floaty_head'); FloatyHead() { if (!Platform.isAndroid) throw PlatformException(code: 'Floaty Head only available for Android'); } /// Start the chathead. /// also check constantly the current state of it. /// /// for that please use the method [isOpen]. void openBubble() async { _platform.invokeMethod('start'); _timer = Timer.periodic(Duration(seconds: 1), (timer) async { _isOpen = await _platform.invokeMethod('isOpen') ?? false; if (!_isOpen) { timer.cancel(); } }); } /// If a [widget] is [pressed] check the [type] of [tap]. /// and returns to the client-dart the component that has been pressed as a /// [string] with his tag. static Future registerOnClickListener( OnClickListener callBackFunction) async { final callBackDispatcher = PluginUtilities.getCallbackHandle(callbackDispatcher)!; final callBack = PluginUtilities.getCallbackHandle(callBackFunction)!; _platform.setMethodCallHandler((MethodCall call) { switch (call.method) { case "callBack": dynamic arguments = call.arguments; if (arguments is List) { final type = arguments[0]; if (type == "onClick") { final tag = arguments[1]; callBackFunction(tag); } } } return null; } as Future Function(MethodCall)?); await _platform.invokeMethod("registerCallBackHandler", [callBackDispatcher.toRawHandle(), callBack.toRawHandle()]); return true; } ///Set a custom [icon] for the chathead. Future setIcon(String assetPath) async { final int result = await (_platform.invokeMethod('setIcon', assetPath) as FutureOr); return result > 0 ? "Icon set" : "There was an error."; } ///Set a custom [Title] to be displayed in the notification bar for the chathead. Future setNotificationTitle(String title) async { final int result = await (_platform.invokeMethod( 'setNotificationTitle', title) as FutureOr); return result > 0 ? "Notification Title set" : "There was an error."; } /// Set a custom [IconTitle] to be displayed in the notification bar for the chathead. /// Please note that in some cases, this is gonna ignore any asset given, and instead /// use the default icon launcher. Future setNotificationIcon(String assetPath) async { final int result = await (_platform.invokeMethod( 'setNotificationIcon', assetPath) as FutureOr); return result > 0 ? "NotificationIcon set" : "There was an error."; } /// Set a custom [Close Icon] to be displayed when the chathead is dragged. Future setCloseIcon(String assetPath) async { final int result = await (_platform.invokeMethod('setCloseIcon', assetPath) as FutureOr); return result > 0 ? "Close Icon set" : "There was an error."; } /// Set a custom [Close Background] to be displayed behind the [Close Icon]. Future setCloseBackgroundIcon(String assetPath) async { final int result = await (_platform.invokeMethod( 'setBackgroundCloseIcon', assetPath) as FutureOr); return result > 0 ? "Close Icon Background set" : "There was an error."; } /// Close the [chathead]. void closeHead() { if (_isOpen) { _platform.invokeMethod('close'); _timer.cancel(); _isOpen = false; } else throw Exception('Floaty Head not running'); } /// This functions updates all the UI that is builded in the custom layout /// that the chathead uses. Future updateFloatyHeadContent({ required FloatyHeadHeader header, FloatyHeadBody? body, FloatyHeadFooter? footer, FloatyHeadMargin? margin, int? width, int? height, }) async { final Map params = { 'header': header.getMap(), 'body': body?.getMap(), 'footer': footer?.getMap(), 'margin': margin?.getMap(), 'gravity': 1.0, 'width': width ?? -1, 'height': height ?? -2 }; return await _platform.invokeMethod('setFloatyHeadContent', params); } } /// Notify to the sender/caller that a widget has been pressed. void callbackDispatcher() { const MethodChannel _backgroundChannel = const MethodChannel('ni.devotion.floaty_head/background'); WidgetsFlutterBinding.ensureInitialized(); _backgroundChannel.setMethodCallHandler((MethodCall call) async { final args = call.arguments; final Function callback = PluginUtilities.getCallbackFromHandle( CallbackHandle.fromRawHandle(args[0]))!; final type = args[1]; if (type == "onClick") { final tag = args[2]; callback(tag); } }); } ================================================ FILE: lib/models/floaty_head_body.dart ================================================ import 'package:floaty_head/floaty_head.dart'; /// This class is used to build the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadBody { List? rows; FloatyHeadPadding? padding; FloatyHeadDecoration? decoration; ///[FloatyHeadBody] currently accepts multiple rows, padding and decoration. ///in case of a new components. /// ///ex: columns ///please add it to this segment. FloatyHeadBody({ this.rows, this.padding, this.decoration, }); /// Map the data obtained from the dart-client. Map getMap() { final Map map = { 'rows': (rows == null) ? null : List.from(rows!.map((x) => x.getMap())), 'padding': padding?.getMap(), 'decoration': decoration?.getMap() }; return map; } } /// This class is used to build the [Row Content] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class EachRow { List? columns; FloatyHeadPadding? padding; FloatyHeadMargin? margin; ContentGravity? gravity; FloatyHeadDecoration? decoration; EachRow({ this.columns, this.padding, this.margin, this.gravity, this.decoration, }); Map getMap() { final Map map = { 'columns': (columns == null) ? null : List.from(columns!.map((x) => x.getMap())), 'padding': padding?.getMap(), 'margin': margin?.getMap(), 'gravity': Commons.getContentGravity(gravity), 'decoration': decoration?.getMap(), }; return map; } } /// This class is used to build the [Column Content] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class EachColumn { FloatyHeadText? text; FloatyHeadPadding? padding; FloatyHeadMargin? margin; FloatyHeadDecoration? decoration; EachColumn({ this.text, this.padding, this.margin, this.decoration, }); Map getMap() { final Map map = { 'text': text?.getMap(), 'padding': padding?.getMap(), 'margin': margin?.getMap(), 'decoration': decoration?.getMap() }; return map; } } ================================================ FILE: lib/models/floaty_head_button.dart ================================================ import 'package:floaty_head/floaty_head.dart'; /// This class is used to build the [Buttons] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadButton { static const int MATCH_PARENT = -1; static const int WRAP_CONTENT = -2; FloatyHeadText text; FloatyHeadPadding? padding; FloatyHeadMargin? margin; FloatyHeadDecoration? decoration; int? width; int? height; String tag; FloatyHeadButton({ required this.text, required this.tag, this.padding, this.margin, this.width, this.height, this.decoration, }); Map getMap() { final Map map = { 'text': text.getMap(), 'tag': tag, 'padding': padding?.getMap(), 'margin': margin?.getMap(), 'width': width ?? WRAP_CONTENT, 'height': height ?? WRAP_CONTENT, 'decoration': decoration?.getMap() }; return map; } } ================================================ FILE: lib/models/floaty_head_decoration.dart ================================================ import 'package:flutter/material.dart'; /// This class is used to build the [Decoration] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadDecoration { Color? startColor; Color? endColor; int? borderWidth; double? borderRadius; Color? borderColor; FloatyHeadDecoration({ this.startColor, this.endColor, this.borderWidth, this.borderRadius, this.borderColor, }); Map getMap() { final Map map = { 'startColor': startColor?.value ?? Colors.white.value, 'endColor': endColor?.value, 'borderWidth': borderWidth ?? 0, 'borderRadius': borderRadius ?? 0.0, 'borderColor': borderColor?.value ?? Colors.white.value }; return map; } } ================================================ FILE: lib/models/floaty_head_footer.dart ================================================ import 'package:floaty_head/floaty_head.dart'; /// This class is used to build the [Footer Content] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadFooter { FloatyHeadText? text; FloatyHeadPadding? padding; List? buttons; ButtonPosition? buttonsPosition; FloatyHeadDecoration? decoration; FloatyHeadFooter({ this.text, this.padding, this.buttons, this.buttonsPosition, this.decoration, }); Map getMap() { final Map map = { 'isShowFooter': (text != null || (buttons != null && buttons!.length > 0)), 'text': text?.getMap(), 'buttons': (buttons == null) ? null : List>.from( buttons!.map((button) => button.getMap())), 'buttonsPosition': Commons.getPosition(buttonsPosition), 'padding': padding?.getMap(), 'decoration': decoration?.getMap() }; return map; } } ================================================ FILE: lib/models/floaty_head_header.dart ================================================ import 'package:floaty_head/floaty_head.dart'; import 'package:flutter/material.dart'; /// This class is used to build the [Header Content] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadHeader { @required FloatyHeadText? title; FloatyHeadText? subTitle; FloatyHeadButton? button; ButtonPosition? buttonsPosition; FloatyHeadPadding? padding; FloatyHeadDecoration? decoration; FloatyHeadHeader({ this.title, this.subTitle, this.button, this.buttonsPosition, this.padding, this.decoration, }); Map getMap() { final Map map = { 'title': title?.getMap(), 'subTitle': subTitle?.getMap(), 'button': button?.getMap(), 'padding': padding?.getMap(), 'buttonPosition': Commons.getPosition(buttonsPosition), 'decoration': decoration?.getMap() }; return map; } } ================================================ FILE: lib/models/floaty_head_margin.dart ================================================ /// This class is used to build the [Margin] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadMargin { int? left; int? right; int? top; int? bottom; FloatyHeadMargin({this.left, this.right, this.top, this.bottom}); Map getMap() { final Map map = { 'left': left ?? 0, 'right': right ?? 0, 'top': top ?? 0, 'bottom': bottom ?? 0, }; return map; } static FloatyHeadMargin setSymmetricMargin(int vertical, int horizontal) { return FloatyHeadMargin( left: horizontal, right: horizontal, top: vertical, bottom: vertical, ); } } ================================================ FILE: lib/models/floaty_head_padding.dart ================================================ /// This class is used to build the [Padding] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadPadding { int? left; int? right; int? top; int? bottom; FloatyHeadPadding({this.left, this.right, this.top, this.bottom}); Map getMap() { final Map map = { 'left': left ?? 0, 'right': right ?? 0, 'top': top ?? 0, 'bottom': bottom ?? 0, }; return map; } static FloatyHeadPadding setSymmetricPadding(int vertical, int horizontal) { return FloatyHeadPadding( left: horizontal, right: horizontal, top: vertical, bottom: vertical, ); } } ================================================ FILE: lib/models/floaty_head_text.dart ================================================ import 'package:floaty_head/floaty_head.dart'; import 'package:flutter/material.dart'; /// This class is used to build any [Text] inside the [Body] that is gonna be displayed /// when the chathead is tapped. class FloatyHeadText { String text; double fontSize; Color textColor; FontWeight fontWeight; FloatyHeadPadding padding; FloatyHeadText( {required this.text, /*required*/ required this.fontSize, /*required*/ required this.fontWeight, /*required*/ required this.textColor, /*required*/ required this.padding}); Map getMap() { final Map map = { 'text': text, 'fontSize': fontSize, 'fontWeight': Commons.getFontWeight(fontWeight), 'textColor': textColor.value, 'padding': padding.getMap(), }; return map; } } ================================================ FILE: lib/utils/commons.dart ================================================ import 'package:floaty_head/floaty_head.dart'; class Commons { /// Replace the [windowGravity] setted in dart-client code. static String getWindowGravity(FloatyHeadGravity? gravity) { if (gravity == null) gravity = FloatyHeadGravity.top; switch (gravity) { case FloatyHeadGravity.center: return "center"; case FloatyHeadGravity.bottom: return "bottom"; case FloatyHeadGravity.top: default: return "top"; } } /// Replace the [contentGravity] setted in dart-client code. static String getContentGravity(ContentGravity? gravity) { if (gravity == null) gravity = ContentGravity.left; switch (gravity) { case ContentGravity.center: return "center"; case ContentGravity.right: return "right"; case ContentGravity.left: default: return "left"; } } /// Replace the [position] setted in dart-client code. static String getPosition(ButtonPosition? buttonPosition) { if (buttonPosition == null) buttonPosition = ButtonPosition.center; switch (buttonPosition) { case ButtonPosition.leading: return "leading"; case ButtonPosition.trailing: return "trailing"; case ButtonPosition.center: default: return "center"; } } /// Replace the [fontWeight] setted in dart-client code. static String getFontWeight(FontWeight? fontWeight) { if (fontWeight == null) fontWeight = FontWeight.normal; switch (fontWeight) { case FontWeight.bold: return "bold"; case FontWeight.italic: return "italic"; case FontWeight.bold_italic: return "bold_italic"; case FontWeight.normal: default: return "normal"; } } } ================================================ FILE: pubspec.yaml ================================================ name: floaty_head description: A flutter plugin to create custom chatheads with hidden content displayed on tap, like Messenger. version: 2.0.0-nullsafety.0 homepage: https://github.com/Crdzbird/floaty_chathead environment: sdk: '>=2.12.0 <3.0.0' flutter: ">=1.20.0" dependencies: flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter: plugin: platforms: android: package: ni.devotion.floaty_head pluginClass: FloatyHeadPlugin ios: pluginClass: FloatyHeadPlugin ================================================ FILE: test/floaty_head_test.dart ================================================ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const MethodChannel channel = MethodChannel('floaty_head'); TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return '42'; }); }); tearDown(() { channel.setMockMethodCallHandler(null); }); }