diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..757c789ab --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: AppIntro diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1190f4acb..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,30 +0,0 @@ - - - -**AppIntro Version**: - - -**Device/Android Version**: - - -**Issue details / Repro steps / Use case background**: - - -**Your Code**: - - -**Stack trace / LogCat**: -```ruby -paste stack trace and/or log here -``` - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..03f2ffb83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report for an issue or a bug with AppIntro +--- + + + +## 🐛 Describe the bug + + +## ⚠️ Current behavior + + +## ✅ Expected behavior + + +## 💣 Steps to reproduce + + +## 📷 Screenshots + + +## 📑 Your Code + + + + + + +## 📱 Tech info + - AppIntro Version: + - Device: + - Android OS Version: \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..6b1902e65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions + url: https://github.com/AppIntro/AppIntro/discussions/new + about: Ask questions about AppIntro and get support for your problems. + - name: Slack Chat + url: https://kotlinlang.slack.com/archives/C019SH1RMBN + about: Join the 'appintro' channel on KotlinLang Slack to chat with other AppIntro developers. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..958d339f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,15 @@ +--- +name: Feature request +about: Suggest an idea or a new feature for AppIntro +--- + +## ⚠️ Is your feature request related to a problem? Please describe + + +## 💡 Describe the solution you'd like + + +## 🤚 Do you want to develop this feature yourself? + +- [ ] Yes +- [ ] No diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index c1f10567e..f12fd6368 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -13,6 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout latest code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v3 diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index 22544204a..4095ac3f9 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -2,42 +2,105 @@ name: Pre Merge Checks on: push: branches: - - '*' + - 'main' pull_request: branches: - '*' jobs: - gradle: - runs-on: [ubuntu-latest] + ktlint: + runs-on: 'ubuntu-latest' + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false steps: - name: Checkout Repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Cache Gradle Caches - uses: actions/cache@v1 - with: - path: ~/.gradle/caches/ - key: cache-clean-gradle-${{ matrix.os }}-${{ matrix.jdk }} - - name: Cache Gradle Wrapper - uses: actions/cache@v1 + - name: Setup Java + uses: actions/setup-java@v4 with: - path: ~/.gradle/wrapper/ - key: cache-clean-wrapper-${{ matrix.os }}-${{ matrix.jdk }} + distribution: 'zulu' + java-version: '17' + cache: 'gradle' - name: Run ktlint run: ./gradlew ktlintCheck + detekt: + runs-on: 'ubuntu-latest' + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + - name: Run detekt run: ./gradlew detekt + lint: + runs-on: 'ubuntu-latest' + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + + - name: Run lint + run: ./gradlew lint + + test: + runs-on: 'ubuntu-latest' + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + - name: Run all the tests run: ./gradlew test - - name: Build everything - run: ./gradlew build + build-debug-apk: + runs-on: 'ubuntu-latest' + name: Build Debug APK + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + + - name: Build the Debug APK + run: ./gradlew assembleDebug - # We stop gradle at the end to make sure the cache folders - # don't contain any lock files and are free to be cached. - - name: Stop Gradle - run: ./gradlew --stop + - uses: actions/upload-artifact@v4 + with: + name: appintro-sample-app-${{ matrix.name }}.apk + path: example/build/outputs/apk/debug/example-debug.apk diff --git a/.gitignore b/.gitignore index 7b4b2e364..3f428279e 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,8 @@ captures/ !/example/example-release-v7.apk !/example/proguard-rules.pro .idea/ + +.DS_Store + +# Kotlin +.kotlin \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a281c7870..ee32fd3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,142 @@ # Change Log +## Version 7.0.0 *(YYYY-MM-DD)* +This is a new major release of AppIntro. Please note that this release contains multiple new features (see below), several bugfixes, as well as multiple breaking changes. + +### Summary of Changes +* [#1112] We migrated AppIntro from ViewPager to ViewPager2 + +### Breaking Changes +* We removed `setScrollDurationFactor` since customizing scroll duration is supported anymore on ViewPager2 +* `setCustomTransformer` accepts a `ViewPager2.PageTransformer` instead of `ViewPager.PageTransformer` + +## Version 6.3.1 *(2023-07-24)* +This release of AppIntro is identical to 6.3.0. An outdated JitPack configuration caused 6.3.0 not to build correctly. + +## Version 6.3.0 *(2023-07-23)* +This is a new minor release of AppIntro. This library comes with several new features (see below) and bugfixes. + +### Summary of Changes +* We deprecated `setScrollDurationFactor` since customizing scroll duration will not be supported anymore in upcoming releases of AppIntro based on ViewPager2 +* Target SDK is now 33 + +### Enhancements 🎁 +* [#1030] AppIntro now internally uses Gradle KTS and Version Catalog +* [#1080] Add ability to change done button background color +* [#1049] Handle onBackPressed deprecation +* [#1051] Register callback on onBackPressedDispatcher + +### Bugfixes 🐛 +* [#1002] Fix RTL bug on wrongly retained currentItem +* [#1108] Fix RTL detection +* [#1109] Fix unexpected crash when using custom layouts with wrong ids with Parallax effect + +### Dependency updates 📦 +* Kotlin to 1.9.0 +* AGP to 8.0.2 +* AppCompat to 1.6.1 +* ConstraintLayout to 2.1.4 + +## Version 6.2.0 *(2022-01-17)* + +This is a new minor release of AppIntro. This library comes with several new features (see below) and bugfixes. + +### Summary of Changes + +* We deprecated `AppIntroFragment.newInstance` in favor of `AppIntroFragment.createInstance`. This was needed in order +to support passing color resources instead of color int, to tackle scenarios such as configuration changes and dark mode [#979]. +* Target SDK is now 31 +* We exposed a couple of properties/methods on the public API that were requested by the community ([#960] and [#959]) +* We added some Java examples for our Java users [#953] + +### Enhancements 🎁 + +* [#959] Add @JvmOverloads on goToNextSlide +* [#960] Expose a protected property for slidesNumber +* [#979] Fix #926: Add color resource parameters +* [#993] Make description scrollable + +### Bugfixes 🐛 + +* [#934] Fix ProgressIndicatorController in RTL + +### Dependency updates 📦 + +* Java version to 11 +* Kotlin to 1.6.10 +* AGP to 7.0.3 +* Androidx Appcompat to 1.4.0 +* ConstraintLayout to 2.1.2 + +## Version 6.1.0 *(2021-02-03)* + +This is a new minor release of AppIntro. This library comes with several new features (see below) and several bugfixes. + +### Summary of Changes + +* Target SDK is now 30. +* Text visualization has been improved with Autosizing TextViews and URL autolinking. +* AppIntro now offers better support for tablets (`sw600dp`). +* Slide indicator has been improved with better color blending and it won't be shown if you have only one slide. +* The AppIntro sample app has been completely rewritten with more examples (for Java, SlidePolicy and more). +* 14 translations have been added or improved. + +### Enhancements 🎁 + +* [#922] Add support for Autosizing TextViews +* [#887] Added start/stop animation for `Animatable` images +* [#883] Changing Visibility of Progress Bar when count of slide is one +* [#878] Add sw600dp support +* [#876] Single slide indicator condition +* [#870] added URL autolinking in appintro_fragment_intro.xml +* [#869] Allow to change skip and next arrow button color +* [#857] Function for setting text appearance of Done and Skip buttons +* [#825] Added ability to color back arrow and done button +* [#796] Extend visibility of goToPreviousSlide to protected + +### Bugfixes 🐛 + +* [#903] Deprecate the setNextPageSwipeLock method +* [#896] Fix for crash on PagerAdapter when using SlidePolicy +* [#891] Do not show back arrow on wizard mode on first slide +* [#856] Fix blending of Indicator Colors +* [#821] Fix bug with SetSwipeLock being reset to false +* [#817] enabling backward sliding when slide policy not met +* [#808] Fix isButtonsEnabled being reset to true after onCreate +* [#802] Fix goToNextSlide default param for RTL +* [#792] Remove reference to mainView in AppIntroBaseFragment +* [#791] Clearing main layout in onDestroyView + +### Translations 🌍 + +* [#919] Romanian (RO) translation +* [#908] Added Bulgarian translation +* [#895] Azerbaijan language support +* [#880] add Ukrainian translation +* [#877] Tamil translation added +* [#874] added translation for bengali +* [#872] added translation for yoruba +* [#868] Translation for Gujarati language +* [#862] Add Japanese translation +* [#839] Complete the Chinese (Simplified) translation +* [#834] Russian localization +* [#831] Finish the French translation +* [#829] Update Persian localization +* [#812] Update strings.xml Spanish + +### Dependency updates 📦 + +* Kotlin to 1.4.21 +* AGP to 4.1.1 +* Androidx Appcompat to 1.2.0 +* Constraintlayout to 2.0.4 + +### Credits + +This release was possible thanks to the contribution of: + +[@CristianCardosoA](https://github.com/CristianCardosoA) [@Debanshu777](https://github.com/Debanshu777) [@Debanshu777](https://github.com/Debanshu777) [@Hoossayn](https://github.com/Hoossayn) [@Hoossayn](https://github.com/Hoossayn) [@JekRock](https://github.com/JekRock) [@LinX64](https://github.com/LinX64) [@NikolaGanchev](https://github.com/NikolaGanchev) [@RobertPal95](https://github.com/RobertPal95) [@WWCheng02](https://github.com/WWCheng02) [@ZakCodes](https://github.com/ZakCodes) [@beefsausage](https://github.com/beefsausage) [@ch22843](https://github.com/ch22843) [@cortinico](https://github.com/cortinico) [@ghost](https://github.com/ghost) [@idish](https://github.com/idish) [@kunal-ch](https://github.com/kunal-ch) [@manimaran96](https://github.com/manimaran96) [@paolorotolo](https://github.com/paolorotolo) [@siper](https://github.com/siper) [@sr01](https://github.com/sr01) [@tsumuchan](https://github.com/tsumuchan) + ## Version 6.0.0 *(2020-05-04)* This is a new major release of AppIntro. Please note that this release contains multiple new features (see below), several bugfixes, as well as multiple breaking changes. @@ -58,7 +195,7 @@ To get a deeper overview of the breaking changes, please read the [migration doc ### Library Internals ⚙️ -* [#774] Move from Travis to Github Actions +* [#774] Move from Travis to GitHub Actions * [#768] Refactor Transformers to use a sealed class * [#766] Add missing @Res annotations * [#765] Remove `I` prefix from interface names @@ -128,10 +265,9 @@ This release was possible thanks to the contribution of: Previous release notes can be found here: [releases] -[releases]: https://github.com/AppIntro/AppIntro/releases?after=v5.0.0 -[#597]: https://github.com/AppIntro/AppIntro/pull/597 -[#590]: https://github.com/AppIntro/AppIntro/pull/590 [#574]: https://github.com/AppIntro/AppIntro/pull/574 +[#590]: https://github.com/AppIntro/AppIntro/pull/590 +[#597]: https://github.com/AppIntro/AppIntro/pull/597 [#600]: https://github.com/AppIntro/AppIntro/pull/600 [#601]: https://github.com/AppIntro/AppIntro/pull/601 [#602]: https://github.com/AppIntro/AppIntro/pull/602 @@ -202,4 +338,53 @@ Previous release notes can be found here: [releases] [#770]: https://github.com/AppIntro/AppIntro/pull/770 [#773]: https://github.com/AppIntro/AppIntro/pull/773 [#774]: https://github.com/AppIntro/AppIntro/pull/774 +[#791]: https://github.com/AppIntro/AppIntro/pull/791 +[#792]: https://github.com/AppIntro/AppIntro/pull/792 +[#796]: https://github.com/AppIntro/AppIntro/pull/796 +[#802]: https://github.com/AppIntro/AppIntro/pull/802 +[#808]: https://github.com/AppIntro/AppIntro/pull/808 +[#812]: https://github.com/AppIntro/AppIntro/pull/812 +[#817]: https://github.com/AppIntro/AppIntro/pull/817 +[#821]: https://github.com/AppIntro/AppIntro/pull/821 +[#825]: https://github.com/AppIntro/AppIntro/pull/825 +[#829]: https://github.com/AppIntro/AppIntro/pull/829 +[#831]: https://github.com/AppIntro/AppIntro/pull/831 +[#834]: https://github.com/AppIntro/AppIntro/pull/834 +[#839]: https://github.com/AppIntro/AppIntro/pull/839 +[#856]: https://github.com/AppIntro/AppIntro/pull/856 +[#857]: https://github.com/AppIntro/AppIntro/pull/857 +[#862]: https://github.com/AppIntro/AppIntro/pull/862 +[#868]: https://github.com/AppIntro/AppIntro/pull/868 +[#869]: https://github.com/AppIntro/AppIntro/pull/869 +[#870]: https://github.com/AppIntro/AppIntro/pull/870 +[#872]: https://github.com/AppIntro/AppIntro/pull/872 +[#874]: https://github.com/AppIntro/AppIntro/pull/874 +[#876]: https://github.com/AppIntro/AppIntro/pull/876 +[#877]: https://github.com/AppIntro/AppIntro/pull/877 +[#878]: https://github.com/AppIntro/AppIntro/pull/878 +[#880]: https://github.com/AppIntro/AppIntro/pull/880 +[#883]: https://github.com/AppIntro/AppIntro/pull/883 +[#887]: https://github.com/AppIntro/AppIntro/pull/887 +[#891]: https://github.com/AppIntro/AppIntro/pull/891 +[#895]: https://github.com/AppIntro/AppIntro/pull/895 +[#896]: https://github.com/AppIntro/AppIntro/pull/896 +[#903]: https://github.com/AppIntro/AppIntro/pull/903 +[#908]: https://github.com/AppIntro/AppIntro/pull/908 +[#919]: https://github.com/AppIntro/AppIntro/pull/919 +[#922]: https://github.com/AppIntro/AppIntro/pull/922 +[#934]: https://github.com/AppIntro/AppIntro/pull/934 +[#953]: https://github.com/AppIntro/AppIntro/pull/953 +[#959]: https://github.com/AppIntro/AppIntro/pull/959 +[#960]: https://github.com/AppIntro/AppIntro/pull/960 +[#979]: https://github.com/AppIntro/AppIntro/pull/979 +[#993]: https://github.com/AppIntro/AppIntro/pull/993 +[#1030]: https://github.com/AppIntro/AppIntro/pull/1030 +[#1080]: https://github.com/AppIntro/AppIntro/pull/1080 +[#1049]: https://github.com/AppIntro/AppIntro/pull/1049 +[#1051]: https://github.com/AppIntro/AppIntro/pull/1051 +[#1002]: https://github.com/AppIntro/AppIntro/pull/1002 +[#1108]: https://github.com/AppIntro/AppIntro/pull/1108 +[#1109]: https://github.com/AppIntro/AppIntro/pull/1109 +[#1112]: https://github.com/AppIntro/AppIntro/pull/1112 +[releases]: https://github.com/AppIntro/AppIntro/releases?after=v5.0.0 diff --git a/README.md b/README.md index 36ba0c4f2..5f2c98c88 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AppIntro -[![](https://jitpack.io/v/AppIntro/AppIntro.svg)](https://jitpack.io/#AppIntro/appintro) [![Pre Merge Checks](https://github.com/AppIntro/AppIntro/workflows/Pre%20Merge%20Checks/badge.svg)](https://github.com/AppIntro/AppIntro/actions?query=workflow%3A%22Pre+Merge+Checks%22) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AppIntro-green.svg?style=flat)](https://android-arsenal.com/details/1/1939) [![Join the chat at https://gitter.im/AppIntro/Lobby](https://badges.gitter.im/AppIntro/Lobby.svg)](https://gitter.im/AppIntro/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) +[![](https://jitpack.io/v/AppIntro/AppIntro.svg)](https://jitpack.io/#AppIntro/appintro) [![Join the chat at https://kotlinlang.slack.com](https://img.shields.io/badge/slack-@kotlinlang/appintro-yellow.svg?logo=slack)](https://kotlinlang.slack.com/archives/C019SH1RMBN) [![Pre Merge Checks](https://github.com/AppIntro/AppIntro/workflows/Pre%20Merge%20Checks/badge.svg)](https://github.com/AppIntro/AppIntro/actions?query=workflow%3A%22Pre+Merge+Checks%22) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) AppIntro is an Android Library that helps you build a **cool carousel intro** for your App. AppIntro has support for **requesting permissions** and helps you create a great onboarding experience in just a couple of minutes. @@ -11,6 +11,7 @@ AppIntro is an Android Library that helps you build a **cool carousel intro** fo * [Getting Started 👣](#getting-started-) * [Adding a dependency](#adding-a-dependency) * [Basic usage](#basic-usage) + * [Java users](#java-users) * [Migrating 🚗](#migrating-) * [Features 🧰](#features-) * [Creating Slides 👩‍🎨](#creating-slides-) @@ -27,6 +28,7 @@ AppIntro is an Android Library that helps you build a **cool carousel intro** fo * [Immersive Mode](#immersive-mode) * [System Back button](#system-back-button) * [System UI (Status Bar and Navigation Bar)](#system-ui-status-bar-and-navigation-bar) + * [Bottom Bar Margin](#bottom-bar-margin) * [Permission 🔒](#permission-) * [Slide Policy](#slide-policy) * [Example App 💡](#example-app-) @@ -57,7 +59,7 @@ repositories { ```groovy dependencies { // AndroidX Capable version - implementation 'com.github.AppIntro:AppIntro:6.0.0' + implementation 'com.github.AppIntro:AppIntro:6.3.1' // *** OR *** @@ -80,11 +82,11 @@ class MyCustomAppIntro : AppIntro() { // Call addSlide passing your Fragments. // You can use AppIntroFragment to use a pre-built fragment - addSlide(AppIntroFragment.newInstance( + addSlide(AppIntroFragment.createInstance( title = "Welcome...", description = "This is the first slide of the example" )) - addSlide(AppIntroFragment.newInstance( + addSlide(AppIntroFragment.createInstance( title = "...Let's get started!", description = "This is the last slide, I won't annoy you more :)" )) @@ -106,6 +108,8 @@ class MyCustomAppIntro : AppIntro() { Please note that you **must NOT call** setContentView. The `AppIntro` superclass is taking care of it for you. +Also confirm that you're overriding `onCreate` with **a single parameter** (`Bundle`) and you're not using another override (like `onCreate(Bundle, PersistableBundle)`) instead. + Finally, declare the activity in your Manifest like so: ``` xml @@ -115,6 +119,10 @@ Finally, declare the activity in your Manifest like so: We suggest to don't declare `MyCustomAppIntro` as your first Activity unless you want the intro to launch every time your app starts. Ideally you should show the AppIntro activity only once to the user, and you should hide it once completed (you can use a flag in the `SharedPreferences`). +### Java users + +You can find many examples in java language in the [examples directory](example/src/main/java/com/github/appintro/example/ui/java/JavaIntro.java) + ## Migrating 🚗 If you're migrating **from AppIntro v5.x to v6.x**, please expect multiple breaking changes. You can find documentation on how to update your code on this other [migration guide](/docs/migrating-from-5.0.md). @@ -143,14 +151,14 @@ You can use the `AppIntroFragment` if you just want to customize title, descript That's the suggested approach if you want to create a quick intro: ```kotlin -addSlide(AppIntroFragment.newInstance( +addSlide(AppIntroFragment.createInstance( title = "The title of your slide", description = "A description that will be shown on the bottom", imageDrawable = R.drawable.the_central_icon, backgroundDrawable = R.drawable.the_background_image, - titleColor = Color.YELLOW, - descriptionColor = Color.RED, - backgroundColor = Color.BLUE, + titleColorRes = R.color.yellow, + descriptionColorRes = R.color.red, + backgroundColorRes = R.color.blue, titleTypefaceFontRes = R.font.opensans_regular, descriptionTypefaceFontRes = R.font.opensans_regular, )) @@ -159,7 +167,7 @@ addSlide(AppIntroFragment.newInstance( All the parameters are optional, so you're free to customize your slide as you wish. If you need to programmatically create several slides you can also use the `SliderPage` class. -This class can be passed to `AppIntroFragment.newInstance(sliderPage: SliderPage)` that will create +This class can be passed to `AppIntroFragment.createInstance(sliderPage: SliderPage)` that will create a new slide starting from that instance. ### `AppIntroCustomLayoutFragment` @@ -185,14 +193,11 @@ AppIntro offers several configuration option to help you customize your onboardi AppIntro comes with a set of _Slide Transformer_ that you can use out of the box to animate your Slide transition. -| Name | Preview | -| ---: | :-----: | -| Fade | fade | -| Zoom | zoom | -| Flow | flow | -| Slide Over | slideover | -| Depth | depth | -| Parallax | parallax | +| Slide Transformers | Slide Transformers | +| :---: | :---: | +| Fade
fade | Zoom
zoom | +| Flow
flow | Slide Over
slideover | +| Depth
depth | Parallax
parallax | You can simply call `setTransformer()` and pass one of the subclass of the sealed class `AppIntroPageTransformerType`: @@ -316,8 +321,8 @@ vibrateDuration = 50L appintro wizard1 appintro wizard2

-AppIntro supports a _wizards_ mode where the Skip button will be replaced with the back arrow. -This comes handy if you're presenting a Wizard to your user with a set of skip they need to do, +AppIntro supports a _Wizard_ mode where the Skip button will be replaced with the back arrow. +This comes handy if you're presenting a Wizard to your users with a set of steps they need to do, and they might frequently go back and forth. You can enable it with: @@ -332,8 +337,8 @@ isWizardMode = true appintro immersive1 appintro immersive2

-If you want to display your Intro with a fullscreen experience, you can enable the _Immersive mode_. This will -hide both the Status Bar and the Navigation bar and the user will have to scroll from the top of the screen to +If you want to display your Intro with a fullscreen experience, you can enable the _Immersive_ mode. This will +hide both the _Status Bar_ and the _Navigation Bar_ and the user will have to scroll from the top of the screen to show them again. This allows you to have more space for your Intro content and graphics. @@ -346,7 +351,7 @@ setImmersiveMode() ### System Back button -You can lock the System Back button if you don't want your user go bo back from intro. +You can lock the System Back button if you don't want your user to go back from intro. This could be useful if you need to request permission and the Intro experience is not optional. If this is the case, please set to true the following flag: @@ -375,6 +380,17 @@ setNavBarColor(Color.RED) setNavBarColorRes(R.color.red) ``` +### Bottom Bar Margin + +By default, the slides use the whole size available in the screen, so it might happen that the +bottom bar overlaps the content of the slide if you have set the background color to a +non-transparent one. +If you want to make sure that the bar doesn't overlap the content, use the `setBarMargin` function +as follows: +```kotlin +setBarMargin(true) +``` + ## Permission 🔒

@@ -392,16 +408,19 @@ Please note that: ```kotlin // Ask for required CAMERA permission on the second slide. askForPermissions( - permissions = arrayOf(Manifest.permission.CAMER), + permissions = arrayOf(Manifest.permission.CAMERA), slideNumber = 2, required = true) // Ask for both optional ACCESS_FINE_LOCATION and WRITE_EXTERNAL_STORAGE // permission on the third slide. askForPermissions( - permissions = arrayOf(Manifest.permission.CAMER), - slideNumber = 2, - required = true) + permissions = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + slideNumber = 3, + required = false) ``` Should you need further control on the permission request, you can override those two methods on the `AppIntro` class: @@ -439,9 +458,13 @@ class MyFragment : Fragment(), SlidePolicy { } ``` +You can find a full working example of `SlidePolicy` in the [example app - CustomSlidePolicyFragment.kt](example/src/main/java/com/github/appintro/example/ui/custom/fragments/CustomSlidePolicyFragment.kt) + ## Example App 💡 -AppIntro comes with a **sample app** full of examples and use case that you can use as inspiration for your project. You can find it inside the [/example folder](https://github.com/AppIntro/AppIntro/tree/master/example). +AppIntro comes with a **sample app** full of examples and use case that you can use as inspiration for your project. You can find it inside the [/example folder](https://github.com/AppIntro/AppIntro/tree/main/example). + +You can get a **debug APK** of the sample app from the **Pre Merge** GitHub Actions job as an [output artifact here](https://github.com/AppIntro/AppIntro/actions?query=workflow%3A%22Pre+Merge+Checks%22).

appintro sample app @@ -473,7 +496,7 @@ If a translation in your language is already available, please check it and even ## Snapshots 📦 -Development of AppIntro happens on the [master](https://github.com/AppIntro/AppIntro/tree/master) branch. You can get `SNAPSHOT` versions directly from JitPack if needed. +Development of AppIntro happens on the [main](https://github.com/AppIntro/AppIntro/tree/main) branch. You can get `SNAPSHOT` versions directly from JitPack if needed. ```gradle repositories { @@ -483,7 +506,7 @@ repositories { ```gradle dependencies { - implementation "com.github.AppIntro:AppIntro:master-SNAPSHOT" + implementation "com.github.AppIntro:AppIntro:main-SNAPSHOT" } ``` @@ -491,7 +514,7 @@ dependencies { ## Contributing 🤝 -We're offering support for [AppIntro on Gitter](https://gitter.im/AppIntro/Lobby). Come and join the conversation over there. +We're offering support for AppIntro on the [#appintro channel on KotlinLang Slack](https://kotlinlang.slack.com/archives/C019SH1RMBN). Come and join the conversation over there. If you don't have access to KotlinLang Slack, you can [request access here](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). **We're looking for contributors! Don't be shy.** 👍 Feel free to open issues/pull requests to help me improve this project. @@ -502,7 +525,7 @@ We're offering support for [AppIntro on Gitter](https://gitter.im/AppIntro/Lobby ### Maintainers -AppIntro is currently developed and maintained by the [AppIntro Github Org](https://github.com/AppIntro). When submitting a new PR, please ping one of: +AppIntro is currently developed and maintained by the [AppIntro GitHub Org](https://github.com/AppIntro). When submitting a new PR, please ping one of: - [@paolorotolo](https://github.com/paolorotolo) - [@cortinico](https://github.com/cortinico) @@ -540,30 +563,18 @@ If you are using AppIntro in your app and would like to be listed here, please o

List of Apps using AppIntro -* [Audio Reminder Pro](https://play.google.com/store/apps/details?id=com.brandon.audioreminderpro) -* [Wizr Daily Quotes](https://play.google.com/store/apps/details?id=com.wizrapp) -* [Planets](https://play.google.com/store/apps/details?id=com.andrewq.planets) -* [PDF Me](https://play.google.com/store/apps/details?id=com.pdfme) * [Smoothie Recipes](https://play.google.com/store/apps/details?id=com.skykonig.smoothierecipes) * [neutriNote](https://play.google.com/store/apps/details?id=com.appmindlab.nano) * [Handwriting Note](https://play.google.com/store/apps/details?id=com.lyk.immersivenote) -* [Friends Roulette](https://play.google.com/store/apps/details?id=com.crioltech.roulette) * [ChineseDictionary (粵韻漢典離線粵語普通話發聲中文字典)](https://play.google.com/store/apps/details?id=com.jonasng.chinesedictionary) -* [Sifter](https://play.google.com/store/apps/details?id=sifter.social.network.archaeologist) -* [Service Notes](https://play.google.com/store/apps/details?id=notes.service.com.servicenotes) * [Salary Barometer](https://play.google.com/store/apps/details?id=anaware.salarybarometer) * [Best Business Idea!](https://play.google.com/store/apps/details?id=anaware.bestidea) * [Wi-Fi password reminder](https://play.google.com/store/apps/details?id=com.rusdelphi.wifipassword) -* [Xpaper - Moto X Wallpapers](https://play.google.com/store/apps/details?id=com.dunrite.xpaper) * [Find My Parked Car](https://play.google.com/store/apps/details?id=com.ofirmiron.findmycarandroidwear) * [Vape Tool Pro](https://play.google.com/store/apps/details?id=com.stasbar.vapetoolpro) -* [sdiwi | Win your purchase!](https://play.google.com/store/apps/details?id=com.sdiwi.app) -* [Schematiskt Skolschema](https://play.google.com/store/apps/details?id=se.zinokader.schematiskt) * [Third Eye](https://play.google.com/store/apps/details?id=com.miragestacks.thirdeye) * [Web Video Cast](https://play.google.com/store/apps/details?id=com.instantbits.cast.webvideo) -* [SchoolBox](https://play.google.com/store/apps/details?id=com.deenysoft.schoolbox) * [Fitness Challenge](https://play.google.com/store/apps/details?id=com.isidroid.fitchallenge) -* [Crunch (ICE)](https://play.google.com/store/apps/details?id=com.figsandolives.ice.free) * [Filmy - Your Movie Guide](https://play.google.com/store/apps/details?id=tech.salroid.filmy) * [HEBF Optimizer ▪ Root](https://play.google.com/store/apps/details?id=com.androidvip.hebf) * [IIFYM](https://play.google.com/store/apps/details?id=com.javierd.iifym) @@ -572,7 +583,21 @@ If you are using AppIntro in your app and would like to be listed here, please o * [Boo Music Player](https://play.google.com/store/apps/details?id=cdn.BooPlayer) * [BeatPrompter](https://play.google.com/store/apps/details?id=com.stevenfrew.beatprompter) * [BlueWords](https://play.google.com/store/apps/details?id=com.thesrb.bluewords&referrer=utm_source%3Dappintro%26utm_medium%3Dgithub%26utm_campaign%3Dreadme) -* [Best Quotes & Status 2019 (99000+ Collection)](https://play.google.com/store/apps/details?id=com.swastik.quotesandstatus&hl=en_IN) * [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) - +* [PhotoGuard Photo Vault](https://play.google.com/store/apps/details?id=com.photovault.photoguard) +* [Ride My Park - Best Spots, Skateparks Map](https://play.google.com/store/apps/details?id=com.andrieutom.rmp) +* [Shopping list](https://play.google.com/store/apps/details?id=de.vanse.shoppinglist) and [Shopping list pro](https://play.google.com/store/apps/details?id=de.vanse.shoppinglist.pro) +* [PZPIC - Pan & Zoom Effect Video from Still Picture](https://play.google.com/store/apps/details?id=com.photo3dlab.pzpic) +* [PrezziBenzina](https://play.google.com/store/apps/details?id=org.vernazza.androidfuel) +* [LogViewer for openHAB](https://github.com/cyb3rko/logviewer-for-openhab-app) +* [Firmo con CIE](https://play.google.com/store/apps/details?id=com.cyberneid.disigoncie) +* [iC-YOURLS](https://play.google.com/store/apps/details?id=net.inscomers.yourls) +* [Noon Happen](https://play.google.com/store/apps/details?id=com.noonhappen.noonhappen) +* [Weather Forecast](https://play.google.com/store/apps/details?id=com.ehlb.weatherapp) +* [Zoned Pomodoro Timer](https://play.google.com/store/apps/details?id=com.just.five) +* [PCAPdroid Network Monitor](https://play.google.com/store/apps/details?id=com.emanuelef.remote_capture) +* [EQtive](https://play.google.com/store/apps/details?id=com.vekanto.eqtive) +* [PocketMark - MarkDown Editor](https://play.google.com/store/apps/details?id=com.ZetaDev.PocketMark) +* [AIRSOFT SPOTTER](https://play.google.com/store/apps/details?id=io.github.precisionmarks.spotter) +
diff --git a/appintro/build.gradle b/appintro/build.gradle deleted file mode 100644 index c05af0723..000000000 --- a/appintro/build.gradle +++ /dev/null @@ -1,65 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'io.gitlab.arturbosch.detekt' -apply plugin: 'org.jlleitschuh.gradle.ktlint' - -group = 'com.github.AppIntro' - -android { - compileSdkVersion 29 - - buildToolsVersion "29.0.2" - defaultConfig { - minSdkVersion 14 - targetSdkVersion 29 - versionCode 20 - versionName "6.0.0" - - consumerProguardFiles 'consumer-proguard-rules.pro' - vectorDrawables.useSupportLibrary = true - } -} - -dependencies { - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.annotation:annotation:1.1.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - testImplementation 'junit:junit:4.13' - testImplementation 'org.mockito:mockito-core:2.28.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' -} - -repositories { - mavenCentral() -} - -ktlint { - version = "0.36.0" - debug = false - verbose = true - android = false - outputToConsole = true - reporters { - reporter "json" - } - ignoreFailures = false - enableExperimentalRules = true - kotlinScriptAdditionalPaths { - include fileTree("scripts/") - } - filter { - exclude("**/generated/**") - include("**/kotlin/**") - } -} - -detekt { - config = files("./detekt-config.yml") - input = files("src/main/java") -} - -tasks.withType(io.gitlab.arturbosch.detekt.Detekt) { - exclude("**/resources/**,**/build/**") -} diff --git a/appintro/build.gradle.kts b/appintro/build.gradle.kts new file mode 100644 index 000000000..6910f8cf3 --- /dev/null +++ b/appintro/build.gradle.kts @@ -0,0 +1,116 @@ +plugins { + alias(libs.plugins.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.detekt) + alias(libs.plugins.ktlint) + id("maven-publish") +} + +group = "com.github.AppIntro" +version = "7.0.0-beta02" + +android { + namespace = "com.github.appintro" + compileSdk = libs.versions.compile.sdk.version.get().toInt() + + defaultConfig { + minSdk = libs.versions.min.sdk.version.get().toInt() + + consumerProguardFiles("consumer-proguard-rules.pro") + vectorDrawables.useSupportLibrary = true + } + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } + lint { + warningsAsErrors = true + abortOnError = true + lint { + disable.addAll( + listOf( + "MissingTranslation", + "OldTargetApi", + "GradleDependency", + ), + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.annotation) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.viewpager2) + implementation(libs.androidx.fragment) + + testImplementation(libs.junit) + testImplementation(libs.mockito.core) +} + +ktlint { + debug.set(false) + verbose.set(true) + android.set(false) + outputToConsole.set(true) + ignoreFailures.set(false) +} + +detekt { + config.setFrom(files("./detekt-config.yml")) +} + +val pomName: String by project +val pomDescription: String by project +val pomLicenseName: String by project +val pomLicenseUrl: String by project +val pomScmConnection: String by project +val pomUrl: String by project + +publishing { + publications { + register("release") { + afterEvaluate { + from(components["release"]) + } + pom { + name.set(pomName) + description.set(pomDescription) + url.set(pomUrl) + licenses { + license { + name.set(pomLicenseName) + url.set(pomLicenseUrl) + } + } + scm { + connection.set(pomScmConnection) + developerConnection.set(pomScmConnection) + url.set(pomUrl) + } + developers { + developer { + id.set("paolorotolo") + name.set("Paolo Rotolo") + email.set("paolo@rotolo.dev") + } + developer { + id.set("cortinico") + name.set("Nicola Corti") + email.set("corti.nico@gmail.com") + } + } + } + } + } +} diff --git a/appintro/detekt-config.yml b/appintro/detekt-config.yml index 93588c81d..cae1c2b9e 100644 --- a/appintro/detekt-config.yml +++ b/appintro/detekt-config.yml @@ -1,5 +1,5 @@ build: - maxIssues: 1 + maxIssues: 0 excludeCorrectable: false weights: # complexity: 2 @@ -9,52 +9,92 @@ build: config: validation: true - # when writing own rules with new properties, exclude the property path e.g.: "my_rule_set,.*>.*>[my_property]" - excludes: "" + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' processors: active: true exclude: - # - 'DetektProgressListener' + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' # - 'FunctionCountProcessor' # - 'PropertyCountProcessor' - # - 'ClassCountProcessor' - # - 'PackageCountProcessor' - # - 'KtFileCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' console-reports: active: true exclude: - # - 'ProjectStatisticsReport' - # - 'ComplexityReport' - # - 'NotificationReport' - # - 'FindingsReport' + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' - 'FileBasedFindingsReport' - # - 'BuildFailureReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' comments: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false CommentOverPrivateFunction: active: false CommentOverPrivateProperty: active: false + DeprecatedBlockTag: + active: false EndOfSentenceFormat: active: false - endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$) + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false UndocumentedPublicClass: active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] searchInNestedClass: true searchInInnerClass: true searchInInnerObject: true searchInInnerInterface: true + searchInProtectedClass: false UndocumentedPublicFunction: active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false UndocumentedPublicProperty: active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false complexity: active: true + CognitiveComplexMethod: + active: false + threshold: 15 ComplexCondition: active: true threshold: 4 @@ -62,16 +102,27 @@ complexity: active: false threshold: 10 includeStaticDeclarations: false - ComplexMethod: + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: active: true threshold: 15 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false - nestingFunctions: run,let,apply,with,also,use,forEach,isNotNull,ifNull + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' LabeledExpression: active: false - ignoredLabels: "" + ignoredLabels: [] LargeClass: active: true threshold: 600 @@ -80,29 +131,47 @@ complexity: threshold: 60 LongParameterList: active: true - threshold: 6 + functionThreshold: 6 + constructorThreshold: 7 ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] MethodOverloading: active: false threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false NestedBlockDepth: active: true threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false StringLiteralDuplication: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] thresholdInFiles: 21 thresholdInClasses: 21 - thresholdInInterfaces: 11 - thresholdInObjects: 11 - thresholdInEnums: 11 + thresholdInInterfaces: 21 + thresholdInObjects: 21 + thresholdInEnums: 21 ignoreDeprecated: false ignorePrivate: false ignoreOverridden: false @@ -111,14 +180,28 @@ coroutines: active: true GlobalCoroutineUsage: active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: active: false + SuspendFunWithFlowReturnType: + active: true empty-blocks: active: true EmptyCatchBlock: active: true - allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + allowedExceptionNameRegex: '_|(ignore|expected).*' EmptyClassBlock: active: true EmptyDefaultConstructor: @@ -133,7 +216,7 @@ empty-blocks: active: true EmptyFunctionBlock: active: true - ignoreOverridden: true + ignoreOverridden: false EmptyIfBlock: active: true EmptyInitBlock: @@ -142,6 +225,8 @@ empty-blocks: active: true EmptySecondaryConstructor: active: true + EmptyTryBlock: + active: true EmptyWhenBlock: active: true EmptyWhileBlock: @@ -150,272 +235,227 @@ empty-blocks: exceptions: active: true ExceptionRaisedInUnexpectedLocation: - active: false - methodNames: 'toString,hashCode,equals,finalize' + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' InstanceOfCheckForException: - active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] NotImplementedDeclaration: active: false - PrintStackTrace: + ObjectExtendsThrowable: active: false + PrintStackTrace: + active: true RethrowCaughtException: - active: false + active: true ReturnFromFinally: - active: false + active: true ignoreLabeled: false SwallowedException: - active: false - ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' - allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' ThrowingExceptionFromFinally: - active: false + active: true ThrowingExceptionInMain: active: false ThrowingExceptionsWithoutMessageOrCause: - active: false - exceptions: 'IllegalArgumentException,IllegalStateException,IOException' + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' ThrowingNewInstanceOfSameException: - active: false + active: true TooGenericExceptionCaught: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] exceptionNames: - - ArrayIndexOutOfBoundsException - - Error - - Exception - - IllegalMonitorStateException - - NullPointerException - - IndexOutOfBoundsException - - RuntimeException - - Throwable - allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' TooGenericExceptionThrown: active: true exceptionNames: - - Error - - Exception - - Throwable - - RuntimeException - -formatting: - active: true - android: false - autoCorrect: true - AnnotationOnSeparateLine: - active: false - autoCorrect: true - ChainWrapping: - active: true - autoCorrect: true - CommentSpacing: - active: true - autoCorrect: true - EnumEntryNameCase: - active: false - autoCorrect: true - Filename: - active: true - FinalNewline: - active: true - autoCorrect: true - ImportOrdering: - active: false - autoCorrect: true - Indentation: - active: false - autoCorrect: true - indentSize: 4 - continuationIndentSize: 4 - MaximumLineLength: - active: true - maxLineLength: 120 - ModifierOrdering: - active: true - autoCorrect: true - MultiLineIfElse: - active: true - autoCorrect: true - NoBlankLineBeforeRbrace: - active: true - autoCorrect: true - NoConsecutiveBlankLines: - active: true - autoCorrect: true - NoEmptyClassBody: - active: true - autoCorrect: true - NoEmptyFirstLineInMethodBlock: - active: false - autoCorrect: true - NoLineBreakAfterElse: - active: true - autoCorrect: true - NoLineBreakBeforeAssignment: - active: true - autoCorrect: true - NoMultipleSpaces: - active: true - autoCorrect: true - NoSemicolons: - active: true - autoCorrect: true - NoTrailingSpaces: - active: true - autoCorrect: true - NoUnitReturn: - active: true - autoCorrect: true - NoUnusedImports: - active: true - autoCorrect: true - NoWildcardImports: - active: true - PackageName: - active: true - autoCorrect: true - ParameterListWrapping: - active: true - autoCorrect: true - indentSize: 4 - SpacingAroundColon: - active: true - autoCorrect: true - SpacingAroundComma: - active: true - autoCorrect: true - SpacingAroundCurly: - active: true - autoCorrect: true - SpacingAroundDot: - active: true - autoCorrect: true - SpacingAroundKeyword: - active: true - autoCorrect: true - SpacingAroundOperators: - active: true - autoCorrect: true - SpacingAroundParens: - active: true - autoCorrect: true - SpacingAroundRangeOperator: - active: true - autoCorrect: true - StringTemplate: - active: true - autoCorrect: true + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' naming: active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' ClassNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - classPattern: '[A-Z$][a-zA-Z0-9$]*' + classPattern: '[A-Z][a-zA-Z0-9]*' ConstructorParameterNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true EnumNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - forbiddenName: '' + forbiddenName: [] FunctionMaxLength: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" maximumFunctionNameLength: 30 FunctionMinLength: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" minimumFunctionNameLength: 3 FunctionNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true FunctionParameterNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true InvalidPackageDeclaration: - active: false + active: true rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' MatchingDeclarationName: active: true + mustBeFirst: true MemberNameEqualsClassName: active: true ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false ObjectPropertyNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" constantPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' TopLevelPropertyNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" constantPattern: '[A-Z][_A-Z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" maximumVariableNameLength: 64 VariableMinLength: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" minimumVariableNameLength: 1 VariableNaming: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true performance: active: true ArrayPrimitive: active: true + CouldBeSequence: + active: false + threshold: 3 ForEachOnRange: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] SpreadOperator: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false Deprecation: active: false - DuplicateCaseInWhenExpression: + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: active: true + ExitOutsideMain: + active: false ExplicitGarbageCollectionCall: active: true HasPlatformType: - active: false + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: active: false + allowExplicitReturnType: true InvalidRange: active: true IteratorHasNextCallsNextMethod: @@ -424,80 +464,156 @@ potential-bugs: active: true LateinitUsage: active: false - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - excludeAnnotatedProperties: "" - ignoreOnClassesPattern: "" + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: - active: false - MissingWhenCase: - active: true - RedundantElseInWhen: active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false UnconditionalJumpStatementInLoop: active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true UnreachableCode: active: true UnsafeCallOnNullableType: active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] UnsafeCast: - active: false + active: true + UnusedUnaryOperator: + active: true UselessPostfixExpression: - active: false + active: true WrongEqualsTypeParameter: active: true style: active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false CollapsibleIfStatements: active: false DataClassContainsFunctions: active: false - conversionFunctionPrefix: 'to' + conversionFunctionPrefix: + - 'to' + allowOperators: false DataClassShouldBeImmutable: active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' EqualsNullCall: active: true EqualsOnSignatureLine: active: false -# ExplicitCollectionElementAccessMethod: -# active: false - ExplicitItLambdaParameter: + ExplicitCollectionElementAccessMethod: active: false + ExplicitItLambdaParameter: + active: true ExpressionBodySyntax: active: false includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' ForbiddenComment: active: true - values: 'TODO:,FIXME:,STOPSHIP:' - allowedPatterns: "" + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' ForbiddenImport: active: false - imports: '' - forbiddenPatterns: "" + imports: [] + forbiddenPatterns: '' ForbiddenMethodCall: active: false - methods: '' - ForbiddenPublicDataClass: + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: active: false - ignorePackages: '*.internal,*.internal.*' + rules: [] ForbiddenVoid: - active: false + active: true ignoreOverridden: false ignoreUsageInGenerics: false FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true - excludedFunctions: 'describeContents' - excludeAnnotatedFunction: "dagger.Provides" - LibraryCodeMustSpecifyReturnType: - active: true + ignoreActualFunction: true + excludedFunctions: [] LoopWithTooManyJumpStatements: active: true maxJumpCount: 1 MagicNumber: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - ignoreNumbers: '-1,0,1,2' + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreLocalVariableDeclaration: false @@ -507,24 +623,41 @@ style: ignoreNamedArgument: true ignoreEnums: false ignoreRanges: false - MandatoryBracesIfStatements: + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: active: false + maxChainedCalls: 5 MaxLineLength: active: true maxLineLength: 120 excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: false + excludeRawStrings: true MayBeConst: active: true ModifierOrder: active: true - NestedClassesVisibility: + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true NewLineAtEndOfFile: active: true NoTabs: active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true OptionalAbstractKeyword: active: true OptionalUnit: @@ -537,71 +670,117 @@ style: active: true RedundantExplicitType: active: false + RedundantHigherOrderMapUsage: + active: true RedundantVisibilityModifierRule: active: false ReturnCount: active: true max: 5 - excludedFunctions: "equals" + excludedFunctions: + - 'equals' excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false SafeCast: active: true SerialVersionUIDInSerializableClass: - active: false + active: true SpacingBetweenPackageAndImports: active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] ThrowsCount: active: true max: 2 + excludeGuardClauses: false TrailingWhitespace: active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' UnderscoresInNumericLiterals: active: false - acceptableDecimalLength: 5 + acceptableLength: 4 + allowNonStandardGrouping: false UnnecessaryAbstractClass: active: true - excludeAnnotatedClasses: "dagger.Module" UnnecessaryAnnotationUseSiteTarget: active: false UnnecessaryApply: + active: true + UnnecessaryBackticks: active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true UnnecessaryInheritance: active: true + UnnecessaryInnerClass: + active: false UnnecessaryLet: active: false UnnecessaryParentheses: active: false + allowForUnclearPrecedence: false UntilInsteadOfRangeTo: active: false UnusedImports: active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' UnusedPrivateClass: active: true UnusedPrivateMember: - active: false - allowedNames: "(_|ignored|expected|serialVersionUID)" + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true UseArrayLiteralsInAnnotations: - active: false + active: true + UseCheckNotNull: + active: true UseCheckOrError: - active: false + active: true UseDataClass: active: false - excludeAnnotatedClasses: "" allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false UseIfInsteadOfWhen: active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: active: false UselessCallOnNotNull: active: true UtilityClassWithPublicConstructor: active: true VarCouldBeVal: - active: false + active: true + ignoreLateinitVar: false WildcardImport: active: true - excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" - excludeImports: 'java.util.*,kotlinx.android.synthetic.*' - + excludeImports: + - 'java.util.*' diff --git a/appintro/gradle.properties b/appintro/gradle.properties index 51a4032e7..3944b82e4 100644 --- a/appintro/gradle.properties +++ b/appintro/gradle.properties @@ -1,17 +1,9 @@ -POM_NAME=AppIntro -POM_ARTIFACT_ID=appintro -POM_PACKAGING=aar -VERSION_NAME=4.1.0 -VERSION_CODE=13 -GROUP=com.github.appintro - -POM_DESCRIPTION=AppIntro library -POM_URL=https://github.com/PaoloRotolo/AppIntro -POM_SCM_URL=https://github.com/PaoloRotolo/AppIntro -POM_SCM_CONNECTION=git@github.com:PaoloRotolo/AppIntro.git -POM_SCM_DEV_CONNECTION=git@github.com:PaoloRotolo/AppIntro.git -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo -POM_DEVELOPER_ID=paolorotolo -POM_DEVELOPER_NAME=paolorotolo +pomArtifactId=appintro +pomName=AppIntro +pomDescription=Make a cool intro for your Android app. +pomUrl=git://github.com/AppIntro/AppIntro.git +pomScmUrl=https://github.com/AppIntro/AppIntro +pomScmConnection=scm:git://github.com/AppIntro/AppIntro.git +pomScmDevConnection=scm:git://github.com/AppIntro/AppIntro.git +pomLicenseName=The Apache Software License, Version 2.0 +pomLicenseUrl=http://www.apache.org/licenses/LICENSE-2.0.txt diff --git a/appintro/src/main/AndroidManifest.xml b/appintro/src/main/AndroidManifest.xml deleted file mode 100644 index 37e06c299..000000000 --- a/appintro/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/appintro/src/main/java/com/github/appintro/AppIntro.kt b/appintro/src/main/java/com/github/appintro/AppIntro.kt index 559b31002..9b116ede5 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntro.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntro.kt @@ -2,23 +2,27 @@ package com.github.appintro import android.graphics.drawable.Drawable import android.view.View +import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.FontRes import androidx.annotation.StringRes +import androidx.annotation.StyleRes +import androidx.core.widget.TextViewCompat import com.github.appintro.internal.TypefaceContainer abstract class AppIntro : AppIntroBase() { - override val layoutId = R.layout.appintro_intro_layout /** * Override viewpager bar color * @param color your color resource */ - fun setBarColor(@ColorInt color: Int) { + fun setBarColor( + @ColorInt color: Int, + ) { val bottomBar = findViewById(R.id.bottom) bottomBar.setBackgroundColor(color) } @@ -28,17 +32,33 @@ abstract class AppIntro : AppIntroBase() { * * @param color your color */ - fun setNextArrowColor(@ColorInt color: Int) { + fun setNextArrowColor( + @ColorInt color: Int, + ) { val nextButton = findViewById(R.id.next) nextButton.setColorFilter(color) } + /** + * Override back button arrow color + * + * @param color your color + */ + fun setBackArrowColor( + @ColorInt color: Int, + ) { + val backButton = findViewById(R.id.back) + backButton.setColorFilter(color) + } + /** * Override separator color * * @param color your color resource */ - fun setSeparatorColor(@ColorInt color: Int) { + fun setSeparatorColor( + @ColorInt color: Int, + ) { val separator = findViewById(R.id.bottom_separator) separator.setBackgroundColor(color) } @@ -58,7 +78,9 @@ abstract class AppIntro : AppIntroBase() { * * @param skipResId your text resource Id */ - fun setSkipText(@StringRes skipResId: Int) { + fun setSkipText( + @StringRes skipResId: Int, + ) { val skipText = findViewById(R.id.skip) skipText.setText(skipResId) } @@ -68,7 +90,9 @@ abstract class AppIntro : AppIntroBase() { * * @param typeface the typeface to apply to Skip button */ - fun setSkipTextTypeface(@FontRes typeface: Int) { + fun setSkipTextTypeface( + @FontRes typeface: Int, + ) { val view = findViewById(R.id.skip) TypefaceContainer(null, typeface).applyTo(view) } @@ -98,7 +122,9 @@ abstract class AppIntro : AppIntroBase() { * * @param doneResId your text resource Id */ - fun setDoneText(@StringRes doneResId: Int) { + fun setDoneText( + @StringRes doneResId: Int, + ) { val doneText = findViewById(R.id.done) doneText.setText(doneResId) } @@ -118,7 +144,9 @@ abstract class AppIntro : AppIntroBase() { * * @param typeface the typeface to apply to Done button */ - fun setDoneTextTypeface(@FontRes typeface: Int) { + fun setDoneTextTypeface( + @FontRes typeface: Int, + ) { val view = findViewById(R.id.done) TypefaceContainer(null, typeface).applyTo(view) } @@ -128,21 +156,49 @@ abstract class AppIntro : AppIntroBase() { * * @param colorDoneText your color resource */ - fun setColorDoneText(@ColorInt colorDoneText: Int) { + fun setColorDoneText( + @ColorInt colorDoneText: Int, + ) { val doneText = findViewById(R.id.done) doneText.setTextColor(colorDoneText) } + /** + * Override done button text overall style + * + * @param textAppearance your text style from resource + */ + fun setDoneTextAppearance( + @StyleRes textAppearance: Int, + ) { + val doneText = findViewById(R.id.done) + TextViewCompat.setTextAppearance(doneText, textAppearance) + } + /** * Override skip button color * * @param colorSkipButton your color resource */ - fun setColorSkipButton(@ColorInt colorSkipButton: Int) { + fun setColorSkipButton( + @ColorInt colorSkipButton: Int, + ) { val skip = findViewById(R.id.skip) skip.setTextColor(colorSkipButton) } + /** + * Override skip button text overall style + * + * @param textAppearance your text style from resource + */ + fun setSkipTextAppearance( + @StyleRes textAppearance: Int, + ) { + val skip = findViewById(R.id.skip) + TextViewCompat.setTextAppearance(skip, textAppearance) + } + /** * Override Next button * @@ -168,4 +224,24 @@ abstract class AppIntro : AppIntroBase() { bottomSeparator.visibility = View.INVISIBLE } } + + /** + * Adds a margin to the bottom of the content of the slide equal to the bottom bar. + * For situations where the bar is not transparent, and you want the content of the slide to + * be intractable. + * + * @param setBarMargin Set : true for margin. false for no margin. + */ + fun setBarMargin(setBarMargin: Boolean) { + val bottomBar = findViewById(R.id.pager_gesture_overlay) + val margin = + if (setBarMargin) { + resources.getDimension(R.dimen.appintro_bottombar_height).toInt() + } else { + 0 + } + (bottomBar.layoutParams as? ViewGroup.MarginLayoutParams) + ?.setMargins(0, 0, 0, margin) + ?.also { bottomBar.requestLayout() } + } } diff --git a/appintro/src/main/java/com/github/appintro/AppIntro2.kt b/appintro/src/main/java/com/github/appintro/AppIntro2.kt index 7f13d40ab..93474404f 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntro2.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntro2.kt @@ -1,19 +1,17 @@ package com.github.appintro import android.graphics.drawable.Drawable -import android.os.Build import android.os.Bundle import android.view.View import android.widget.ImageButton import androidx.annotation.ColorInt -import androidx.annotation.IdRes +import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.ConstraintLayout abstract class AppIntro2 : AppIntroBase() { - override val layoutId = R.layout.appintro_intro_layout2 - @IdRes + @DrawableRes var backgroundResource: Int? = null set(value) { field = value @@ -26,9 +24,7 @@ abstract class AppIntro2 : AppIntroBase() { set(value) { field = value if (field != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - backgroundFrame.background = field - } + backgroundFrame.background = field } } @@ -50,7 +46,9 @@ abstract class AppIntro2 : AppIntroBase() { * Override viewpager bar color * @param color your color resource */ - fun setBarColor(@ColorInt color: Int) { + fun setBarColor( + @ColorInt color: Int, + ) { bottomBar.setBackgroundColor(color) } @@ -61,4 +59,38 @@ abstract class AppIntro2 : AppIntroBase() { fun setImageSkipButton(imageSkipButton: Drawable) { skipImageButton.setImageDrawable(imageSkipButton) } + + /** + * Override next button arrow color + * + * @param color your color + */ + fun setNextArrowColor( + @ColorInt color: Int, + ) { + val nextButton = findViewById(R.id.next) + nextButton.setColorFilter(color) + } + + /** + * Override skip button color + * + * @param colorSkipButton your color resource + */ + fun setSkipArrowColor( + @ColorInt colorSkipButton: Int, + ) { + val skip = findViewById(R.id.skip) + skip.setColorFilter(colorSkipButton) + } + + /** + * Override done button drawable resource + * + * @param imageDoneButton your drawable resource + */ + fun setImageDoneButton(imageDoneButton: Drawable) { + val done = findViewById(R.id.done) + done.setImageDrawable(imageDoneButton) + } } diff --git a/appintro/src/main/java/com/github/appintro/AppIntroBase.kt b/appintro/src/main/java/com/github/appintro/AppIntroBase.kt index 093a9f502..f26134bd5 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntroBase.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntroBase.kt @@ -3,18 +3,15 @@ package com.github.appintro import android.animation.ArgbEvaluator -import android.annotation.SuppressLint -import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.os.Vibrator import android.view.KeyEvent import android.view.View import android.view.ViewGroup import android.view.Window -import android.view.WindowManager import android.widget.ImageButton +import androidx.activity.OnBackPressedCallback import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.LayoutRes @@ -23,15 +20,19 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.widget.TooltipCompat.setTooltipText import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat.Type.systemBars +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 import com.github.appintro.indicator.DotIndicatorController import com.github.appintro.indicator.IndicatorController import com.github.appintro.indicator.ProgressIndicatorController -import com.github.appintro.internal.AppIntroViewPager +import com.github.appintro.internal.AppIntroViewPagerController import com.github.appintro.internal.LayoutUtil import com.github.appintro.internal.LogHelper import com.github.appintro.internal.PermissionWrapper +import com.github.appintro.internal.VibrationHelper import com.github.appintro.internal.viewpager.PagerAdapter import com.github.appintro.internal.viewpager.ViewPagerTransformer @@ -40,7 +41,6 @@ import com.github.appintro.internal.viewpager.ViewPagerTransformer * the lifecycle and all the event callbacks for AppIntro. */ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { - /** The layout ID that will be used during inflation. */ @get:LayoutRes protected abstract val layoutId: Int @@ -99,10 +99,15 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { * */ protected var isVibrate = false + /** + * Read-only property with the total number of slides for this AppIntro. + */ + protected val totalSlidesNumber: Int get() = slidesNumber + // Private Fields private lateinit var pagerAdapter: PagerAdapter - private lateinit var pager: AppIntroViewPager + private lateinit var pagerController: AppIntroViewPagerController private var slidesNumber: Int = 0 private var savedCurrentItem: Int = 0 private var currentlySelectedItem = -1 @@ -116,19 +121,17 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { // Asks the ViewPager for the current slide number. Useful to query the [permissionsMap] private val currentSlideNumber: Int - get() = pager.getCurrentSlideNumber(fragments.size) + get() = pagerController.getCurrentSlideNumber(fragments.size) /** HashMap that contains the [PermissionWrapper] objects */ private var permissionsMap = HashMap() - private var retainIsButtonEnabled = true + private var retainIsButtonsEnabled = true - // Android SDK - private lateinit var vibrator: Vibrator private val argbEvaluator = ArgbEvaluator() internal val isRtl: Boolean - get() = LayoutUtil.isRtl(applicationContext) + get() = LayoutUtil.isRtl(this) /* PUBLIC API @@ -141,13 +144,14 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { protected fun addSlide(fragment: Fragment) { if (isRtl) { fragments.add(0, fragment) + pagerAdapter.notifyItemInserted(0) } else { fragments.add(fragment) + pagerAdapter.notifyItemInserted(fragments.size) } if (isWizardMode) { - pager.offscreenPageLimit = fragments.size + pagerController.setOffscreenPageLimit(fragments.size) } - pagerAdapter.notifyDataSetChanged() } /** @@ -158,7 +162,11 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { * @param required - Whether the user can change this slide without granting the permissions. */ @JvmOverloads - protected fun askForPermissions(permissions: Array, slideNumber: Int, required: Boolean = true) { + protected fun askForPermissions( + permissions: Array, + slideNumber: Int, + required: Boolean = true, + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (slideNumber <= 0) { error("Invalid Slide Number: $slideNumber") @@ -169,91 +177,86 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { } /** Moves the AppIntro to the previous slide */ - private fun goToPreviousSlide() { - pager.goToPreviousSlide() + protected fun goToPreviousSlide() { + pagerController.goToPreviousSlide() } /** Moves the AppIntro to the next slide */ - protected fun goToNextSlide(isLastSlide: Boolean = pager.currentItem + 1 == slidesNumber) { + @JvmOverloads + protected fun goToNextSlide(isLastSlide: Boolean = pagerController.isLastSlide(fragments.size)) { if (isLastSlide) { onIntroFinished() } else { - pager.goToNextSlide() + pagerController.goToNextSlide() onNextSlide() } } /** Enable the Immersive Sticky Mode */ protected fun setImmersiveMode() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - window.decorView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN - ) + WindowCompat.getInsetsController(window, window.decorView).apply { + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(systemBars()) } } /** Customize the color of the Status Bar */ - protected fun setStatusBarColor(@ColorInt color: Int) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - window.statusBarColor = color - } + protected fun setStatusBarColor( + @ColorInt color: Int, + ) { + // We set the light status bar/translucent first via the WindowInsetsControllerCompat + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true + window.statusBarColor = color } /** Customize the color of the Status Bar */ - protected fun setStatusBarColorRes(@ColorRes color: Int) { + protected fun setStatusBarColorRes( + @ColorRes color: Int, + ) { setStatusBarColor(ContextCompat.getColor(this, color)) } /** Customize the color of the Navigation Bar */ - protected fun setNavBarColor(@ColorInt color: Int) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.navigationBarColor = color - } + protected fun setNavBarColor( + @ColorInt color: Int, + ) { + window.navigationBarColor = color } /** Customize the color of the Navigation Bar */ - protected fun setNavBarColorRes(@ColorRes color: Int) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.navigationBarColor = ContextCompat.getColor(this, color) - } + protected fun setNavBarColorRes( + @ColorRes color: Int, + ) { + window.navigationBarColor = ContextCompat.getColor(this, color) } /** Toggle the Status Bar visibility */ protected fun showStatusBar(show: Boolean) { + val controller = WindowCompat.getInsetsController(window, window.decorView) if (show) { - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + controller.show(systemBars()) } else { - window.setFlags( - WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN - ) + controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + controller.hide(systemBars()) } } /** - * Setting to disable forward swiping right on current page and allow swiping left. If a swipe - * left occurs, the lock state is reset and swiping is re-enabled. (one shot disable) This also - * hides/shows the Next and Done buttons accordingly. - * * @param lock Set true to disable forward swiping. False to enable. + * @deprecated setNextPageSwipeLock has been deprecated in favor of setSwipeLock or SlidePolicy */ + @Deprecated( + "setNextPageSwipeLock has been deprecated in favor of setSwipeLock or SlidePolicy", + ReplaceWith("setSwipeLock"), + DeprecationLevel.ERROR, + ) + @Suppress("UnusedPrivateMember", "UNUSED_PARAMETER") protected fun setNextPageSwipeLock(lock: Boolean) { - // We retain the button state in order to be able to restore - // it properly afterwards. - if (lock) { - retainIsButtonEnabled = this.isButtonsEnabled - this.isButtonsEnabled = true - } else { - this.isButtonsEnabled = retainIsButtonEnabled - } - pager.isNextPagingEnabled = !lock + LogHelper.w( + TAG, + "Calling setNextPageSwipeLock has not effect here. Please switch to setSwipeLock or SlidePolicy", + ) } /** @@ -266,12 +269,12 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { // We retain the button state in order to be able to restore // it properly afterwards. if (lock) { - retainIsButtonEnabled = this.isButtonsEnabled + retainIsButtonsEnabled = this.isButtonsEnabled this.isButtonsEnabled = true } else { - this.isButtonsEnabled = retainIsButtonEnabled + this.isButtonsEnabled = retainIsButtonsEnabled } - pager.isFullPagingEnabled = !lock + pagerController.isFullPagingEnabled = !lock } /** @@ -290,7 +293,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { */ protected fun setIndicatorColor( @ColorInt selectedIndicatorColor: Int, - @ColorInt unselectedIndicatorColor: Int + @ColorInt unselectedIndicatorColor: Int, ) { indicatorController?.selectedIndicatorColor = selectedIndicatorColor indicatorController?.unselectedIndicatorColor = unselectedIndicatorColor @@ -300,23 +303,14 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { TRANSFORMERS =================================== */ - /** - * Sets the scroll duration factor - by default it is 1. This factor will - * multiply duration - * @param factor the new factor that will be applied to the scroll - default: 1 - */ - protected fun setScrollDurationFactor(factor: Int) { - pager.setScrollDurationFactor(factor.toDouble()) - } - /** Allows to specify one of the [AppIntroPageTransformerType] for the ViewPager */ protected fun setTransformer(appIntroTransformer: AppIntroPageTransformerType) { - pager.setAppIntroPageTransformer(appIntroTransformer) + pagerController.setAppIntroPageTransformer(appIntroTransformer) } /** Overrides viewpager transformer with you custom [ViewPagerTransformer] */ - protected fun setCustomTransformer(transformer: ViewPager.PageTransformer?) { - pager.setPageTransformer(true, transformer) + protected fun setCustomTransformer(transformer: ViewPager2.PageTransformer?) { + pagerController.setPageTransformer(transformer) } /* @@ -379,17 +373,24 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { * @param newFragment Instance of the fragment which is displayed now. * This might be null if the intro has finished */ - protected open fun onSlideChanged(oldFragment: Fragment?, newFragment: Fragment?) {} + protected open fun onSlideChanged( + oldFragment: Fragment?, + newFragment: Fragment?, + ) {} /* LIFECYCLE =================================== */ + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { - requestWindowFeature(Window.FEATURE_NO_TITLE) + supportRequestWindowFeature(Window.FEATURE_NO_TITLE) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) super.onCreate(savedInstanceState) + // Add back handler + addBackHandler() + // We default the indicator controller to the Dotted one. indicatorController = DotIndicatorController(this) @@ -421,24 +422,24 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { backButton.scaleX = -1f } - vibrator = this.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - - pagerAdapter = PagerAdapter(supportFragmentManager, fragments) - pager = findViewById(R.id.view_pager) + pagerAdapter = PagerAdapter(this@AppIntroBase, fragments) + pagerController = + AppIntroViewPagerController( + viewPager = findViewById(R.id.view_pager), + viewPagerGestureOverlay = findViewById(R.id.pager_gesture_overlay), + ) doneButton.setOnClickListener(NextSlideOnClickListener(isLastSlide = true)) nextButton.setOnClickListener(NextSlideOnClickListener(isLastSlide = false)) - backButton.setOnClickListener { pager.goToPreviousSlide() } + backButton.setOnClickListener { pagerController.goToPreviousSlide() } skipButton.setOnClickListener { dispatchVibration() - onSkipPressed(pagerAdapter.getItem(pager.currentItem)) + onSkipPressed(getPagerItem(pagerController.getCurrentItem())) } - pager.adapter = this.pagerAdapter - pager.addOnPageChangeListener(OnPageChangeListener()) - pager.onNextPageRequestedListener = this - - setScrollDurationFactor(DEFAULT_SCROLL_DURATION_FACTOR) + pagerController.setAdapter(this.pagerAdapter) + pagerController.registerOnPageChangeCallback(OnPageChangeCallback()) + pagerController.onNextPageRequestedListener = this } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -447,21 +448,21 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { slidesNumber = fragments.size initializeIndicator() + // Makes sure we correctly retain the `isButtonsEnabled` just after onCreate + retainIsButtonsEnabled = isButtonsEnabled + // Required for triggering onPageSelected and onSlideChanged for the first page. if (isRtl) { - pager.currentItem = fragments.size - savedCurrentItem + pagerController.setCurrentViewPagerItem(fragments.size - savedCurrentItem) } else { - pager.currentItem = savedCurrentItem + pagerController.setCurrentViewPagerItem(savedCurrentItem) } - pager.post { - val fragment = pagerAdapter.getItem(pager.currentItem) - // Fragment is null when no slides are passed to AppIntro - if (fragment != null) { + pagerController.post { + if (pagerController.getCurrentItem() < pagerAdapter.itemCount) { dispatchSlideChangedCallbacks( null, - pagerAdapter - .getItem(pager.currentItem) + getPagerItem(pagerController.getCurrentItem()), ) } else { // Close the intro if there are no slides to show @@ -474,15 +475,20 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { super.onSaveInstanceState(outState) outState.apply { putInt(ARG_BUNDLE_SLIDES_NUMBER, slidesNumber) - putBoolean(ARG_BUNDLE_RETAIN_IS_BUTTON_ENABLED, retainIsButtonEnabled) - putBoolean(ARG_BUNDLE_IS_BUTTON_ENABLED, isButtonsEnabled) + putBoolean(ARG_BUNDLE_RETAIN_IS_BUTTONS_ENABLED, retainIsButtonsEnabled) + putBoolean(ARG_BUNDLE_IS_BUTTONS_ENABLED, isButtonsEnabled) putBoolean(ARG_BUNDLE_IS_SKIP_BUTTON_ENABLED, isSkipButtonEnabled) putBoolean(ARG_BUNDLE_IS_INDICATOR_ENABLED, isIndicatorEnabled) - putInt(ARG_BUNDLE_LOCK_PAGE, pager.lockPage) - putInt(ARG_BUNDLE_CURRENT_ITEM, pager.currentItem) - putBoolean(ARG_BUNDLE_IS_FULL_PAGING_ENABLED, pager.isFullPagingEnabled) - putBoolean(ARG_BUNDLE_IS_NEXT_PAGING_ENABLED, pager.isNextPagingEnabled) + // We can't use pager.currentItem as we need the current item that is RTL-invariant. + val currentItem = + if (isRtl) { + fragments.size - pagerController.getCurrentItem() + } else { + pagerController.getCurrentItem() + } + putInt(ARG_BUNDLE_CURRENT_ITEM, currentItem) + putBoolean(ARG_BUNDLE_IS_FULL_PAGING_ENABLED, pagerController.isFullPagingEnabled) putSerializable(ARG_BUNDLE_PERMISSION_MAP, permissionsMap) @@ -494,20 +500,26 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { super.onRestoreInstanceState(savedInstanceState) with(savedInstanceState) { slidesNumber = getInt(ARG_BUNDLE_SLIDES_NUMBER) - retainIsButtonEnabled = getBoolean(ARG_BUNDLE_RETAIN_IS_BUTTON_ENABLED) - isButtonsEnabled = getBoolean(ARG_BUNDLE_IS_BUTTON_ENABLED) + retainIsButtonsEnabled = getBoolean(ARG_BUNDLE_RETAIN_IS_BUTTONS_ENABLED) + isButtonsEnabled = getBoolean(ARG_BUNDLE_IS_BUTTONS_ENABLED) isSkipButtonEnabled = getBoolean(ARG_BUNDLE_IS_SKIP_BUTTON_ENABLED) isIndicatorEnabled = getBoolean(ARG_BUNDLE_IS_INDICATOR_ENABLED) - pager.lockPage = getInt(ARG_BUNDLE_LOCK_PAGE) savedCurrentItem = getInt(ARG_BUNDLE_CURRENT_ITEM) - pager.isFullPagingEnabled = getBoolean(ARG_BUNDLE_IS_FULL_PAGING_ENABLED) - pager.isNextPagingEnabled = getBoolean(ARG_BUNDLE_IS_NEXT_PAGING_ENABLED) + pagerController.isFullPagingEnabled = getBoolean(ARG_BUNDLE_IS_FULL_PAGING_ENABLED) + + @Suppress("UNCHECKED_CAST", "DEPRECATION") + permissionsMap = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ( + getSerializable(ARG_BUNDLE_PERMISSION_MAP, HashMap::class.java) + as HashMap? + ) ?: hashMapOf() + } else { + (getSerializable(ARG_BUNDLE_PERMISSION_MAP) as HashMap?) + ?: hashMapOf() + } - permissionsMap = ( - (getSerializable(ARG_BUNDLE_PERMISSION_MAP) as HashMap?) - ?: hashMapOf() - ) isColorTransitionsEnabled = getBoolean(ARG_BUNDLE_COLOR_TRANSITIONS_ENABLED) } } @@ -518,35 +530,49 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { indicatorController?.selectPosition(currentlySelectedItem) } - override fun onKeyDown(code: Int, event: KeyEvent): Boolean { + override fun onKeyDown( + code: Int, + event: KeyEvent, + ): Boolean { // Handle the navigation with 'Enter' or Dpad events. if (code == KeyEvent.KEYCODE_ENTER || code == KeyEvent.KEYCODE_BUTTON_A || code == KeyEvent.KEYCODE_DPAD_CENTER ) { - val isLastSlide = pager.currentItem == pagerAdapter.count - 1 + val isLastSlide = pagerController.isLastSlide(fragments.size) goToNextSlide(isLastSlide) if (isLastSlide) { // We emulate the onDonePressed here to keep backward compatibility // with the previous API (users expect an onDonePressed to kill the Activity). // Ideally we should get rid of this extra callback in one of the future release. - onDonePressed(pagerAdapter.getItem(pager.currentItem)) + onDonePressed(getPagerItem(pagerController.getCurrentItem())) } return false } return super.onKeyDown(code, event) } - override fun onBackPressed() { - // Do nothing if go back lock is enabled or slide has custom policy. - if (isSystemBackButtonLocked) { - return - } - if (pager.isFirstSlide(fragments.size)) { - super.onBackPressed() - } else { - pager.goToPreviousSlide() - } + /* + BACK HANDLER + =================================== */ + + private fun addBackHandler() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // Do nothing if go back lock is enabled or slide has custom policy. + if (isSystemBackButtonLocked) { + return + } + if (pagerController.isFirstSlide(fragments.size)) { + finish() + } else { + pagerController.goToPreviousSlide() + } + } + }, + ) } /* @@ -555,13 +581,12 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { private fun updateButtonsVisibility() { if (isButtonsEnabled) { - val isLastSlide = - !isRtl && pager.currentItem == slidesNumber - 1 || - isRtl && pager.currentItem == 0 + val isLastSlide = pagerController.isLastSlide(fragments.size) + val isFirstSlide = pagerController.isFirstSlide(fragments.size) nextButton.isVisible = !isLastSlide doneButton.isVisible = isLastSlide skipButton.isVisible = isSkipButtonEnabled && !isLastSlide - backButton.isVisible = isWizardMode + backButton.isVisible = isWizardMode && !isFirstSlide } else { nextButton.isVisible = false doneButton.isVisible = false @@ -581,7 +606,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { * @return true, if the slide change should be allowed, else false */ override fun onCanRequestNextPage(): Boolean { - val currentFragment = pagerAdapter.getItem(pager.currentItem) + val currentFragment = getPagerItem(pagerController.getCurrentItem()) // Check if the current fragment implements SlidePolicy, else a change is always allowed. return if (currentFragment is SlidePolicy && !currentFragment.isPolicyRespected) { @@ -594,7 +619,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { } override fun onIllegallyRequestedNextPage() { - val currentFragment = pagerAdapter.getItem(pager.currentItem) + val currentFragment = getPagerItem(pagerController.getCurrentItem()) if (currentFragment is SlidePolicy) { if (!currentFragment.isPolicyRespected) { currentFragment.onUserIllegallyRequestedNextPage() @@ -616,7 +641,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { ActivityCompat.requestPermissions( this, it.permissions, - PERMISSIONS_REQUEST_ALL_PERMISSIONS + PERMISSIONS_REQUEST_ALL_PERMISSIONS, ) } } @@ -627,7 +652,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, - grantResults: IntArray + grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) setSwipeLock(false) @@ -636,10 +661,11 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { return } - val deniedPermissions = grantResults - .mapIndexed { index, result -> (permissions[index] to result) } - .filter { (_, result) -> result == PackageManager.PERMISSION_DENIED } - .map { (permission, _) -> permission } + val deniedPermissions = + grantResults + .mapIndexed { index, result -> (permissions[index] to result) } + .filter { (_, result) -> result == PackageManager.PERMISSION_DENIED } + .map { (permission, _) -> permission } // Check if all permissions are granted. if (deniedPermissions.isEmpty()) { @@ -651,7 +677,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { // At least one or all of the permissions have been denied. deniedPermissions.forEach(::handleDeniedPermission) // Let's force a recenter of the current slide. - pager.reCenterCurrentSlide() + pagerController.reCenterCurrentSlide() } } @@ -664,46 +690,39 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { // Permission is denied for the first time (never ask again box is not checked). // Ask again explaining the usage of the permission (Show an AlertDialog or Snackbar) onUserDeniedPermission(permission) - - // If the permission was not required, we can remove it from the App and let the user proceed. - permissionsMap[currentSlideNumber]?.let { requestedPermission -> - if (!requestedPermission.required) { - permissionsMap.remove(requestedPermission.position) - goToNextSlide() - } - } } else { // Permission is disabled (never ask again is checked) // Ask the user to go to settings to enable permission. onUserDisabledPermission(permission) } + + // If the permission was not required, we can remove it from the App and let the user proceed. + permissionsMap[currentSlideNumber]?.let { requestedPermission -> + if (!requestedPermission.required) { + permissionsMap.remove(requestedPermission.position) + goToNextSlide() + } + } } - // You must grant vibration permissions on your AndroidManifest.xml file - @SuppressLint("MissingPermission") private fun dispatchVibration() { if (isVibrate) { - vibrator.vibrate(vibrateDuration) + VibrationHelper.vibrate(this, vibrateDuration) } } /** - * Called by [ViewPager.OnPageChangeListener.onPageSelected] to tell [AppIntroViewPager] - * to request permissions on swipe. - * This method notifies [AppIntroViewPager] that the currently selected slide + * Getter used to notify [AppIntroViewPager] if the currently selected slide * has permissions attached to it. */ - private fun setPermissionSlide() { - if (pager.getCurrentSlideNumber(fragments.size) in permissionsMap) { - pager.isPermissionSlide = true - } else { - pager.isPermissionSlide = false - setSwipeLock(false) - } - } + private val isPermissionSlide: Boolean + get() = pagerController.getCurrentSlideNumber(fragments.size) in permissionsMap /** Takes care of calling all the necessary callbacks on Slide Changing. */ - private fun dispatchSlideChangedCallbacks(oldFragment: Fragment?, newFragment: Fragment?) { + private fun dispatchSlideChangedCallbacks( + oldFragment: Fragment?, + newFragment: Fragment?, + ) { if (oldFragment is SlideSelectionListener) { oldFragment.onSlideDeselected() } @@ -714,18 +733,25 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { } /** Performs color interpolation between two slides.. */ - private fun performColorTransition(currentSlide: Fragment?, nextSlide: Fragment?, positionOffset: Float) { + private fun performColorTransition( + currentSlide: Fragment?, + nextSlide: Fragment?, + positionOffset: Float, + ) { + if (nextSlide == null) return + if (currentSlide is SlideBackgroundColorHolder && nextSlide is SlideBackgroundColorHolder ) { // Check if both fragments are attached to an activity, // otherwise getDefaultBackgroundColor may fail. if (currentSlide.isAdded && nextSlide.isAdded) { - val newColor = argbEvaluator.evaluate( - positionOffset, - currentSlide.defaultBackgroundColor, - nextSlide.defaultBackgroundColor - ) as Int + val newColor = + argbEvaluator.evaluate( + positionOffset, + getSlideColor(currentSlide), + getSlideColor(nextSlide), + ) as Int currentSlide.setBackgroundColor(newColor) nextSlide.setBackgroundColor(newColor) } @@ -734,6 +760,18 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { } } + @ColorInt + @Suppress("DEPRECATION") + private fun getSlideColor(slide: SlideBackgroundColorHolder): Int { + if (slide.defaultBackgroundColorRes != 0) { + return ContextCompat.getColor(this, slide.defaultBackgroundColorRes) + } + + return slide.defaultBackgroundColor + } + + private fun getPagerItem(position: Int): Fragment? = pagerAdapter.getItem(position, supportFragmentManager) + /** * Onclick listener for the Next/Done button. * @param isLastSlide True if you're using this for the DONE button. @@ -752,7 +790,7 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { } // We can successfully change slide, let's do it. - val currentFragment = pagerAdapter.getItem(pager.currentItem) + val currentFragment = getPagerItem(pagerController.getCurrentItem()) if (isLastSlide) { onDonePressed(currentFragment) } else { @@ -763,15 +801,18 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { } /** - * [OnPageChangeListener] used to handle all the callbacks coming from the ViewPager. + * [OnPageChangeCallback] used to handle all the callbacks coming from the ViewPager. * Moreover, if [isColorTransitionsEnabled] a color interpolation will happen in the [onPageScrolled] */ - internal inner class OnPageChangeListener : ViewPager.OnPageChangeListener { - - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - if (isColorTransitionsEnabled && position < pagerAdapter.count - 1) { - val currentSlide = pagerAdapter.getItem(position) - val nextSlide = pagerAdapter.getItem(position + 1) + internal inner class OnPageChangeCallback : ViewPager2.OnPageChangeCallback() { + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int, + ) { + if (isColorTransitionsEnabled && position < pagerAdapter.itemCount - 1) { + val currentSlide = getPagerItem(position) + val nextSlide = getPagerItem(position + 1) performColorTransition(currentSlide, nextSlide, positionOffset) } } @@ -780,54 +821,43 @@ abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener { if (slidesNumber >= 1) { indicatorController?.selectPosition(position) } - - // Allow the swipe to be re-enabled if a user swipes to a previous slide. Restore - // state of progress button depending on global progress button setting - if (!pager.isNextPagingEnabled) { - if (pager.currentItem != pager.lockPage) { - isButtonsEnabled = retainIsButtonEnabled - pager.isNextPagingEnabled = true - } - } updateButtonsVisibility() - setPermissionSlide() + pagerController.isPermissionSlide = this@AppIntroBase.isPermissionSlide // Firing all the necessary Callbacks this@AppIntroBase.onPageSelected(position) if (slidesNumber > 0) { if (currentlySelectedItem == -1) { - dispatchSlideChangedCallbacks(null, pagerAdapter.getItem(position)) + dispatchSlideChangedCallbacks( + null, + getPagerItem(position), + ) } else { dispatchSlideChangedCallbacks( - pagerAdapter.getItem(currentlySelectedItem), - pagerAdapter.getItem(pager.currentItem) + getPagerItem(currentlySelectedItem), + getPagerItem(pagerController.getCurrentItem()), ) } } currentlySelectedItem = position } - - override fun onPageScrollStateChanged(state: Int) {} } private companion object { private val TAG = LogHelper.makeLogTag(AppIntroBase::class.java) - private const val DEFAULT_SCROLL_DURATION_FACTOR = 1 private const val DEFAULT_VIBRATE_DURATION = 20L private const val PERMISSIONS_REQUEST_ALL_PERMISSIONS = 1 private const val ARG_BUNDLE_COLOR_TRANSITIONS_ENABLED = "colorTransitionEnabled" private const val ARG_BUNDLE_CURRENT_ITEM = "currentItem" - private const val ARG_BUNDLE_IS_BUTTON_ENABLED = "isButtonsEnabled" + private const val ARG_BUNDLE_IS_BUTTONS_ENABLED = "isButtonsEnabled" private const val ARG_BUNDLE_IS_FULL_PAGING_ENABLED = "isFullPagingEnabled" private const val ARG_BUNDLE_IS_INDICATOR_ENABLED = "isIndicatorEnabled" - private const val ARG_BUNDLE_IS_NEXT_PAGING_ENABLED = "isNextPagingEnabled" private const val ARG_BUNDLE_IS_SKIP_BUTTON_ENABLED = "isSkipButtonsEnabled" - private const val ARG_BUNDLE_LOCK_PAGE = "lockPage" private const val ARG_BUNDLE_PERMISSION_MAP = "permissionMap" - private const val ARG_BUNDLE_RETAIN_IS_BUTTON_ENABLED = "retainIsButtonEnabled" + private const val ARG_BUNDLE_RETAIN_IS_BUTTONS_ENABLED = "retainIsButtonsEnabled" private const val ARG_BUNDLE_SLIDES_NUMBER = "slidesNumber" } } diff --git a/appintro/src/main/java/com/github/appintro/AppIntroBaseFragment.kt b/appintro/src/main/java/com/github/appintro/AppIntroBaseFragment.kt index b61cb2985..1996274fe 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntroBaseFragment.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntroBaseFragment.kt @@ -1,160 +1,186 @@ package com.github.appintro +import android.graphics.drawable.Animatable import android.os.Bundle +import android.text.method.ScrollingMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.ColorInt +import androidx.annotation.ColorRes import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import com.github.appintro.internal.LogHelper import com.github.appintro.internal.TypefaceContainer -internal const val ARG_TITLE = "title" -internal const val ARG_TITLE_TYPEFACE = "title_typeface" -internal const val ARG_TITLE_TYPEFACE_RES = "title_typeface_res" -internal const val ARG_DESC = "desc" -internal const val ARG_DESC_TYPEFACE = "desc_typeface" -internal const val ARG_DESC_TYPEFACE_RES = "desc_typeface_res" -internal const val ARG_DRAWABLE = "drawable" -internal const val ARG_BG_COLOR = "bg_color" -internal const val ARG_TITLE_COLOR = "title_color" -internal const val ARG_DESC_COLOR = "desc_color" -internal const val ARG_BG_DRAWABLE = "bg_drawable" - abstract class AppIntroBaseFragment : Fragment(), SlideSelectionListener, SlideBackgroundColorHolder { + private val viewModel: AppIntroFragmentViewModel by viewModels() private val logTAG = LogHelper.makeLogTag(AppIntroBaseFragment::class.java) @get:LayoutRes protected abstract val layoutId: Int - private var drawable: Int = 0 - private var bgDrawable: Int = 0 - - private var titleColor: Int = 0 - private var descColor: Int = 0 + @ColorInt + @Deprecated( + "`defaultBackgroundColor` has been deprecated to support configuration changes", + ReplaceWith("defaultBackgroundColorRes"), + ) final override var defaultBackgroundColor: Int = 0 private set - private var title: String? = null - private var description: String? = null - private var titleTypeface: TypefaceContainer? = null - private var descTypeface: TypefaceContainer? = null - - private var mainLayout: ConstraintLayout? = null + @ColorRes + final override var defaultBackgroundColorRes: Int = 0 + private set override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - retainInstance = true + + @Suppress("DEPRECATION") + defaultBackgroundColor = viewModel.defaultBackgroundColor ?: 0 + defaultBackgroundColorRes = viewModel.defaultBackgroundColorRes ?: 0 val args = arguments if (args != null && args.size() != 0) { - drawable = args.getInt(ARG_DRAWABLE) - title = args.getString(ARG_TITLE) - description = args.getString(ARG_DESC) - bgDrawable = args.getInt(ARG_BG_DRAWABLE) - - val argsTitleTypeface = args.getString(ARG_TITLE_TYPEFACE) - val argsDescTypeface = args.getString(ARG_DESC_TYPEFACE) - val argsTitleTypefaceRes = args.getInt(ARG_TITLE_TYPEFACE_RES) - val argsDescTypefaceRes = args.getInt(ARG_DESC_TYPEFACE_RES) - titleTypeface = TypefaceContainer(argsTitleTypeface, argsTitleTypefaceRes) - descTypeface = TypefaceContainer(argsDescTypeface, argsDescTypefaceRes) - - defaultBackgroundColor = args.getInt(ARG_BG_COLOR) - titleColor = args.getInt(ARG_TITLE_COLOR, 0) - descColor = args.getInt(ARG_DESC_COLOR, 0) - } - } + viewModel.drawable = args.getInt(ARG_DRAWABLE) + viewModel.title = args.getCharSequence(ARG_TITLE) + viewModel.description = args.getCharSequence(ARG_DESC) + + if (args.containsKey(ARG_BG_DRAWABLE)) { + viewModel.bgDrawable = args.getInt(ARG_BG_DRAWABLE) + } + + viewModel.titleTypefaceUrl = args.getString(ARG_TITLE_TYPEFACE_URL) + viewModel.titleTypefaceRes = args.getInt(ARG_TITLE_TYPEFACE_RES) + + viewModel.descTypefaceUrl = args.getString(ARG_DESC_TYPEFACE_URL) + viewModel.descTypefaceRes = args.getInt(ARG_DESC_TYPEFACE_RES) + + @Suppress("DEPRECATION") + viewModel.defaultBackgroundColor = args.getInt(ARG_BG_COLOR) + viewModel.defaultBackgroundColorRes = args.getInt(ARG_BG_COLOR_RES) + + if (args.containsKey(ARG_TITLE_COLOR)) { + viewModel.titleColor = args.getInt(ARG_TITLE_COLOR) + } + + if (args.containsKey(ARG_TITLE_COLOR_RES)) { + viewModel.titleColorRes = args.getInt(ARG_TITLE_COLOR_RES) + } + + if (args.containsKey(ARG_DESC_COLOR)) { + viewModel.descColor = args.getInt(ARG_DESC_COLOR) + } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - if (savedInstanceState != null) { - drawable = savedInstanceState.getInt(ARG_DRAWABLE) - title = savedInstanceState.getString(ARG_TITLE) - description = savedInstanceState.getString(ARG_DESC) - - titleTypeface = TypefaceContainer( - savedInstanceState.getString(ARG_TITLE_TYPEFACE), - savedInstanceState.getInt(ARG_TITLE_TYPEFACE_RES, 0) - ) - descTypeface = TypefaceContainer( - savedInstanceState.getString(ARG_DESC_TYPEFACE), - savedInstanceState.getInt(ARG_DESC_TYPEFACE_RES, 0) - ) - - defaultBackgroundColor = savedInstanceState.getInt(ARG_BG_COLOR) - bgDrawable = savedInstanceState.getInt(ARG_BG_DRAWABLE) - titleColor = savedInstanceState.getInt(ARG_TITLE_COLOR) - descColor = savedInstanceState.getInt(ARG_DESC_COLOR) + if (args.containsKey(ARG_DESC_COLOR_RES)) { + viewModel.descColorRes = args.getInt(ARG_DESC_COLOR_RES) + } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? { val view = inflater.inflate(layoutId, container, false) val titleText = view.findViewById(R.id.title) val descriptionText = view.findViewById(R.id.description) val slideImage = view.findViewById(R.id.image) - mainLayout = view.findViewById(R.id.main) + val mainLayout = view.findViewById(R.id.main) - titleText.text = title - descriptionText.text = description - if (titleColor != 0) { + titleText.text = viewModel.title + descriptionText.text = viewModel.description + + val titleColorRes = viewModel.titleColorRes + val titleColor = viewModel.titleColor + + if (titleColorRes != null) { + titleText.setTextColor(ContextCompat.getColor(requireContext(), titleColorRes)) + } else if (titleColor != null) { // Fallback to deprecated static color titleText.setTextColor(titleColor) } - if (descColor != 0) { + + val descColorRes = viewModel.descColorRes + val descColor = viewModel.descColor + + if (descColorRes != null) { + descriptionText.setTextColor(ContextCompat.getColor(requireContext(), descColorRes)) + } else if (descColor != null) { // Fallback to deprecated static color descriptionText.setTextColor(descColor) } - titleTypeface?.applyTo(titleText) - descTypeface?.applyTo(descriptionText) - - slideImage.setImageResource(drawable) - if (bgDrawable != 0) { - mainLayout?.setBackgroundResource(bgDrawable) - } else { - mainLayout?.setBackgroundColor(defaultBackgroundColor) + + TypefaceContainer( + typeFaceUrl = viewModel.titleTypefaceUrl, + typeFaceResource = viewModel.titleTypefaceRes ?: 0, + ).applyTo(titleText) + + TypefaceContainer( + typeFaceUrl = viewModel.descTypefaceUrl, + typeFaceResource = viewModel.descTypefaceRes ?: 0, + ).applyTo(descriptionText) + + viewModel.drawable?.let { + slideImage.setImageResource(it) + } + + val bgDrawable = viewModel.bgDrawable + + when { + bgDrawable != null -> { + mainLayout?.setBackgroundResource(bgDrawable) + } + defaultBackgroundColorRes != 0 -> { + mainLayout?.setBackgroundColor(ContextCompat.getColor(requireContext(), defaultBackgroundColorRes)) + } + else -> { + @Suppress("DEPRECATION") + mainLayout?.setBackgroundColor(defaultBackgroundColor) + } } + titleText.movementMethod = ScrollingMovementMethod() + descriptionText.movementMethod = ScrollingMovementMethod() + return view } - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(ARG_DRAWABLE, drawable) - outState.putInt(ARG_BG_DRAWABLE, bgDrawable) - outState.putString(ARG_TITLE, title) - outState.putString(ARG_DESC, description) - outState.putInt(ARG_BG_COLOR, defaultBackgroundColor) - outState.putInt(ARG_TITLE_COLOR, titleColor) - outState.putInt(ARG_DESC_COLOR, descColor) - if (titleTypeface != null) { - outState.putString(ARG_TITLE_TYPEFACE, titleTypeface?.typeFaceUrl) - outState.putInt(ARG_TITLE_TYPEFACE_RES, titleTypeface?.typeFaceResource ?: 0) + override fun onResume() { + super.onResume() + + view?.findViewById(R.id.image).let { + if (it is Animatable) { + it.start() + } } - if (descTypeface != null) { - outState.putString(ARG_DESC_TYPEFACE, descTypeface?.typeFaceUrl) - outState.putInt(ARG_DESC_TYPEFACE_RES, descTypeface?.typeFaceResource ?: 0) + } + + override fun onPause() { + super.onPause() + + view?.findViewById(R.id.image).let { + if (it is Animatable) { + it.start() + } } - super.onSaveInstanceState(outState) } override fun onSlideDeselected() { - LogHelper.d(logTAG, "Slide $title has been deselected.") + LogHelper.d(logTAG, "Slide ${viewModel.title} has been deselected.") } override fun onSlideSelected() { - LogHelper.d(logTAG, "Slide $title has been selected.") + LogHelper.d(logTAG, "Slide ${viewModel.title} has been selected.") } - override fun setBackgroundColor(@ColorInt backgroundColor: Int) { - mainLayout?.setBackgroundColor(backgroundColor) + override fun setBackgroundColor( + @ColorInt backgroundColor: Int, + ) { + view?.findViewById(R.id.main)?.setBackgroundColor(backgroundColor) } } diff --git a/appintro/src/main/java/com/github/appintro/AppIntroCustomLayoutFragment.kt b/appintro/src/main/java/com/github/appintro/AppIntroCustomLayoutFragment.kt index 80a04d02c..75a82b3e7 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntroCustomLayoutFragment.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntroCustomLayoutFragment.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.Fragment * to your AppIntro. */ class AppIntroCustomLayoutFragment : Fragment() { - private var layoutResId = 0 override fun onCreate(savedInstanceState: Bundle?) { @@ -25,11 +24,12 @@ class AppIntroCustomLayoutFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? = inflater.inflate(layoutResId, container, false) companion object { private const val ARG_LAYOUT_RES_ID = "layoutResId" + @JvmStatic fun newInstance(layoutResId: Int): AppIntroCustomLayoutFragment { val customSlide = AppIntroCustomLayoutFragment() diff --git a/appintro/src/main/java/com/github/appintro/AppIntroFragment.kt b/appintro/src/main/java/com/github/appintro/AppIntroFragment.kt index d83a5eeb8..3bfcf7c18 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntroFragment.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntroFragment.kt @@ -1,17 +1,16 @@ package com.github.appintro import androidx.annotation.ColorInt +import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FontRes import com.github.appintro.model.SliderPage @Suppress("LongParameterList") class AppIntroFragment : AppIntroBaseFragment() { - override val layoutId: Int get() = R.layout.appintro_fragment_intro companion object { - /** * Generates a new instance for [AppIntroFragment] * @@ -32,18 +31,27 @@ class AppIntroFragment : AppIntroBaseFragment() { */ @JvmOverloads @JvmStatic + @Deprecated( + "`newInstance` is deprecated to support color resources instead of color int " + + "for configuration changes and dark theme support", + ReplaceWith( + "createInstance(title, description, imageDrawable, backgroundColor, " + + "titleColor, descriptionColor, titleTypefaceFontRes, descriptionTypefaceFontRes, " + + "backgroundDrawable)", + ), + ) fun newInstance( title: CharSequence? = null, description: CharSequence? = null, - @DrawableRes imageDrawable: Int = 0, - @ColorInt backgroundColor: Int = 0, - @ColorInt titleColor: Int = 0, - @ColorInt descriptionColor: Int = 0, - @FontRes titleTypefaceFontRes: Int = 0, - @FontRes descriptionTypefaceFontRes: Int = 0, - @DrawableRes backgroundDrawable: Int = 0 + @DrawableRes imageDrawable: Int? = null, + @ColorInt backgroundColor: Int? = null, + @ColorInt titleColor: Int? = null, + @ColorInt descriptionColor: Int? = null, + @FontRes titleTypefaceFontRes: Int? = null, + @FontRes descriptionTypefaceFontRes: Int? = null, + @DrawableRes backgroundDrawable: Int? = null, ): AppIntroFragment { - return newInstance( + return createInstance( SliderPage( title = title, description = description, @@ -53,11 +61,75 @@ class AppIntroFragment : AppIntroBaseFragment() { descriptionColor = descriptionColor, titleTypefaceFontRes = titleTypefaceFontRes, descriptionTypefaceFontRes = descriptionTypefaceFontRes, - backgroundDrawable = backgroundDrawable - ) + backgroundDrawable = backgroundDrawable, + ), ) } + /** + * Generates a new instance for [AppIntroFragment] + * + * @param title CharSequence which will be the slide title + * @param description CharSequence which will be the slide description + * @param imageDrawable @DrawableRes (Integer) the image that will be + * displayed, obtained from Resources + * @param backgroundColorRes @ColorRes (Integer) custom background color + * @param titleColorRes @ColorRes (Integer) custom title color + * @param descriptionColorRes @ColorRes (Integer) custom description color + * @param titleTypefaceFontRes @FontRes (Integer) custom title typeface obtained + * from Resources + * @param descriptionTypefaceFontRes @FontRes (Integer) custom description typeface obtained + * from Resources + * @param backgroundDrawable @DrawableRes (Integer) custom background drawable + * + * @return An [AppIntroFragment] created instance + */ + @JvmOverloads + @JvmStatic + fun createInstance( + title: CharSequence? = null, + description: CharSequence? = null, + @DrawableRes imageDrawable: Int? = null, + @ColorRes backgroundColorRes: Int? = null, + @ColorRes titleColorRes: Int? = null, + @ColorRes descriptionColorRes: Int? = null, + @FontRes titleTypefaceFontRes: Int? = null, + @FontRes descriptionTypefaceFontRes: Int? = null, + @DrawableRes backgroundDrawable: Int? = null, + ): AppIntroFragment { + return createInstance( + SliderPage( + title = title, + description = description, + imageDrawable = imageDrawable, + backgroundColorRes = backgroundColorRes, + titleColorRes = titleColorRes, + descriptionColorRes = descriptionColorRes, + titleTypefaceFontRes = titleTypefaceFontRes, + descriptionTypefaceFontRes = descriptionTypefaceFontRes, + backgroundDrawable = backgroundDrawable, + ), + ) + } + + /** + * Generates an [AppIntroFragment] from a given [SliderPage] + * + * @param sliderPage the [SliderPage] object which contains all attributes for + * the current slide + * + * @return An [AppIntroFragment] created instance + */ + @JvmStatic + @Deprecated( + "`newInstance` is deprecated to support color resources instead of color int " + + "for configuration changes and dark theme support", + ReplaceWith( + "createInstance(sliderPage)", + ), + ) + fun newInstance(sliderPage: SliderPage) = createInstance(sliderPage) + /** * Generates an [AppIntroFragment] from a given [SliderPage] * @@ -67,7 +139,7 @@ class AppIntroFragment : AppIntroBaseFragment() { * @return An [AppIntroFragment] created instance */ @JvmStatic - fun newInstance(sliderPage: SliderPage): AppIntroFragment { + fun createInstance(sliderPage: SliderPage): AppIntroFragment { val slide = AppIntroFragment() slide.arguments = sliderPage.toBundle() return slide diff --git a/appintro/src/main/java/com/github/appintro/AppIntroFragmentViewModel.kt b/appintro/src/main/java/com/github/appintro/AppIntroFragmentViewModel.kt new file mode 100644 index 000000000..b89b89510 --- /dev/null +++ b/appintro/src/main/java/com/github/appintro/AppIntroFragmentViewModel.kt @@ -0,0 +1,56 @@ +package com.github.appintro + +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.FontRes +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.github.appintro.internal.delegate + +internal const val ARG_TITLE = "title" +internal const val ARG_TITLE_TYPEFACE_URL = "title_typeface" +internal const val ARG_TITLE_TYPEFACE_RES = "title_typeface_res" +internal const val ARG_DESC = "desc" +internal const val ARG_DESC_TYPEFACE_URL = "desc_typeface" +internal const val ARG_DESC_TYPEFACE_RES = "desc_typeface_res" +internal const val ARG_DRAWABLE = "drawable" +internal const val ARG_BG_COLOR = "bg_color" +internal const val ARG_BG_COLOR_RES = "bg_color_res" +internal const val ARG_TITLE_COLOR = "title_color" +internal const val ARG_TITLE_COLOR_RES = "title_color_res" +internal const val ARG_DESC_COLOR = "desc_color" +internal const val ARG_DESC_COLOR_RES = "desc_color_res" +internal const val ARG_BG_DRAWABLE = "bg_drawable" + +internal class AppIntroFragmentViewModel(state: SavedStateHandle) : ViewModel() { + internal var title by state.delegate(ARG_TITLE) + internal var description by state.delegate(ARG_DESC) + internal var drawable by state.delegate(ARG_DRAWABLE) + internal var bgDrawable by state.delegate(ARG_BG_DRAWABLE) + + @get:ColorInt + internal var titleColor by state.delegate(ARG_TITLE_COLOR) + + @get:ColorRes + internal var titleColorRes by state.delegate(ARG_TITLE_COLOR_RES) + + @get:ColorInt + internal var descColor by state.delegate(ARG_DESC_COLOR) + + @get:ColorRes + internal var descColorRes by state.delegate(ARG_DESC_COLOR_RES) + + @get:ColorRes + internal var defaultBackgroundColorRes by state.delegate(ARG_BG_COLOR_RES) + + @get:ColorInt + internal var defaultBackgroundColor by state.delegate(ARG_BG_COLOR) + + @get:FontRes + internal var titleTypefaceRes by state.delegate(ARG_TITLE_TYPEFACE_RES) + internal var titleTypefaceUrl by state.delegate(ARG_TITLE_TYPEFACE_URL) + + @get:FontRes + internal var descTypefaceRes by state.delegate(ARG_DESC_TYPEFACE_RES) + internal var descTypefaceUrl by state.delegate(ARG_DESC_TYPEFACE_URL) +} diff --git a/appintro/src/main/java/com/github/appintro/AppIntroPageTransformerType.kt b/appintro/src/main/java/com/github/appintro/AppIntroPageTransformerType.kt index 5e1ea68a6..150a268e2 100644 --- a/appintro/src/main/java/com/github/appintro/AppIntroPageTransformerType.kt +++ b/appintro/src/main/java/com/github/appintro/AppIntroPageTransformerType.kt @@ -1,11 +1,12 @@ package com.github.appintro +import androidx.annotation.IdRes + /** * Sealed class to represent all the possible Page Transformers * offered by AppIntro. */ sealed class AppIntroPageTransformerType { - /** Sets the animation of the intro to a flow animation */ object Flow : AppIntroPageTransformerType() @@ -26,10 +27,16 @@ sealed class AppIntroPageTransformerType { * @property titleParallaxFactor Parallax factor of title * @property imageParallaxFactor Parallax factor of image * @property descriptionParallaxFactor Parallax factor of description + * @property titleViewId The ID to use for the title view to animate + * @property imageViewId The ID to use for the image view to animate + * @property descriptionViewId The ID to use for the description view to animate */ class Parallax( val titleParallaxFactor: Double = 1.0, val imageParallaxFactor: Double = -1.0, - val descriptionParallaxFactor: Double = 2.0 + val descriptionParallaxFactor: Double = 2.0, + @IdRes val titleViewId: Int = R.id.title, + @IdRes val imageViewId: Int = R.id.image, + @IdRes val descriptionViewId: Int = R.id.description, ) : AppIntroPageTransformerType() } diff --git a/appintro/src/main/java/com/github/appintro/SlideBackgroundColorHolder.kt b/appintro/src/main/java/com/github/appintro/SlideBackgroundColorHolder.kt index 0e35f3f42..d41aeda6a 100644 --- a/appintro/src/main/java/com/github/appintro/SlideBackgroundColorHolder.kt +++ b/appintro/src/main/java/com/github/appintro/SlideBackgroundColorHolder.kt @@ -1,22 +1,36 @@ package com.github.appintro import androidx.annotation.ColorInt +import androidx.annotation.ColorRes interface SlideBackgroundColorHolder { - /** * Returns the default background color of the slide * * @return The default background color of the slide */ @get:ColorInt + @Deprecated( + "`defaultBackgroundColor` has been deprecated to support configuration changes", + ReplaceWith("defaultBackgroundColorRes"), + ) val defaultBackgroundColor: Int + /** + * Returns the default background color of the slide + * + * @return The default background color of the slide + */ + @get:ColorRes + val defaultBackgroundColorRes: Int + /** * Sets the actual background color of the slide. This does not affect the default background color. * This method should change the background color of the slide's root layout element (e.g. LinearLayout). * * @param backgroundColor New actual background color. */ - fun setBackgroundColor(@ColorInt backgroundColor: Int) + fun setBackgroundColor( + @ColorInt backgroundColor: Int, + ) } diff --git a/appintro/src/main/java/com/github/appintro/SlidePolicy.kt b/appintro/src/main/java/com/github/appintro/SlidePolicy.kt index 0ca97be30..02fb1b643 100644 --- a/appintro/src/main/java/com/github/appintro/SlidePolicy.kt +++ b/appintro/src/main/java/com/github/appintro/SlidePolicy.kt @@ -1,7 +1,6 @@ package com.github.appintro interface SlidePolicy { - /** * Whether the user has fulfilled the slides policy and should be allowed to navigate through the intro further. * If false is returned, [.onUserIllegallyRequestedNextPage] will be called. diff --git a/appintro/src/main/java/com/github/appintro/indicator/DotIndicatorController.kt b/appintro/src/main/java/com/github/appintro/indicator/DotIndicatorController.kt index b910ca15a..57c815363 100644 --- a/appintro/src/main/java/com/github/appintro/indicator/DotIndicatorController.kt +++ b/appintro/src/main/java/com/github/appintro/indicator/DotIndicatorController.kt @@ -1,13 +1,13 @@ package com.github.appintro.indicator import android.content.Context -import android.graphics.PorterDuff import android.view.Gravity import android.view.Gravity.CENTER import android.view.View import android.widget.ImageView import android.widget.LinearLayout import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import com.github.appintro.R /** @@ -15,14 +15,13 @@ import com.github.appintro.R * Use this when the number of page you're dealing with is not too high. */ class DotIndicatorController(context: Context) : IndicatorController, LinearLayout(context) { - - override var selectedIndicatorColor = -1 + override var selectedIndicatorColor = ContextCompat.getColor(context, R.color.appintro_default_selected_color) set(value) { field = value selectPosition(currentPosition) } - override var unselectedIndicatorColor = -1 + override var unselectedIndicatorColor = ContextCompat.getColor(context, R.color.appintro_default_unselected_color) set(value) { field = value selectPosition(currentPosition) @@ -32,9 +31,11 @@ class DotIndicatorController(context: Context) : IndicatorController, LinearLayo private var slideCount = 0 override fun newInstance(context: Context): View { - val newLayoutParams = LayoutParams( - LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT - ) + val newLayoutParams = + LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, + ) newLayoutParams.gravity = Gravity.CENTER_VERTICAL layoutParams = newLayoutParams orientation = HORIZONTAL @@ -44,20 +45,19 @@ class DotIndicatorController(context: Context) : IndicatorController, LinearLayo override fun initialize(slideCount: Int) { this.slideCount = slideCount - for (i in 0 until slideCount) { val dot = ImageView(this.context) dot.setImageDrawable( - ContextCompat.getDrawable( - this.context, - R.drawable.ic_appintro_indicator_unselected - ) - ) - - val params = LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT + ContextCompat.getDrawable(this.context, R.drawable.ic_appintro_indicator), ) + val params = + LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, + ) + if (slideCount == 1) { + dot.visibility = View.INVISIBLE + } this.addView(dot, params) } selectPosition(0) @@ -66,24 +66,13 @@ class DotIndicatorController(context: Context) : IndicatorController, LinearLayo override fun selectPosition(index: Int) { currentPosition = index for (i in 0 until slideCount) { - val drawableId = if (i == index) { - R.drawable.ic_appintro_indicator_selected - } else { R.drawable.ic_appintro_indicator_unselected } - val drawable = ContextCompat.getDrawable(this.context, drawableId) - - if (selectedIndicatorColor != DEFAULT_COLOR && i == index) { - drawable!!.mutate().setColorFilter( - selectedIndicatorColor, - PorterDuff.Mode.SRC_IN - ) - } - if (unselectedIndicatorColor != DEFAULT_COLOR && i != index) { - drawable!!.mutate().setColorFilter( - unselectedIndicatorColor, - PorterDuff.Mode.SRC_IN - ) - } - (getChildAt(i) as ImageView).setImageDrawable(drawable) + val tint = + if (i == index) { + selectedIndicatorColor + } else { + unselectedIndicatorColor + } + (getChildAt(i) as ImageView).let { DrawableCompat.setTint(it.drawable, tint) } } } } diff --git a/appintro/src/main/java/com/github/appintro/indicator/IndicatorController.kt b/appintro/src/main/java/com/github/appintro/indicator/IndicatorController.kt index f2b250039..a5d1006d3 100644 --- a/appintro/src/main/java/com/github/appintro/indicator/IndicatorController.kt +++ b/appintro/src/main/java/com/github/appintro/indicator/IndicatorController.kt @@ -4,15 +4,12 @@ import android.content.Context import android.view.View import androidx.annotation.ColorInt -internal const val DEFAULT_COLOR = 1 - /** * A controller that is used to provide custom indicator implementations and to control * their behaviour. This is used for [AppIntro.setCustomIndicator] and * [AppIntro2.setCustomIndicator] */ interface IndicatorController { - @get:ColorInt var selectedIndicatorColor: Int diff --git a/appintro/src/main/java/com/github/appintro/indicator/ProgressIndicatorController.kt b/appintro/src/main/java/com/github/appintro/indicator/ProgressIndicatorController.kt index 5a56636d0..62676f343 100644 --- a/appintro/src/main/java/com/github/appintro/indicator/ProgressIndicatorController.kt +++ b/appintro/src/main/java/com/github/appintro/indicator/ProgressIndicatorController.kt @@ -1,9 +1,14 @@ package com.github.appintro.indicator import android.content.Context -import android.graphics.PorterDuff import android.util.AttributeSet +import android.view.View import android.widget.ProgressBar +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import com.github.appintro.internal.LayoutUtil + +internal const val DEFAULT_COLOR = 1 /** * An [IndicatorController] that shows a [ProgressBar] for express the number of @@ -11,32 +16,54 @@ import android.widget.ProgressBar * Use this when the number of page is higher and the [DotIndicatorController] * would not fit in the screen. */ -class ProgressIndicatorController @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = android.R.attr.progressBarStyleHorizontal -) : IndicatorController, ProgressBar(context, attrs, defStyleAttr) { - - override var selectedIndicatorColor = DEFAULT_COLOR - set(value) { - field = value - progressDrawable.setColorFilter(value, PorterDuff.Mode.SRC_IN) - } +class ProgressIndicatorController + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.progressBarStyleHorizontal, + ) : IndicatorController, ProgressBar(context, attrs, defStyleAttr) { + override var selectedIndicatorColor = DEFAULT_COLOR + set(value) { + field = value + progressDrawable.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + value, + BlendModeCompat.SRC_ATOP, + ) + } - override var unselectedIndicatorColor = DEFAULT_COLOR - set(value) { - field = value - indeterminateDrawable.setColorFilter(value, PorterDuff.Mode.SRC_IN) - } + override var unselectedIndicatorColor = DEFAULT_COLOR + set(value) { + field = value + progressDrawable.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + value, + BlendModeCompat.SRC_ATOP, + ) + } - override fun newInstance(context: Context) = this + override fun newInstance(context: Context) = this - override fun initialize(slideCount: Int) { - this.max = slideCount - selectPosition(0) - } + override fun initialize(slideCount: Int) { + this.max = slideCount + if (isRtl) { + this.scaleX = -1F + } + if (slideCount == 1) { + this.visibility = View.INVISIBLE + } + selectPosition(0) + } + + override fun selectPosition(index: Int) { + this.progress = + if (isRtl) { + max - index + } else { + index + 1 + } + } - override fun selectPosition(index: Int) { - this.progress = index + 1 + private val isRtl: Boolean get() = LayoutUtil.isRtl(this.context) } -} diff --git a/appintro/src/main/java/com/github/appintro/internal/AppIntroViewPager.kt b/appintro/src/main/java/com/github/appintro/internal/AppIntroViewPager.kt deleted file mode 100644 index 629fe41d6..000000000 --- a/appintro/src/main/java/com/github/appintro/internal/AppIntroViewPager.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.github.appintro.internal - -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.animation.Interpolator -import androidx.viewpager.widget.ViewPager -import com.github.appintro.AppIntroBase -import com.github.appintro.AppIntroPageTransformerType -import com.github.appintro.AppIntroViewPagerListener -import com.github.appintro.internal.viewpager.ViewPagerTransformer -import kotlin.math.absoluteValue -import kotlin.math.max - -/** - * Class that controls the [AppIntro] of AppIntro. - * This is responsible of handling of paging, managing touch and dispatching events. - * - * @property isFullPagingEnabled Enable or disable swiping at all. - * @property isPermissionSlide If the current slide has permissions. - * @property lockPage Set the page where the lock happened. - * @property onNextPageRequestedListener Listener for Next Page events. - * @property isNextPagingEnabled Enable or disable swiping to the next page. - */ -internal class AppIntroViewPager(context: Context, attrs: AttributeSet) : ViewPager(context, attrs) { - - var isFullPagingEnabled = true - var isPermissionSlide = false - var lockPage = 0 - var onNextPageRequestedListener: AppIntroViewPagerListener? = null - var isNextPagingEnabled: Boolean = true - set(value) { - field = value - if (!value) { - lockPage = currentItem - } - } - - private var currentTouchDownX: Float = 0.toFloat() - private var currentTouchDownY: Float = 0.toFloat() - private var illegallyRequestedNextPageLastCalled: Long = 0 - private var customScroller: ScrollerCustomDuration? = null - private var pageChangeListener: OnPageChangeListener? = null - - init { - // Override the Scroller instance with our own class so we can change the duration - try { - val scroller = ViewPager::class.java.getDeclaredField("mScroller") - scroller.isAccessible = true - - val interpolator = ViewPager::class.java.getDeclaredField("sInterpolator") - interpolator.isAccessible = true - - customScroller = ScrollerCustomDuration(context, interpolator.get(null) as Interpolator) - scroller.set(this, customScroller) - } catch (e: NoSuchFieldException) { - e.printStackTrace() - } - } - - internal fun addOnPageChangeListener(listener: AppIntroBase.OnPageChangeListener) { - super.addOnPageChangeListener(listener) - this.pageChangeListener = listener - } - - fun goToNextSlide() { - currentItem += if (!LayoutUtil.isRtl(context)) 1 else -1 - } - - fun goToPreviousSlide() { - currentItem += if (!LayoutUtil.isRtl(context)) -1 else 1 - } - - internal fun reCenterCurrentSlide() { - // Hacky way to force a recenter of the ViewPager to the current slide. - // We perform a page back and forward to recenter the ViewPager at the current position. - // This is needed as we're interrupting the user Swipe due to Permissions. - // If the user denies a permission, we want to recenter the slide. - val item = currentItem - setCurrentItem(max(item - 1, 0), false) - setCurrentItem(item, false) - } - - fun isFirstSlide(size: Int): Boolean { - return if (LayoutUtil.isRtl(context)) (currentItem - size + 1 == 0) else (currentItem == 0) - } - - fun getCurrentSlideNumber(size: Int): Int { - return if (LayoutUtil.isRtl(context)) (size - currentItem) else (currentItem + 1) - } - - /** - * Override is required to trigger [AppIntroBase.OnPageChangeListener.onPageSelected] for the first page. - * This is needed to correctly handle progress button display after rotation on a locked first page. - */ - override fun setCurrentItem(currentItem: Int) { - val oldItem = super.getCurrentItem() - super.setCurrentItem(currentItem) - - // When you pass set current item to 0, - // The pageChangeListener won't be called so we call it on our own - if (oldItem == 0 && currentItem == 0) { - pageChangeListener?.onPageSelected(0) - } - } - - /** - * Set the factor by which the Scrolling duration will change. - */ - fun setScrollDurationFactor(factor: Double) { - customScroller?.scrollDurationFactor = factor - } - - override fun performClick() = super.performClick() - - override fun onInterceptTouchEvent(event: MotionEvent): Boolean { - if (!handleTouchEvent(event)) { - return false - } - - // Calling super will allow the slider to "work" left and right. - return super.onInterceptTouchEvent(event) - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - if (!handleTouchEvent(event)) { - return false - } - - // Calling super will allow the slider to "work" left and right. - return super.onTouchEvent(event) - } - - /** - * Checks for illegal sliding attempts. - * Every time the user presses the screen, the respective coordinates are stored. - * Once the user swipes/stops pressing, the new coordinates are checked against the stored ones. - * Therefor [userIllegallyRequestNextPage] is called. If this call detects an illegal swipe, - * the respective listener [onNextPageRequestedListener] gets called. - */ - private fun handleTouchEvent(event: MotionEvent): Boolean { - // If paging is disabled we should ignore any viewpager touch - // (also, not display any error message) - if (!isFullPagingEnabled) { - return false - } - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - currentTouchDownX = event.x - currentTouchDownY = event.y - } - else -> { - if (event.action == MotionEvent.ACTION_UP) { - performClick() - } - val canRequestNextPage = onNextPageRequestedListener?.onCanRequestNextPage() ?: true - - // If user can't request the page, we shortcircuit the ACTION_MOVE logic here. - // We need to return false, and also call onIllegallyRequestedNextPage if the - // threshold was too high (so the user can be informed). - if (!canRequestNextPage) { - if (userIllegallyRequestNextPage(event)) { - onNextPageRequestedListener?.onIllegallyRequestedNextPage() - } - - return false - } - - // If the slide contains permissions, check for forward swipe. - if (isPermissionSlide && isSwipeForward(currentTouchDownX, event.x)) { - onNextPageRequestedListener?.onUserRequestedPermissionsDialog() - } - } - } - return isFullPagingEnabled - } - - /** - * Util function to check if the user swiped forward. - * The direction of forward is different in RTL mode. - */ - private fun isSwipeForward(oldX: Float, newX: Float): Boolean { - return (if (LayoutUtil.isRtl(context)) (newX > oldX) else (oldX > newX)) - } - - /** - * Util function to check if the user illegally requests a swipe. - * Throttles such requests to max one request per second. - * - * To prevent false positives one has to check that the user scrolls mainly horizontally - * and the horizontal scrolling does not belong to a actual vertical scrolling. - */ - private fun userIllegallyRequestNextPage(event: MotionEvent): Boolean { - if (isASwipeGesture(event, currentTouchDownX, currentTouchDownY) && - System.currentTimeMillis() - illegallyRequestedNextPageLastCalled >= - ON_ILLEGALLY_REQUESTED_NEXT_PAGE_MAX_INTERVAL - ) { - illegallyRequestedNextPageLastCalled = System.currentTimeMillis() - return true - } - - return false - } - - /** - * Checks if two points are aligned and could represent a slide gesture from the user. - */ - private fun isASwipeGesture(startPoint: MotionEvent, x: Float, y: Float) = ( - (startPoint.x - x).absoluteValue >= VALID_SWIPE_THRESHOLD_PX_X && - (startPoint.y - y).absoluteValue <= VALID_SWIPE_THRESHOLD_PX_Y - ) - - fun setAppIntroPageTransformer(appIntroTransformer: AppIntroPageTransformerType) { - setPageTransformer(true, ViewPagerTransformer(appIntroTransformer)) - } - - private companion object { - private const val ON_ILLEGALLY_REQUESTED_NEXT_PAGE_MAX_INTERVAL = 1000 - private const val VALID_SWIPE_THRESHOLD_PX_X = 25 - private const val VALID_SWIPE_THRESHOLD_PX_Y = 25 - } -} diff --git a/appintro/src/main/java/com/github/appintro/internal/AppIntroViewPagerController.kt b/appintro/src/main/java/com/github/appintro/internal/AppIntroViewPagerController.kt new file mode 100644 index 000000000..a19892b4b --- /dev/null +++ b/appintro/src/main/java/com/github/appintro/internal/AppIntroViewPagerController.kt @@ -0,0 +1,302 @@ +package com.github.appintro.internal + +import android.gesture.GestureOverlayView +import android.gesture.GestureOverlayView.OnGestureListener +import android.view.MotionEvent +import androidx.viewpager2.widget.ViewPager2 +import com.github.appintro.AppIntroBase +import com.github.appintro.AppIntroPageTransformerType +import com.github.appintro.AppIntroViewPagerListener +import com.github.appintro.internal.viewpager.PagerAdapter +import com.github.appintro.internal.viewpager.ViewPagerTransformer +import kotlin.math.max + +/** + * Class that controls the [ViewPager2] of AppIntro. + * This is responsible of handling of paging, managing touch and dispatching events. + * + * @property isFullPagingEnabled Enable or disable swiping at all. + * @property isPermissionSlide If the current slide has permissions. + * @property onNextPageRequestedListener Listener for Next Page events. + */ +internal class AppIntroViewPagerController( + private val viewPager: ViewPager2, + private val viewPagerGestureOverlay: GestureOverlayView, +) { + var isFullPagingEnabled = true + var isPermissionSlide = false + var onNextPageRequestedListener: AppIntroViewPagerListener? = null + + private var currentTouchDownX: Float = 0.toFloat() + private var currentTouchDownY: Float = 0.toFloat() + private var illegallyRequestedNextPageLastCalled: Long = 0 + private var pageChangeCallback: AppIntroBase.OnPageChangeCallback? = null + + init { + addPagerTouchInterceptor() + } + + fun goToNextSlide() { + with(viewPager) { + // avoid IllegalStateException when changing item while fake dragging + endFakeDrag() + setCurrentViewPagerItem( + position = if (!LayoutUtil.isRtl(context)) currentItem + 1 else currentItem - 1, + smoothScrool = true, + ) + } + } + + fun goToPreviousSlide() { + with(viewPager) { + // avoid IllegalStateException when changing item while fake dragging + endFakeDrag() + setCurrentViewPagerItem( + position = if (!LayoutUtil.isRtl(context)) currentItem - 1 else currentItem + 1, + smoothScrool = true, + ) + } + } + + fun isFirstSlide(size: Int): Boolean { + with(viewPager) { + return if (LayoutUtil.isRtl(context)) (currentItem - size + 1 == 0) else (currentItem == 0) + } + } + + fun isLastSlide(size: Int): Boolean { + with(viewPager) { + return if (LayoutUtil.isRtl(context)) (currentItem == 0) else (currentItem - size + 1 == 0) + } + } + + fun getCurrentSlideNumber(size: Int): Int { + with(viewPager) { + return if (LayoutUtil.isRtl(context)) (size - currentItem) else (currentItem + 1) + } + } + + /** + * Override is required to trigger [AppIntroBase.OnPageChangeCallback.onPageSelected] for the first page. + * This is needed to correctly handle progress button display after rotation on a locked first page. + */ + fun setCurrentViewPagerItem( + position: Int, + smoothScrool: Boolean = false, + ) { + with(viewPager) { + endFakeDrag() + + val oldItem = currentItem + viewPager.setCurrentItem(position, smoothScrool) + + // When you pass set current item to 0, + // The pageChangeListener won't be called so we call it on our own + if (oldItem == 0 && position == 0) { + pageChangeCallback?.onPageSelected(0) + } + } + } + + /** + * Checks for illegal sliding attempts. + * Every time the user presses the screen, the respective coordinates are stored. + * Once the user swipes/stops pressing, the new coordinates are checked against the stored ones. + * Therefore [userIllegallyRequestNextPage] is called. If this call detects an illegal swipe, + * the respective listener [onNextPageRequestedListener] gets called. + */ + private fun canPerformTouchEvent(event: MotionEvent?): Boolean { + // If paging is disabled we should ignore any viewpager touch + // (also, not display any error message) + if (!isFullPagingEnabled || event == null) { + return false + } + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + currentTouchDownX = event.x + currentTouchDownY = event.y + } + else -> { + if (event.action == MotionEvent.ACTION_UP) { + viewPager.performClick() + } + val canRequestNextPage = onNextPageRequestedListener?.onCanRequestNextPage() ?: true + + // If user can't request the page, we shortcircuit the ACTION_MOVE logic here. + // We need to return false if we detect that the user swipes forward, + // and also call onIllegallyRequestedNextPage if the threshold was too high + // (so the user can be informed). + if (!canRequestNextPage && isSwipeForward(currentTouchDownX, event.x)) { + if (userIllegallyRequestNextPage()) { + onNextPageRequestedListener?.onIllegallyRequestedNextPage() + } + return false + } + + // If the slide contains permissions, check for forward swipe. + if (isPermissionSlide && isSwipeForward(currentTouchDownX, event.x)) { + onNextPageRequestedListener?.onUserRequestedPermissionsDialog() + } + } + } + + return isFullPagingEnabled + } + + private var lastTouchValue: Float = 0f + + /** + * Simulate touch events on the ViewPager2 using fakeDrag, since isUserInputEnabled = false + * We need this to eventually block user touches in forward if policy is not respected + */ + private fun handleOnTouchEvent(event: MotionEvent?): Boolean { + if (!canPerformTouchEvent(event)) { + return false + } + + // allow the slider to "work" left and right. + when (event?.action) { + MotionEvent.ACTION_DOWN -> { + lastTouchValue = event.x + if (!viewPager.isFakeDragging) { + viewPager.beginFakeDrag() + } + } + + MotionEvent.ACTION_MOVE -> { + val value = event.x + val delta = value - lastTouchValue + + viewPager.fakeDragBy(delta) + lastTouchValue = value + return true + } + + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + viewPager.endFakeDrag() + } + } + return true + } + + /** + * Util function to check if the user swiped forward. + * The direction of forward is different in RTL mode. + */ + private fun isSwipeForward( + oldX: Float, + newX: Float, + ): Boolean { + with(viewPager) { + return (if (LayoutUtil.isRtl(context)) (newX > oldX) else (oldX > newX)) + } + } + + /** + * Util function to throttle illegallyRequestedNext to max one request per second. + */ + private fun userIllegallyRequestNextPage(): Boolean { + if (System.currentTimeMillis() - illegallyRequestedNextPageLastCalled >= + ON_ILLEGALLY_REQUESTED_NEXT_PAGE_MAX_INTERVAL + ) { + illegallyRequestedNextPageLastCalled = System.currentTimeMillis() + return true + } + + return false + } + + /** + * Disables ViewPager swipes and handles manually touch events. + * This is because we may want to discard some swipes in a particular direction + * if policy doesn't allow that. + * + * Touch events are then forwarded (if policy allows that) to pager + * using fakeDrag feature of ViewPager2. + */ + private fun addPagerTouchInterceptor() { + // disable ViewPager swipe + // touch events will be forwarded to gesture overlay to check policies + viewPager.isUserInputEnabled = false + + viewPagerGestureOverlay.addOnGestureListener( + object : OnGestureListener { + override fun onGestureStarted( + overlayView: GestureOverlayView?, + event: MotionEvent?, + ) { + handleOnTouchEvent(event) + } + + override fun onGesture( + overlayView: GestureOverlayView?, + event: MotionEvent?, + ) { + handleOnTouchEvent(event) + } + + override fun onGestureEnded( + overlayView: GestureOverlayView?, + event: MotionEvent?, + ) { + handleOnTouchEvent(event) + } + + override fun onGestureCancelled( + overlayView: GestureOverlayView?, + event: MotionEvent?, + ) { + handleOnTouchEvent(event) + } + }, + ) + } + + internal fun reCenterCurrentSlide() { + // Hacky way to force a recenter of the ViewPager to the current slide. + // We perform a page back and forward to recenter the ViewPager at the current position. + // This is needed as we're interrupting the user Swipe due to Permissions. + // If the user denies a permission, we want to recenter the slide. + with(viewPager) { + val item = currentItem + setCurrentViewPagerItem(max(item - 1, 0), false) + setCurrentViewPagerItem(item, false) + } + } + + internal fun registerOnPageChangeCallback(callback: AppIntroBase.OnPageChangeCallback) { + viewPager.registerOnPageChangeCallback(callback) + this.pageChangeCallback = callback + } + + fun setAppIntroPageTransformer(appIntroTransformer: AppIntroPageTransformerType) { + with(viewPager) { + setPageTransformer(ViewPagerTransformer(appIntroTransformer)) + } + } + + fun setPageTransformer(pageTransformer: ViewPager2.PageTransformer?) { + with(viewPager) { + setPageTransformer(pageTransformer) + } + } + + fun setOffscreenPageLimit(offscreenPageLimit: Int) { + viewPager.offscreenPageLimit = offscreenPageLimit + } + + fun setAdapter(pagerAdapter: PagerAdapter) { + viewPager.adapter = pagerAdapter + } + + fun post(function: () -> Unit) { + viewPager.post(function) + } + + fun getCurrentItem() = viewPager.currentItem + + private companion object { + private const val ON_ILLEGALLY_REQUESTED_NEXT_PAGE_MAX_INTERVAL = 1000 + } +} diff --git a/appintro/src/main/java/com/github/appintro/internal/CustomFontCache.kt b/appintro/src/main/java/com/github/appintro/internal/CustomFontCache.kt index 99e883c24..2f553741d 100644 --- a/appintro/src/main/java/com/github/appintro/internal/CustomFontCache.kt +++ b/appintro/src/main/java/com/github/appintro/internal/CustomFontCache.kt @@ -9,11 +9,14 @@ import androidx.core.content.res.ResourcesCompat * Prevent(s) memory leaks due to Typeface objects */ internal object CustomFontCache { - private val TAG = LogHelper.makeLogTag(CustomFontCache::class) private val cache = hashMapOf() - fun getFont(ctx: Context, path: String?, fontCallback: ResourcesCompat.FontCallback) { + fun getFont( + ctx: Context, + path: String?, + fontCallback: ResourcesCompat.FontCallback, + ) { if (path.isNullOrEmpty()) { LogHelper.w(TAG, "Empty typeface path provided!") return diff --git a/appintro/src/main/java/com/github/appintro/internal/LayoutUtil.kt b/appintro/src/main/java/com/github/appintro/internal/LayoutUtil.kt index e64826185..d9d66cc31 100644 --- a/appintro/src/main/java/com/github/appintro/internal/LayoutUtil.kt +++ b/appintro/src/main/java/com/github/appintro/internal/LayoutUtil.kt @@ -1,17 +1,14 @@ package com.github.appintro.internal import android.content.Context -import android.os.Build import android.view.View /** * Util object for interacting with Layouts */ internal object LayoutUtil { - @JvmStatic fun isRtl(ctx: Context): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && - ctx.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL + return ctx.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL } } diff --git a/appintro/src/main/java/com/github/appintro/internal/LogHelper.kt b/appintro/src/main/java/com/github/appintro/internal/LogHelper.kt index d4dfbe8ae..0e8bb7031 100644 --- a/appintro/src/main/java/com/github/appintro/internal/LogHelper.kt +++ b/appintro/src/main/java/com/github/appintro/internal/LogHelper.kt @@ -11,7 +11,6 @@ private const val MAX_LOG_TAG_LENGTH = 23 * Helper object to interact with the Android [Log] class. */ internal object LogHelper { - /** * Creates a tag for the logs from a [Class] * Don't use this when obfuscating class names! @@ -29,7 +28,10 @@ internal object LogHelper { LOG_PREFIX + cutTagLength(cls.simpleName ?: "", MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) - private fun cutTagLength(tag: String, length: Int): String { + private fun cutTagLength( + tag: String, + length: Int, + ): String { return if (tag.length > length) { tag.substring(0, length - 1) } else { @@ -38,29 +40,50 @@ internal object LogHelper { } @JvmStatic - fun d(tag: String, message: String) = Log.d(tag, message) + fun d( + tag: String, + message: String, + ) = Log.d(tag, message) @JvmStatic - fun v(tag: String, message: String) = Log.v(tag, message) + fun v( + tag: String, + message: String, + ) = Log.v(tag, message) @JvmStatic - fun i(tag: String, message: String) = Log.i(tag, message) + fun i( + tag: String, + message: String, + ) = Log.i(tag, message) @JvmOverloads @JvmStatic - fun w(tag: String, message: String, throwable: Throwable? = null) { + fun w( + tag: String, + message: String, + throwable: Throwable? = null, + ) { Log.w(tag, message, throwable) } @JvmOverloads @JvmStatic - fun e(tag: String, message: String, throwable: Throwable? = null) { + fun e( + tag: String, + message: String, + throwable: Throwable? = null, + ) { Log.e(tag, message, throwable) } @JvmOverloads @JvmStatic - fun wtf(tag: String, message: String, throwable: Throwable? = null) { + fun wtf( + tag: String, + message: String, + throwable: Throwable? = null, + ) { Log.wtf(tag, message, throwable) } } diff --git a/appintro/src/main/java/com/github/appintro/internal/PermissionWrapper.kt b/appintro/src/main/java/com/github/appintro/internal/PermissionWrapper.kt index 7dfc3cf52..3d6e4a0b7 100644 --- a/appintro/src/main/java/com/github/appintro/internal/PermissionWrapper.kt +++ b/appintro/src/main/java/com/github/appintro/internal/PermissionWrapper.kt @@ -11,9 +11,8 @@ import java.io.Serializable internal data class PermissionWrapper( var permissions: Array, var position: Int, - var required: Boolean = true + var required: Boolean = true, ) : Serializable { - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -33,4 +32,8 @@ internal data class PermissionWrapper( result = 31 * result + required.hashCode() return result } + + companion object { + private const val serialVersionUID: Long = 1L + } } diff --git a/appintro/src/main/java/com/github/appintro/internal/SavedStateHelper.kt b/appintro/src/main/java/com/github/appintro/internal/SavedStateHelper.kt new file mode 100644 index 000000000..83ee9e41e --- /dev/null +++ b/appintro/src/main/java/com/github/appintro/internal/SavedStateHelper.kt @@ -0,0 +1,25 @@ +package com.github.appintro.internal + +import androidx.lifecycle.SavedStateHandle +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +inline fun SavedStateHandle.delegate(key: String? = null): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue( + thisRef: Any, + property: KProperty<*>, + ): T? { + val stateKey = key ?: property.name + return this@delegate[stateKey] + } + + override fun setValue( + thisRef: Any, + property: KProperty<*>, + value: T?, + ) { + val stateKey = key ?: property.name + this@delegate[stateKey] = value + } + } diff --git a/appintro/src/main/java/com/github/appintro/internal/ScrollerCustomDuration.kt b/appintro/src/main/java/com/github/appintro/internal/ScrollerCustomDuration.kt deleted file mode 100644 index 4ecfd8f72..000000000 --- a/appintro/src/main/java/com/github/appintro/internal/ScrollerCustomDuration.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.appintro.internal - -import android.content.Context -import android.view.animation.Interpolator -import android.widget.Scroller - -private const val DEFAULT_SCROLL_DURATION_FACTOR = 6.0 - -/** - * A [Scroller] that will allow to customize the duration of the scrolling. - */ -internal class ScrollerCustomDuration constructor( - context: Context, - interpolator: Interpolator -) : Scroller(context, interpolator) { - - /** - * Set the factor by which the duration will change - */ - var scrollDurationFactor = DEFAULT_SCROLL_DURATION_FACTOR - - override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) { - super.startScroll(startX, startY, dx, dy, (duration * scrollDurationFactor).toInt()) - } -} diff --git a/appintro/src/main/java/com/github/appintro/internal/TypefaceContainer.kt b/appintro/src/main/java/com/github/appintro/internal/TypefaceContainer.kt index 0d277fcbc..c5079cf43 100644 --- a/appintro/src/main/java/com/github/appintro/internal/TypefaceContainer.kt +++ b/appintro/src/main/java/com/github/appintro/internal/TypefaceContainer.kt @@ -14,9 +14,8 @@ import androidx.core.content.res.ResourcesCompat */ internal data class TypefaceContainer( var typeFaceUrl: String? = null, - @FontRes var typeFaceResource: Int = 0 + @FontRes var typeFaceResource: Int = 0, ) { - /** * Applies typeface to a given TextView object. * If there is no typeface (either URL or resource) set, this method is a no-op. @@ -32,14 +31,16 @@ internal data class TypefaceContainer( } // Callback to font retrieval - val callback = object : ResourcesCompat.FontCallback() { - override fun onFontRetrievalFailed(reason: Int) { - // Don't be panic, just do nothing. - } - override fun onFontRetrieved(typeface: Typeface) { - textView.typeface = typeface + val callback = + object : ResourcesCompat.FontCallback() { + override fun onFontRetrievalFailed(reason: Int) { + // Don't be panic, just do nothing. + } + + override fun onFontRetrieved(typeface: Typeface) { + textView.typeface = typeface + } } - } // We give priority to the FontRes here. if (typeFaceResource != 0) { diff --git a/appintro/src/main/java/com/github/appintro/internal/VibrationHelper.kt b/appintro/src/main/java/com/github/appintro/internal/VibrationHelper.kt new file mode 100644 index 000000000..18ae80bea --- /dev/null +++ b/appintro/src/main/java/com/github/appintro/internal/VibrationHelper.kt @@ -0,0 +1,43 @@ +package com.github.appintro.internal + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat.getSystemService + +object VibrationHelper { + private var vibrator: Vibrator? = null + + private fun initializeVibrator(context: Context) { + vibrator = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = + context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + @Suppress("DEPRECATION") + context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator + } + } + + // You must grant vibration permissions on your AndroidManifest.xml file + @SuppressLint("MissingPermission") + fun vibrate( + context: Context, + vibrateDuration: Long, + ) { + if (vibrator == null) { + initializeVibrator(context) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator?.vibrate(VibrationEffect.createOneShot(vibrateDuration, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + vibrator?.vibrate(vibrateDuration) + } + } +} diff --git a/appintro/src/main/java/com/github/appintro/internal/viewpager/PagerAdapter.kt b/appintro/src/main/java/com/github/appintro/internal/viewpager/PagerAdapter.kt index 3b076363b..26b02fc32 100644 --- a/appintro/src/main/java/com/github/appintro/internal/viewpager/PagerAdapter.kt +++ b/appintro/src/main/java/com/github/appintro/internal/viewpager/PagerAdapter.kt @@ -1,17 +1,24 @@ package com.github.appintro.internal.viewpager import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter +import androidx.viewpager2.adapter.FragmentStateAdapter internal class PagerAdapter( - fragmentManager: FragmentManager, - private val fragments: List -) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + fragmentActivity: FragmentActivity, + private val fragments: MutableList, +) : FragmentStateAdapter(fragmentActivity) { + override fun getItemCount() = this.fragments.size - override fun getItem(position: Int): Fragment { + override fun createFragment(position: Int): Fragment { return fragments[position] } - override fun getCount() = this.fragments.size + fun getItem( + position: Int, + fragmentManager: FragmentManager, + ): Fragment? { + return fragmentManager.findFragmentByTag("f$position") + } } diff --git a/appintro/src/main/java/com/github/appintro/internal/viewpager/ViewPagerTransformer.kt b/appintro/src/main/java/com/github/appintro/internal/viewpager/ViewPagerTransformer.kt index f7998f5cb..26a2105d3 100644 --- a/appintro/src/main/java/com/github/appintro/internal/viewpager/ViewPagerTransformer.kt +++ b/appintro/src/main/java/com/github/appintro/internal/viewpager/ViewPagerTransformer.kt @@ -1,11 +1,11 @@ package com.github.appintro.internal.viewpager import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 import com.github.appintro.AppIntroPageTransformerType -import com.github.appintro.R +import com.github.appintro.internal.LogHelper +import kotlin.math.abs +import kotlin.math.max private const val MIN_SCALE_DEPTH = 0.75f private const val MIN_SCALE_ZOOM = 0.85f @@ -16,14 +16,16 @@ private const val MIN_ALPHA_SLIDE = 0.35f private const val FLOW_ROTATION_ANGLE = -30f internal class ViewPagerTransformer( - private val transformType: AppIntroPageTransformerType -) : ViewPager.PageTransformer { - + private val transformType: AppIntroPageTransformerType, +) : ViewPager2.PageTransformer { private var titlePF: Double = 0.0 private var imagePF: Double = 0.0 private var descriptionPF: Double = 0.0 - override fun transformPage(page: View, position: Float) { + override fun transformPage( + page: View, + position: Float, + ) { when (transformType) { AppIntroPageTransformerType.Flow -> { page.rotationY = position * FLOW_ROTATION_ANGLE @@ -36,32 +38,65 @@ internal class ViewPagerTransformer( titlePF = transformType.titleParallaxFactor imagePF = transformType.imageParallaxFactor descriptionPF = transformType.descriptionParallaxFactor - transformParallax(position, page) + transformParallax( + position, + page, + transformType.titleViewId, + transformType.imageViewId, + transformType.descriptionViewId, + ) } } } - private fun transformParallax(position: Float, page: View) { + private fun transformParallax( + position: Float, + page: View, + titleViewId: Int, + imageViewId: Int, + descriptionViewId: Int, + ) { if (position > -1 && position < 1) { try { - applyParallax(page, position) + applyParallax(page, position, titleViewId, titlePF, "title") + applyParallax(page, position, imageViewId, imagePF, "image") + applyParallax(page, position, descriptionViewId, descriptionPF, "description") } catch (e: IllegalStateException) { - e.printStackTrace() + LogHelper.e(TAG, "Failed to apply parallax effect", e) } } } - private fun applyParallax(page: View, position: Float) { - page.findViewById(R.id.title).translationX = computeParallax(page, position, titlePF) - page.findViewById(R.id.image).translationX = computeParallax(page, position, imagePF) - page.findViewById(R.id.description).translationX = computeParallax(page, position, descriptionPF) + private fun applyParallax( + page: View, + position: Float, + viewId: Int, + parallaxFactor: Double, + logLabel: String, + ) { + page.findViewById(viewId)?.let { + it.translationX = computeParallax(page, position, parallaxFactor) + } ?: { + LogHelper.e( + TAG, + "Could not parallax animate view '$logLabel' as " + + "the provided view ID can't be found in the layout", + ) + } } - private fun computeParallax(page: View, position: Float, parallaxFactor: Double): Float { + private fun computeParallax( + page: View, + position: Float, + parallaxFactor: Double, + ): Float { return (-position * (page.width / parallaxFactor)).toFloat() } - private fun transformFade(position: Float, page: View) { + private fun transformFade( + position: Float, + page: View, + ) { if (position <= -1.0f || position >= 1.0f) { page.translationX = page.width.toFloat() page.alpha = 0.0f @@ -73,13 +108,16 @@ internal class ViewPagerTransformer( } else { // position is between -1.0F & 0.0F OR 0.0F & 1.0F page.translationX = page.width * -position - page.alpha = 1.0f - Math.abs(position) + page.alpha = 1.0f - abs(position) } } - private fun transformZoom(position: Float, page: View) { + private fun transformZoom( + position: Float, + page: View, + ) { if (position >= -1 && position <= 1) { - page.scaleXY = Math.max(MIN_SCALE_ZOOM, 1 - Math.abs(position)) + page.scaleXY = max(MIN_SCALE_ZOOM, 1 - abs(position)) page.alpha = MIN_ALPHA_ZOOM + (page.scaleXY - MIN_SCALE_ZOOM) / (1 - MIN_SCALE_ZOOM) * (1 - MIN_ALPHA_ZOOM) val vMargin = page.height * (1 - page.scaleXY) / 2 @@ -94,22 +132,28 @@ internal class ViewPagerTransformer( } } - private fun transformDepth(position: Float, page: View) { + private fun transformDepth( + position: Float, + page: View, + ) { if (position > 0 && position < 1) { // moving to the right page.alpha = 1 - position - page.scaleXY = MIN_SCALE_DEPTH + (1 - MIN_SCALE_DEPTH) * (1 - Math.abs(position)) + page.scaleXY = MIN_SCALE_DEPTH + (1 - MIN_SCALE_DEPTH) * (1 - abs(position)) page.translationX = page.width * -position } else { page.transformDefaults() } } - private fun transformSlideOver(position: Float, page: View) { + private fun transformSlideOver( + position: Float, + page: View, + ) { if (position < 0 && position > -1) { // this is the page to the left - page.scaleXY = Math.abs(Math.abs(position) - 1) * (1.0f - SCALE_FACTOR_SLIDE) + SCALE_FACTOR_SLIDE - page.alpha = Math.max(MIN_ALPHA_SLIDE, 1 - Math.abs(position)) + page.scaleXY = abs(abs(position) - 1) * (1.0f - SCALE_FACTOR_SLIDE) + SCALE_FACTOR_SLIDE + page.alpha = max(MIN_ALPHA_SLIDE, 1 - abs(position)) val pageWidth = page.width val translateValue = position * -pageWidth if (translateValue > -pageWidth) { @@ -121,6 +165,10 @@ internal class ViewPagerTransformer( page.transformDefaults() } } + + companion object { + private val TAG = LogHelper.makeLogTag(ViewPagerTransformer::class) + } } private fun View.transformDefaults() { diff --git a/appintro/src/main/java/com/github/appintro/model/SliderPage.kt b/appintro/src/main/java/com/github/appintro/model/SliderPage.kt index 189064ed8..802d731e2 100644 --- a/appintro/src/main/java/com/github/appintro/model/SliderPage.kt +++ b/appintro/src/main/java/com/github/appintro/model/SliderPage.kt @@ -2,58 +2,88 @@ package com.github.appintro.model import android.os.Bundle import androidx.annotation.ColorInt +import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FontRes import com.github.appintro.ARG_BG_COLOR +import com.github.appintro.ARG_BG_COLOR_RES import com.github.appintro.ARG_BG_DRAWABLE import com.github.appintro.ARG_DESC import com.github.appintro.ARG_DESC_COLOR -import com.github.appintro.ARG_DESC_TYPEFACE +import com.github.appintro.ARG_DESC_COLOR_RES import com.github.appintro.ARG_DESC_TYPEFACE_RES +import com.github.appintro.ARG_DESC_TYPEFACE_URL import com.github.appintro.ARG_DRAWABLE import com.github.appintro.ARG_TITLE import com.github.appintro.ARG_TITLE_COLOR -import com.github.appintro.ARG_TITLE_TYPEFACE +import com.github.appintro.ARG_TITLE_COLOR_RES import com.github.appintro.ARG_TITLE_TYPEFACE_RES +import com.github.appintro.ARG_TITLE_TYPEFACE_URL /** * Slide Page Model. * * This data class represent a single page that can be visualized with AppIntro. */ -data class SliderPage @JvmOverloads constructor( - var title: CharSequence? = null, - var description: CharSequence? = null, - @DrawableRes var imageDrawable: Int = 0, - @ColorInt var backgroundColor: Int = 0, - @ColorInt var titleColor: Int = 0, - @ColorInt var descriptionColor: Int = 0, - @FontRes var titleTypefaceFontRes: Int = 0, - @FontRes var descriptionTypefaceFontRes: Int = 0, - var titleTypeface: String? = null, - var descriptionTypeface: String? = null, - @DrawableRes var backgroundDrawable: Int = 0 -) { - val titleString: String? get() = title?.toString() - val descriptionString: String? get() = description?.toString() +data class SliderPage + @JvmOverloads + constructor( + var title: CharSequence? = null, + var description: CharSequence? = null, + @DrawableRes var imageDrawable: Int? = null, + @ColorInt + @Deprecated( + "`backgroundColor` has been deprecated to support configuration changes", + ReplaceWith("backgroundColorRes"), + ) + var backgroundColor: Int? = null, + @ColorInt + @Deprecated( + "`titleColor` has been deprecated to support configuration changes", + ReplaceWith("titleColorRes"), + ) + var titleColor: Int? = null, + @ColorInt + @Deprecated( + "`descriptionColor` has been deprecated to support configuration changes", + ReplaceWith("descriptionColorRes"), + ) + var descriptionColor: Int? = null, + @ColorRes var backgroundColorRes: Int? = null, + @ColorRes var titleColorRes: Int? = null, + @ColorRes var descriptionColorRes: Int? = null, + @FontRes var titleTypefaceFontRes: Int? = null, + @FontRes var descriptionTypefaceFontRes: Int? = null, + var titleTypeface: String? = null, + var descriptionTypeface: String? = null, + @DrawableRes var backgroundDrawable: Int? = null, + ) { + val titleString: CharSequence? get() = title + val descriptionString: CharSequence? get() = description - /** - * Util method to convert a [SliderPage] into an Android [Bundle]. - * This method will be used to pass the [SliderPage] to [AppIntroBaseFragment] implementations. - */ - fun toBundle(): Bundle { - val newBundle = Bundle() - newBundle.putString(ARG_TITLE, this.titleString) - newBundle.putString(ARG_TITLE_TYPEFACE, this.titleTypeface) - newBundle.putInt(ARG_TITLE_TYPEFACE_RES, this.titleTypefaceFontRes) - newBundle.putInt(ARG_TITLE_COLOR, this.titleColor) - newBundle.putString(ARG_DESC, this.descriptionString) - newBundle.putString(ARG_DESC_TYPEFACE, this.descriptionTypeface) - newBundle.putInt(ARG_DESC_TYPEFACE_RES, this.descriptionTypefaceFontRes) - newBundle.putInt(ARG_DESC_COLOR, this.descriptionColor) - newBundle.putInt(ARG_DRAWABLE, this.imageDrawable) - newBundle.putInt(ARG_BG_COLOR, this.backgroundColor) - newBundle.putInt(ARG_BG_DRAWABLE, this.backgroundDrawable) - return newBundle + /** + * Util method to convert a [SliderPage] into an Android [Bundle]. + * This method will be used to pass the [SliderPage] to [AppIntroBaseFragment] implementations. + */ + @Suppress("DEPRECATION") + fun toBundle(): Bundle { + val newBundle = Bundle() + newBundle.putCharSequence(ARG_TITLE, this.titleString) + newBundle.putString(ARG_TITLE_TYPEFACE_URL, this.titleTypeface) + newBundle.putCharSequence(ARG_DESC, this.descriptionString) + newBundle.putString(ARG_DESC_TYPEFACE_URL, this.descriptionTypeface) + + this.titleTypefaceFontRes?.let { newBundle.putInt(ARG_TITLE_TYPEFACE_RES, it) } + this.titleColor?.let { newBundle.putInt(ARG_TITLE_COLOR, it) } + this.titleColorRes?.let { newBundle.putInt(ARG_TITLE_COLOR_RES, it) } + this.descriptionTypefaceFontRes?.let { newBundle.putInt(ARG_DESC_TYPEFACE_RES, it) } + this.descriptionColor?.let { newBundle.putInt(ARG_DESC_COLOR, it) } + this.descriptionColorRes?.let { newBundle.putInt(ARG_DESC_COLOR_RES, it) } + this.imageDrawable?.let { newBundle.putInt(ARG_DRAWABLE, it) } + this.backgroundColor?.let { newBundle.putInt(ARG_BG_COLOR, it) } + this.backgroundColorRes?.let { newBundle.putInt(ARG_BG_COLOR_RES, it) } + this.backgroundDrawable?.let { newBundle.putInt(ARG_BG_DRAWABLE, it) } + + return newBundle + } } -} diff --git a/appintro/src/main/java/com/github/appintro/model/SliderPagerBuilder.kt b/appintro/src/main/java/com/github/appintro/model/SliderPagerBuilder.kt index ab0adf284..380b270f7 100644 --- a/appintro/src/main/java/com/github/appintro/model/SliderPagerBuilder.kt +++ b/appintro/src/main/java/com/github/appintro/model/SliderPagerBuilder.kt @@ -1,6 +1,7 @@ package com.github.appintro.model import androidx.annotation.ColorInt +import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FontRes @@ -10,35 +11,43 @@ import androidx.annotation.FontRes * a [SliderPage] directly. */ class SliderPagerBuilder { - private var title: CharSequence? = null private var description: CharSequence? = null @DrawableRes - private var imageDrawable: Int = 0 + private var imageDrawable: Int? = null @ColorInt - private var backgroundColor: Int = 0 + private var backgroundColor: Int? = null + + @ColorRes + private var backgroundColorRes: Int? = null @ColorInt - private var titleColor: Int = 0 + private var titleColor: Int? = null + + @ColorRes + private var titleColorRes: Int? = null @ColorInt - private var descriptionColor: Int = 0 + private var descriptionColor: Int? = null + + @ColorRes + private var descriptionColorRes: Int? = null @FontRes - private var titleTypefaceFontRes: Int = 0 + private var titleTypefaceFontRes: Int? = null @FontRes - private var descriptionTypefaceFontRes: Int = 0 + private var descriptionTypefaceFontRes: Int? = null private var titleTypeface: String? = null private var descriptionTypeface: String? = null @DrawableRes - private var backgroundDrawable: Int = 0 + private var backgroundDrawable: Int? = null fun title(title: CharSequence): SliderPagerBuilder { this.title = title @@ -50,32 +59,77 @@ class SliderPagerBuilder { return this } - fun imageDrawable(@DrawableRes imageDrawable: Int): SliderPagerBuilder { + fun imageDrawable( + @DrawableRes imageDrawable: Int, + ): SliderPagerBuilder { this.imageDrawable = imageDrawable return this } - fun backgroundColor(@ColorInt backgroundColor: Int): SliderPagerBuilder { + @Deprecated( + "`backgroundColor(...)` has been deprecated to support configuration changes", + ReplaceWith("backgroundColorRes(backgroundColor)"), + ) + fun backgroundColor( + @ColorInt backgroundColor: Int, + ): SliderPagerBuilder { this.backgroundColor = backgroundColor return this } - fun titleColor(@ColorInt titleColor: Int): SliderPagerBuilder { + fun backgroundColorRes( + @ColorRes backgroundColorRes: Int, + ): SliderPagerBuilder { + this.backgroundColorRes = backgroundColorRes + return this + } + + @Deprecated( + "`titleColor(...)` has been deprecated to support configuration changes", + ReplaceWith("titleColorRes(titleColor)"), + ) + fun titleColor( + @ColorInt titleColor: Int, + ): SliderPagerBuilder { this.titleColor = titleColor return this } - fun descriptionColor(@ColorInt descriptionColor: Int): SliderPagerBuilder { + fun titleColorRes( + @ColorRes titleColorRes: Int, + ): SliderPagerBuilder { + this.titleColorRes = titleColorRes + return this + } + + @Deprecated( + "`descriptionColor(...)` has been deprecated to support configuration changes", + ReplaceWith("descriptionColorRes(descriptionColor)"), + ) + fun descriptionColor( + @ColorInt descriptionColor: Int, + ): SliderPagerBuilder { this.descriptionColor = descriptionColor return this } - fun titleTypefaceFontRes(@FontRes titleTypefaceFontRes: Int): SliderPagerBuilder { + fun descriptionColorRes( + @ColorRes descriptionColorRes: Int, + ): SliderPagerBuilder { + this.descriptionColorRes = descriptionColorRes + return this + } + + fun titleTypefaceFontRes( + @FontRes titleTypefaceFontRes: Int, + ): SliderPagerBuilder { this.titleTypefaceFontRes = titleTypefaceFontRes return this } - fun descriptionTypefaceFontRes(@FontRes descriptionTypefaceFontRes: Int): SliderPagerBuilder { + fun descriptionTypefaceFontRes( + @FontRes descriptionTypefaceFontRes: Int, + ): SliderPagerBuilder { this.descriptionTypefaceFontRes = descriptionTypefaceFontRes return this } @@ -90,22 +144,28 @@ class SliderPagerBuilder { return this } - fun backgroundDrawable(@DrawableRes backgroundDrawable: Int): SliderPagerBuilder { + fun backgroundDrawable( + @DrawableRes backgroundDrawable: Int, + ): SliderPagerBuilder { this.backgroundDrawable = backgroundDrawable return this } - fun build() = SliderPage( - title = this.title, - description = this.description, - imageDrawable = this.imageDrawable, - backgroundColor = this.backgroundColor, - titleColor = this.titleColor, - descriptionColor = this.descriptionColor, - titleTypefaceFontRes = this.titleTypefaceFontRes, - descriptionTypeface = this.descriptionTypeface, - titleTypeface = this.titleTypeface, - descriptionTypefaceFontRes = this.descriptionTypefaceFontRes, - backgroundDrawable = this.backgroundDrawable - ) + fun build() = + SliderPage( + title = this.title, + description = this.description, + imageDrawable = this.imageDrawable, + backgroundColor = this.backgroundColor, + backgroundColorRes = this.backgroundColorRes, + titleColor = this.titleColor, + titleColorRes = this.titleColorRes, + descriptionColor = this.descriptionColor, + descriptionColorRes = this.descriptionColorRes, + titleTypefaceFontRes = this.titleTypefaceFontRes, + descriptionTypeface = this.descriptionTypeface, + titleTypeface = this.titleTypeface, + descriptionTypefaceFontRes = this.descriptionTypefaceFontRes, + backgroundDrawable = this.backgroundDrawable, + ) } diff --git a/appintro/src/main/res/drawable-v21/ic_appintro_ripple.xml b/appintro/src/main/res/drawable-v21/ic_appintro_ripple.xml deleted file mode 100644 index 7ec20e180..000000000 --- a/appintro/src/main/res/drawable-v21/ic_appintro_ripple.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/appintro/src/main/res/drawable/ic_appintro_indicator_selected.xml b/appintro/src/main/res/drawable/ic_appintro_indicator.xml similarity index 86% rename from appintro/src/main/res/drawable/ic_appintro_indicator_selected.xml rename to appintro/src/main/res/drawable/ic_appintro_indicator.xml index 31f37bebe..30f193f34 100644 --- a/appintro/src/main/res/drawable/ic_appintro_indicator_selected.xml +++ b/appintro/src/main/res/drawable/ic_appintro_indicator.xml @@ -6,7 +6,7 @@ android:right="@dimen/appintro_indicator_inset" android:top="@dimen/appintro_indicator_inset"> - + diff --git a/appintro/src/main/res/drawable/ic_appintro_indicator_unselected.xml b/appintro/src/main/res/drawable/ic_appintro_indicator_unselected.xml deleted file mode 100644 index d328b9c0d..000000000 --- a/appintro/src/main/res/drawable/ic_appintro_indicator_unselected.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/appintro/src/main/res/drawable/ic_appintro_ripple.xml b/appintro/src/main/res/drawable/ic_appintro_ripple.xml index a90d72dfd..a194c18a0 100644 --- a/appintro/src/main/res/drawable/ic_appintro_ripple.xml +++ b/appintro/src/main/res/drawable/ic_appintro_ripple.xml @@ -1,4 +1,9 @@ - - - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/appintro/src/main/res/layout-land/appintro_fragment_intro.xml b/appintro/src/main/res/layout-land/appintro_fragment_intro.xml index 394dfe7a5..d1346da6e 100644 --- a/appintro/src/main/res/layout-land/appintro_fragment_intro.xml +++ b/appintro/src/main/res/layout-land/appintro_fragment_intro.xml @@ -13,12 +13,14 @@ android:id="@+id/title" style="@style/AppIntroDefaultHeading" android:layout_width="0dp" - android:layout_height="wrap_content" + android:layout_height="0dp" + app:layout_constrainedWidth="true" app:layout_constraintBottom_toTopOf="@+id/description" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/midline" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="spread" + app:layout_constraintVertical_weight="2" tools:text="Welcome" /> + app:layout_constraintTop_toBottomOf="@+id/title" + app:layout_constraintVertical_weight="5" /> diff --git a/appintro/src/main/res/layout/appintro_intro_layout.xml b/appintro/src/main/res/layout/appintro_intro_layout.xml index 859377808..ef28eaf88 100644 --- a/appintro/src/main/res/layout/appintro_intro_layout.xml +++ b/appintro/src/main/res/layout/appintro_intro_layout.xml @@ -5,20 +5,26 @@ android:id="@+id/background" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/appintro_background_color" android:fitsSystemWindows="false" android:theme="@style/AppIntroStyle"> - + app:layout_constraintTop_toTopOf="parent"> + + + @@ -55,6 +62,7 @@ android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/bottom" app:srcCompat="@drawable/ic_appintro_next" /> + tools:background="@drawable/ic_appintro_indicator" />