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
================================================
.*:id
http://schemas.android.com/apk/res/android
.*:name
http://schemas.android.com/apk/res/android
.*
http://schemas.android.com/apk/res/android
ANDROID_ATTRIBUTE_ORDER
================================================
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
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)
[](https://pub.dev/packages/floaty_chatheads)
[](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
================================================
.*:id
http://schemas.android.com/apk/res/android
.*:name
http://schemas.android.com/apk/res/android
.*
http://schemas.android.com/apk/res/android
ANDROID_ATTRIBUTE_ORDER
================================================
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/.gitignore
================================================
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
================================================
FILE: example/.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: app
================================================
FILE: example/README.md
================================================
# floaty_head_example
Demonstrates how to use the floaty_head plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
================================================
FILE: example/android/.gitignore
================================================
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
================================================
FILE: example/android/app/build.gradle
================================================
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 29
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "ni.devotion.floaty_head_example"
minSdkVersion 16
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
================================================
FILE: example/android/app/src/debug/AndroidManifest.xml
================================================
================================================
FILE: example/android/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: example/android/app/src/main/kotlin/ni/devotion/floaty_head_example/Application.kt
================================================
package ni.devotion.floaty_head_example;
import androidx.annotation.NonNull
import ni.devotion.floaty_head.FloatyHeadPlugin
import ni.devotion.floaty_head.utils.Managment
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.app.FlutterApplication
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback
import io.flutter.embedding.engine.FlutterEngine
/**
* For the [Application] class also use the
* ```kotlin
* Managment.pluginRegistrantC = this
* ```
* to set the callback between Kotlin/Dart.
*/
class Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
FloatyHeadPlugin().setPluginRegistrant(this)
Managment.pluginRegistrantC = this
}
override fun registerWith(registry: PluginRegistry) {
FloatyHeadPlugin().registerWith(registry.registrarFor(this.packageName))
}
}
================================================
FILE: example/android/app/src/main/kotlin/ni/devotion/floaty_head_example/MainActivity.kt
================================================
package ni.devotion.floaty_head_example;
import android.os.Bundle;
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
/**
* When using this plugin please set the [Application().onCreate()]
* if this isn't setted the chathead cannot communicate with the dart-client code
*/
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
Application().onCreate()
}
}
================================================
FILE: example/android/app/src/main/res/drawable/launch_background.xml
================================================
================================================
FILE: example/android/app/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);
});
}