├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── colors.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_color.png
│ │ │ │ ├── ic_close_black.png
│ │ │ │ ├── ic_collections.png
│ │ │ │ ├── ic_format_size.png
│ │ │ │ ├── ic_sentiment_satisfied.png
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── elconfidencial
│ │ │ └── bubbleshowcase
│ │ │ └── MainActivity.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── elconfidencial
│ │ │ └── bubbleshowcase
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── elconfidencial
│ │ └── bubbleshowcase
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── bubbleshowcase
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── bool.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── attrs.xml
│ │ │ ├── values-w600dp
│ │ │ │ └── bool.xml
│ │ │ ├── drawable-hdpi
│ │ │ │ └── ic_close.png
│ │ │ ├── drawable-mdpi
│ │ │ │ └── ic_close.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── ic_close.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── ic_close.png
│ │ │ ├── drawable
│ │ │ │ └── rounded_rectangle.xml
│ │ │ └── layout
│ │ │ │ └── view_bubble_message.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── elconfidencial
│ │ │ └── bubbleshowcase
│ │ │ ├── SequenceShowCaseListener.kt
│ │ │ ├── OnBubbleMessageViewListener.kt
│ │ │ ├── BubbleShowCaseListener.kt
│ │ │ ├── BubbleShowCaseSequence.kt
│ │ │ ├── AnimationUtils.kt
│ │ │ ├── ScreenUtils.kt
│ │ │ ├── BubbleShowCaseBuilder.kt
│ │ │ ├── BubbleMessageView.kt
│ │ │ └── BubbleShowCase.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── elconfidencial
│ │ │ └── bubbleshowcase
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── elconfidencial
│ │ └── bubbleshowcase
│ │ └── ExampleInstrumentedTest.java
├── libraryUpdateInstructions
├── proguard-rules.pro
├── build.gradle
└── deploy.gradle
├── settings.gradle
├── resources
└── usage_sample.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── .idea
├── vcs.xml
├── runConfigurations.xml
├── gradle.xml
├── modules.xml
└── misc.xml
├── gradle.properties
├── LICENSE
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/bubbleshowcase/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':bubbleshowcase'
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BubbleShowCase
3 |
4 |
--------------------------------------------------------------------------------
/resources/usage_sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/resources/usage_sample.gif
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BubbleShowCase
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/drawable/ic_color.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/values/bool.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/drawable/ic_close_black.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_collections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/drawable/ic_collections.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_format_size.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/drawable/ic_format_size.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/values-w600dp/bool.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sentiment_satisfied.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/drawable/ic_sentiment_satisfied.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/drawable-hdpi/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/bubbleshowcase/src/main/res/drawable-hdpi/ic_close.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/drawable-mdpi/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/bubbleshowcase/src/main/res/drawable-mdpi/ic_close.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/drawable-xhdpi/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/bubbleshowcase/src/main/res/drawable-xhdpi/ic_close.png
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/drawable-xxhdpi/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ECLaboratorio/BubbleShowCase-Android/HEAD/bubbleshowcase/src/main/res/drawable-xxhdpi/ic_close.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | /bubbleshowcase/keystore.gradle
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/SequenceShowCaseListener.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | /**
4 | * Created by jcampos on 11/09/2018.
5 | */
6 | interface SequenceShowCaseListener {
7 | fun onDismiss()
8 | }
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #77000000
4 | #3F51B5
5 | #FFF
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 04 15:24:44 CEST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/elconfidencial/bubbleshowcase/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/drawable/rounded_rectangle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
11 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/OnBubbleMessageViewListener.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | /**
4 | * Created by jcampos on 10/09/2018.
5 | */
6 | interface OnBubbleMessageViewListener {
7 | /**
8 | * It is called when a user clicks the close action image in the BubbleMessageView
9 | */
10 | fun onCloseActionImageClick()
11 |
12 |
13 | /**
14 | * It is called when a user clicks the BubbleMessageView
15 | */
16 | fun onBubbleClick()
17 | }
--------------------------------------------------------------------------------
/bubbleshowcase/src/test/java/com/elconfidencial/bubbleshowcase/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #33691E
6 | #E65100
7 | #C62828
8 | #D81B60
9 | #CFD8DC
10 | #FF4081
11 | #FFF59D
12 | #000
13 | #00BFA5
14 |
15 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/bubbleshowcase/libraryUpdateInstructions:
--------------------------------------------------------------------------------
1 | Steps:
2 |
3 | 1. Push all changes to Github https://github.com/ECLaboratorio/BubbleShowCase-Android
4 | 2. Check if you have the file keystore.gradle. If not, create it with this:
5 |
6 | ext {
7 | bintray_api_key = 'API KEY' // Get it from here https://bintray.com/profile/edit
8 | }
9 |
10 | 3. Go to deploy.gradle: Upgrade libraryVersion number
11 | 4. Execute this command in terminal: "gradle install" to build the new version
12 | 5. Execute this command in terminal: "gradle bintrayUpload" to upload changes in Bintray. You can check if everything is correct in https://bintray.com/laboratorioec/Laboratorio-apps/BubbleShowCase
13 |
14 | For more information, check this blog: https://notes.devlabs.bg/beginners-guide-for-publishing-a-kotlin-library-to-jcenter-41272bfc214
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/elconfidencial/bubbleshowcase/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getTargetContext()
22 | assertEquals("com.elconfidencial.bubbleshowcase", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/bubbleshowcase/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/BubbleShowCaseListener.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | /**
4 | * Created by jcampos on 04/09/2018.
5 | *
6 | * Listener of user actions in a BubbleShowCase
7 | */
8 | interface BubbleShowCaseListener {
9 | /**
10 | * It is called when the user clicks on the targetView
11 | */
12 | fun onTargetClick(bubbleShowCase: BubbleShowCase)
13 |
14 | /**
15 | * It is called when the user clicks on the close icon
16 | */
17 | fun onCloseActionImageClick(bubbleShowCase: BubbleShowCase)
18 |
19 | /**
20 | * It is called when the user clicks on the background dim
21 | */
22 | fun onBackgroundDimClick(bubbleShowCase: BubbleShowCase)
23 |
24 | /**
25 | * It is called when the user clicks on the bubble
26 | */
27 | fun onBubbleClick(bubbleShowCase: BubbleShowCase)
28 | }
--------------------------------------------------------------------------------
/bubbleshowcase/src/androidTest/java/com/elconfidencial/bubbleshowcase/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.elconfidencial.bubbleshowcaseview.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Datos
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 27
9 | defaultConfig {
10 | applicationId "com.elconfidencial.bubbleshowcase"
11 | minSdkVersion 16
12 | targetSdkVersion 27
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation project(':bubbleshowcase')
28 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
29 | implementation 'com.android.support:design:27.1.1'
30 | implementation 'com.android.support:appcompat-v7:27.1.1'
31 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
32 | testImplementation 'junit:junit:4.12'
33 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
34 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
35 | }
36 |
--------------------------------------------------------------------------------
/bubbleshowcase/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.github.dcendents.android-maven'
5 | apply plugin: 'com.jfrog.bintray'
6 | apply plugin: 'org.jetbrains.dokka'
7 | apply plugin: 'maven-publish'
8 |
9 |
10 |
11 | android {
12 | compileSdkVersion 26
13 |
14 |
15 |
16 | defaultConfig {
17 | minSdkVersion 16
18 | targetSdkVersion 26
19 | versionCode 1
20 | versionName "1.0"
21 |
22 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
23 |
24 | }
25 |
26 | buildTypes {
27 | release {
28 | minifyEnabled false
29 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
30 | }
31 | }
32 |
33 | }
34 |
35 | dokka {
36 | outputFormat = 'html'
37 | outputDirectory = "$buildDir/javadoc"
38 | }
39 |
40 | dependencies {
41 | implementation fileTree(dir: 'libs', include: ['*.jar'])
42 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
43 | implementation 'com.android.support.constraint:constraint-layout:1.0.2'
44 | implementation 'com.android.support:appcompat-v7:26.1.0'
45 | testImplementation 'junit:junit:4.12'
46 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
47 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
48 | }
49 | repositories {
50 | mavenCentral()
51 | }
52 |
53 |
54 | apply from: 'deploy.gradle'
55 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/BubbleShowCaseSequence.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | /**
4 | * Created by jcampos on 10/09/2018.
5 | */
6 | class BubbleShowCaseSequence{
7 | private val mBubbleShowCaseBuilderList = ArrayList()
8 |
9 | init{
10 | mBubbleShowCaseBuilderList.clear()
11 | }
12 |
13 | fun addShowCase(bubbleShowCaseBuilder: BubbleShowCaseBuilder): BubbleShowCaseSequence {
14 | mBubbleShowCaseBuilderList.add(bubbleShowCaseBuilder)
15 | return this
16 | }
17 |
18 | fun addShowCases(bubbleShowCaseBuilderList: List): BubbleShowCaseSequence {
19 | mBubbleShowCaseBuilderList.addAll(bubbleShowCaseBuilderList)
20 | return this
21 | }
22 |
23 | fun show() = show(0)
24 |
25 | private fun show(position: Int){
26 | if(position >= mBubbleShowCaseBuilderList.size)
27 | return
28 |
29 | when(position){
30 | 0 -> {
31 | mBubbleShowCaseBuilderList[position].isFirstOfSequence(true)
32 | mBubbleShowCaseBuilderList[position].isLastOfSequence(false)
33 | }
34 | mBubbleShowCaseBuilderList.size-1 -> {
35 | mBubbleShowCaseBuilderList[position].isFirstOfSequence(false)
36 | mBubbleShowCaseBuilderList[position].isLastOfSequence(true)
37 | }
38 | else -> {
39 | mBubbleShowCaseBuilderList[position].isFirstOfSequence(false)
40 | mBubbleShowCaseBuilderList[position].isLastOfSequence(false)
41 | }
42 | }
43 | mBubbleShowCaseBuilderList[position].sequenceListener(object : SequenceShowCaseListener{
44 | override fun onDismiss() {
45 | show(position + 1)
46 | }
47 | }).show()
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/AnimationUtils.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import android.animation.ObjectAnimator
4 | import android.animation.PropertyValuesHolder
5 | import android.view.View
6 | import android.view.animation.AlphaAnimation
7 | import android.view.animation.Animation
8 | import android.view.animation.DecelerateInterpolator
9 | import android.view.animation.ScaleAnimation
10 |
11 | /**
12 | * Created by jcampos on 05/09/2018.
13 | */
14 | object AnimationUtils {
15 |
16 | fun getScaleAnimation(offset: Int, duration: Int): Animation {
17 | val anim = ScaleAnimation(
18 | 0f, 1f, // Start and end values for the X axis scaling
19 | 0f, 1f, // Start and end values for the Y axis scaling
20 | Animation.RELATIVE_TO_SELF, 0.5f, // Pivot point of X scaling
21 | Animation.RELATIVE_TO_SELF, 0.5f) // Pivot point of Y scaling
22 | anim.fillAfter = true
23 | anim.startOffset = offset.toLong()
24 | anim.duration = duration.toLong()
25 | return anim
26 | }
27 |
28 | fun getFadeInAnimation(offset: Int, duration: Int): Animation {
29 | val fadeIn = AlphaAnimation(0f, 1f)
30 | fadeIn.startOffset = offset.toLong()
31 | fadeIn.interpolator = DecelerateInterpolator()
32 | fadeIn.duration = duration.toLong()
33 | return fadeIn
34 | }
35 |
36 | fun setBouncingAnimation(view: View, offset: Int, duration: Int): View {
37 |
38 | val objAnim = ObjectAnimator.ofPropertyValuesHolder(view,
39 | PropertyValuesHolder.ofFloat("scaleX", 1.05f),
40 | PropertyValuesHolder.ofFloat("scaleY", 1.05f))
41 | objAnim.duration = duration.toLong()
42 | objAnim.startDelay = offset.toLong()
43 | objAnim.repeatCount = ObjectAnimator.INFINITE
44 | objAnim.repeatMode = ObjectAnimator.REVERSE
45 | objAnim.start()
46 | return view
47 | }
48 |
49 | fun setAnimationToView(view: View, animation: Animation): View {
50 | view.startAnimation(animation)
51 | return view
52 | }
53 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 1.8
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/bubbleshowcase/deploy.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.dcendents.android-maven'
2 | apply plugin: 'com.jfrog.bintray'
3 | apply from: 'keystore.gradle'
4 |
5 | //This file was created following instructions from: https://notes.devlabs.bg/beginners-guide-for-publishing-a-kotlin-library-to-jcenter-41272bfc214
6 |
7 | ext{
8 | groupId = 'com.elconfidencial.bubbleshowcase'
9 | artifactId = 'bubbleshowcase'
10 | libraryVersion = '1.3.1'
11 | }
12 |
13 | version = libraryVersion
14 | group = groupId
15 |
16 | bintray{
17 | user = 'laboratorioec'
18 | key = bintray_api_key //Defined in keystore.gradle file
19 | configurations = ['archives']
20 | publish = true
21 | override = true
22 | pkg {
23 | repo = "Laboratorio-apps"
24 | name = "BubbleShowCase"
25 | version {
26 | name = libraryVersion
27 | }
28 | }
29 | }
30 |
31 | install {
32 | repositories.mavenInstaller {
33 | pom.project {
34 | packaging 'aar'
35 | groupId groupId
36 | artifactId artifactId
37 | version libraryVersion
38 | name artifactId
39 | }
40 | }
41 | }
42 |
43 | if (project.hasProperty("kotlin")) { //Kotlin libraries
44 | task sourcesJar(type: Jar) {
45 | classifier = 'sources'
46 | from android.sourceSets.main.java.srcDirs
47 | }
48 |
49 | task javadoc(type: Javadoc, dependsOn: dokka) {
50 |
51 | }
52 | } else if (project.hasProperty("android")) {
53 | task sourcesJar(type: Jar) {
54 | classifier = 'sources'
55 | from android.sourceSets.main.java.srcDirs
56 | }
57 |
58 | task javadoc(type: Javadoc) {
59 | source = android.sourceSets.main.java.srcDirs
60 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
61 | }
62 | } else { // Java libraries
63 | task sourcesJar(type: Jar, dependsOn: classes) {
64 | classifier = 'sources'
65 | from sourceSets.main.allSource
66 | }
67 | }
68 |
69 | task javadocJar(type: Jar, dependsOn: javadoc) {
70 | classifier = 'javadoc'
71 | from javadoc.destinationDir
72 | // options.encoding = 'UTF-8'
73 | }
74 |
75 | artifacts {
76 | archives javadocJar
77 | archives sourcesJar
78 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/res/layout/view_bubble_message.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
26 |
27 |
38 |
39 |
49 |
50 |
59 |
60 |
61 |
62 |
63 |
72 |
73 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/ScreenUtils.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.res.Resources
6 | import android.graphics.Point
7 | import android.graphics.Rect
8 | import android.util.DisplayMetrics
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.view.WindowManager
12 | import android.view.Window.ID_ANDROID_CONTENT
13 |
14 |
15 |
16 | /**
17 | * Created by jcampos on 05/09/2018.
18 | */
19 | object ScreenUtils {
20 |
21 | fun getScreenHeight(context: Context): Int {
22 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
23 | val display = wm.defaultDisplay
24 | val size = Point()
25 | display.getSize(size)
26 | return size.y
27 | }
28 |
29 | fun getScreenWidth(context: Context): Int {
30 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
31 | val display = wm.defaultDisplay
32 | val size = Point()
33 | display.getSize(size)
34 | return size.x
35 | }
36 |
37 | fun getAxisXpositionOfViewOnScreen(targetView: View): Int {
38 | val locationTarget = IntArray(2)
39 | targetView.getLocationOnScreen(locationTarget)
40 | return locationTarget[0]
41 | }
42 |
43 | fun getAxisYpositionOfViewOnScreen(targetView: View): Int {
44 | val locationTarget = IntArray(2)
45 | targetView.getLocationOnScreen(locationTarget)
46 | return locationTarget[1]
47 | }
48 |
49 | fun getVerticalScreenOffset(activity: Activity): Int{
50 | val viewRoot = getViewRoot(activity)
51 | return getScreenHeight(activity) - viewRoot.height
52 | }
53 |
54 | fun getHorizontalScreenOffset(activity: Activity): Int{
55 | val viewRoot = getViewRoot(activity)
56 | return getScreenWidth(activity) - viewRoot.width
57 | }
58 |
59 | private fun getViewRoot(activity: Activity): ViewGroup {
60 | val androidContent = activity.findViewById(android.R.id.content)
61 | return androidContent.parent.parent as ViewGroup
62 | }
63 |
64 | fun getStatusBarHeight(context: Context): Int {
65 | var result = 0
66 | val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
67 | if (resourceId > 0) {
68 | result = context.resources.getDimensionPixelSize(resourceId)
69 | }
70 | return result
71 | }
72 |
73 | fun pxToDp(px: Int): Int {
74 | val metrics = Resources.getSystem().displayMetrics
75 | return Math.round(px / (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
76 | }
77 |
78 | fun dpToPx(dp: Int): Int {
79 | val metrics = Resources.getSystem().displayMetrics
80 | return Math.round(dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
81 | }
82 |
83 | fun isViewLocatedAtHalfTopOfTheScreen(activity: Activity, targetView: View): Boolean{
84 | val screenHeight = getScreenHeight(activity)
85 | val positionTargetAxisY = getAxisYpositionOfViewOnScreen(targetView)
86 | return screenHeight/2 > positionTargetAxisY
87 | }
88 |
89 | fun isViewLocatedAtHalfLeftOfTheScreen(activity: Activity, targetView: View): Boolean{
90 | val screenWidth = getScreenWidth(activity)
91 | val positionTargetAxisX = getAxisXpositionOfViewOnScreen(targetView)
92 | return screenWidth/2 > positionTargetAxisX
93 | }
94 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
32 |
33 |
43 |
44 |
54 |
55 |
65 |
66 |
76 |
77 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BubbleShowCase - Android
2 | [  ](https://bintray.com/laboratorioec/Laboratorio-apps/BubbleShowCase/_latestVersion)
3 | []( https://android-arsenal.com/details/1/7119 )
4 |
5 | BubbleShowCase is an elegant and simple framework developed in Kotlin (usable also in Java) that let you to use informative bubbles to help your users pointing out different features of your application or in your App onboarding. The basic use of the framework consists on a target element passed as input which will be highlighted over a translucent background and pointed out by a customizable bubble.
6 |
7 |
8 |
9 | ## Getting started
10 |
11 | Add the library into your proyect is really easy, you just need to add this line in dependencies block in your app Gradle:
12 | ```groovy
13 | implementation 'com.elconfidencial.bubbleshowcase:bubbleshowcase:LATEST_VERSION'
14 | ```
15 | **NOTE:** You can check the LATEST_VERSION in the version badge at the top of this file.
16 |
17 | ## Usage
18 | ### Basic sample
19 |
20 | Create a new BubbleShowCase is pretty straightforward. It is only needed an instance of the current activity and the target view to carry it out.
21 | ```kotlin
22 | BubbleShowCaseBuilder(this) //Activity instance
23 | .title("foo") //Any title for the bubble view
24 | .targetView(view) //View to point out
25 | .show() //Display the ShowCase
26 | ```
27 | **NOTE:** If the target is not passed as input, the bubble will be located by default in the middle of the screen without arrows.
28 |
29 | ### Custom BubbleShowCase
30 |
31 | Below it is showed an example of a BubbleShowCase will all possible input params which can be custom.
32 |
33 | ```kotlin
34 | BubbleShowCaseBuilder(this) //Activity instance
35 | .title("foo") //Any title for the bubble view
36 | .description("bar") //More detailed description
37 | .arrowPosition(BubbleShowCase.ArrowPosition.RIGHT) //You can force the position of the arrow to change the location of the bubble.
38 | .backgroundColor(Color.GREEN) //Bubble background color
39 | .textColor(Color.BLACK) //Bubble Text color
40 | .titleTextSize(17) //Title text size in SP (default value 16sp)
41 | .descriptionTextSize(15) //Subtitle text size in SP (default value 14sp)
42 | .image(imageDrawable) //Bubble main image
43 | .closeActionImage(CloseImageDrawable) //Custom close action image
44 | .showOnce("BUBBLE_SHOW_CASE_ID") //Id to show only once the BubbleShowCase
45 | .listener(listener(object : BubbleShowCaseListener{ //Listener for user actions
46 | override fun onTargetClick(bubbleShowCase: BubbleShowCase) {
47 | //Called when the user clicks the target
48 | }
49 | override fun onCloseActionImageClick(bubbleShowCase: BubbleShowCase) {
50 | //Called when the user clicks the close button
51 | }
52 | override fun onBubbleClick(bubbleShowCase: BubbleShowCase) {
53 | //Called when the user clicks on the bubble
54 | }
55 |
56 | override fun onBackgroundDimClick(bubbleShowCase: BubbleShowCase) {
57 | //Called when the user clicks on the background dim
58 | }
59 | })
60 | .targetView(view) //View to point out
61 | .show() //Display the ShowCase
62 | ```
63 |
64 | **NOTE:** Set more than one arrow position is allowed. For that case, the arrows will be painted and the bubble will be located in the middle of the screen. It could be useful to indicate a swipe or a scroll movement.
65 |
66 | ### BubbleShowCaseSequence sample
67 |
68 | It has been also implemented a BubbleShowCaseSequence object in order to chain more than one BubbleShowCase. It could be useful when it is desired to point out several things at the same moment.
69 | ```kotlin
70 | BubbleShowCaseSequence()
71 | .addShowCase(firstShowCaseBuilder) //First BubbleShowCase to show
72 | .addShowCase(secondShowCaseBuilder) // This one will be showed when firstShowCase is dismissed
73 | .addShowCase(thirdShowCaseBuilder) // This one will be showed when secondShowCase is dismissed
74 | .show() //Display the ShowCaseSequence
75 | ```
76 |
77 | For more information and examples, please check our [sample app](/app).
78 | If you have any issues or feedback, please visit [issue section](https://github.com/ECLaboratorio/BubbleShowCase-Android/issues).
79 | Please feel free to collaborate with us making this framework as best as possible.
80 |
81 | ## License
82 |
83 | `BubbleShowCase-Android` is available under the MIT license. See the [LICENSE](/LICENSE) file for more info.
84 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/elconfidencial/bubbleshowcase/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import android.support.v7.app.AppCompatActivity
4 | import android.os.Bundle
5 | import android.support.v4.content.ContextCompat
6 | import android.support.v7.view.menu.ActionMenuItemView
7 | import android.view.Menu
8 | import android.view.MenuItem
9 | import android.widget.Toast
10 | import kotlinx.android.synthetic.main.activity_main.*
11 |
12 | class MainActivity : AppCompatActivity() {
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 | setUpListeners()
18 | }
19 |
20 | private fun setUpListeners(){
21 | buttonSimpleShowCase.setOnClickListener { getSimpleShowCaseBuilder().show() }
22 | buttonColorShowCase.setOnClickListener { getCustomColorShowCaseBuilder().show() }
23 | buttonTextSizeShowCase.setOnClickListener { getTextSizeShowCaseBuilder().show() }
24 | buttonArrowLeftShowCase.setOnClickListener { getArrowLeftShowCaseBuilder().show() }
25 | buttonArrowRightShowCase.setOnClickListener { getArrowRightShowCaseBuilder().show() }
26 | buttonEventListener.setOnClickListener { getListenerShowCaseBuilder().show() }
27 | buttonSequence.setOnClickListener { getSequence().show() }
28 | }
29 |
30 | //SHOW CASES GETTERS
31 |
32 | private fun getSimpleShowCaseBuilder(): BubbleShowCaseBuilder{
33 | return BubbleShowCaseBuilder(this)
34 | .title("Welcome!!!")
35 | .description("This is a simple BubbleShowCase with default values.")
36 | .targetView(buttonSimpleShowCase)
37 | }
38 |
39 | private fun getCustomColorShowCaseBuilder(): BubbleShowCaseBuilder{
40 | return BubbleShowCaseBuilder(this)
41 | .title("Custom your bubble style!")
42 | .description("It is possible to change the text color, background ... and you can even add an image into your bubble.")
43 | .backgroundColor(ContextCompat.getColor(this, R.color.colorBlueGray))
44 | .image(ContextCompat.getDrawable(this, R.drawable.ic_color)!!)
45 | .closeActionImage(ContextCompat.getDrawable(this, R.drawable.ic_close_black)!!)
46 | .textColor(ContextCompat.getColor(this, R.color.colorBlack))
47 | .targetView(buttonColorShowCase)
48 | }
49 |
50 | private fun getTextSizeShowCaseBuilder(): BubbleShowCaseBuilder{
51 | return BubbleShowCaseBuilder(this)
52 | .title("Change text sizes!")
53 | .description("You can also choose the best text size for you.")
54 | .backgroundColor(ContextCompat.getColor(this, R.color.colorTeal))
55 | .image(ContextCompat.getDrawable(this, R.drawable.ic_format_size)!!)
56 | .titleTextSize(18)
57 | .descriptionTextSize(16)
58 | .closeActionImage(null)
59 | .targetView(buttonTextSizeShowCase)
60 | }
61 |
62 | private fun getArrowLeftShowCaseBuilder(): BubbleShowCaseBuilder{
63 | return BubbleShowCaseBuilder(this)
64 | .title("Force the position of the bubble!")
65 | .description("You only have to specify in which side you want the arrow, and the bubble will be located depending on it.")
66 | .arrowPosition(BubbleShowCase.ArrowPosition.LEFT)
67 | .backgroundColor(ContextCompat.getColor(this, R.color.colorRed))
68 | .targetView(buttonArrowLeftShowCase)
69 | }
70 |
71 | private fun getArrowRightShowCaseBuilder(): BubbleShowCaseBuilder{
72 | return BubbleShowCaseBuilder(this)
73 | .title("Arrow set on right side this time :)")
74 | .arrowPosition(BubbleShowCase.ArrowPosition.RIGHT)
75 | .backgroundColor(ContextCompat.getColor(this, R.color.colorPink))
76 | .targetView(buttonArrowRightShowCase)
77 | }
78 |
79 |
80 | private fun getListenerShowCaseBuilder(): BubbleShowCaseBuilder{
81 | return BubbleShowCaseBuilder(this)
82 | .title("Listen user actions!")
83 | .description("You can detect when the user interacts with the different view elements to act consequently.")
84 | .backgroundColor(ContextCompat.getColor(this, R.color.colorOrange))
85 | .image(ContextCompat.getDrawable(this, R.drawable.ic_sentiment_satisfied)!!)
86 | .listener(object : BubbleShowCaseListener{
87 | override fun onBubbleClick(bubbleShowCase: BubbleShowCase) {
88 | Toast.makeText(this@MainActivity, "OnBubbleClick", Toast.LENGTH_SHORT).show()
89 | }
90 |
91 | override fun onBackgroundDimClick(bubbleShowCase: BubbleShowCase) {
92 | Toast.makeText(this@MainActivity, "OnBackgroundDimClick", Toast.LENGTH_SHORT).show()
93 | }
94 |
95 | override fun onTargetClick(bubbleShowCase: BubbleShowCase) {
96 | Toast.makeText(this@MainActivity, "OnTargetClick", Toast.LENGTH_SHORT).show()
97 | }
98 |
99 | override fun onCloseActionImageClick(bubbleShowCase: BubbleShowCase) {
100 | Toast.makeText(this@MainActivity, "OnClose", Toast.LENGTH_SHORT).show()
101 | }
102 | })
103 | .targetView(buttonEventListener)
104 | }
105 |
106 | private fun getSequence(): BubbleShowCaseSequence{
107 | return BubbleShowCaseSequence().addShowCases(listOf(
108 | getSimpleShowCaseBuilder(),
109 | getCustomColorShowCaseBuilder(),
110 | getTextSizeShowCaseBuilder(),
111 | getArrowLeftShowCaseBuilder(),
112 | getArrowRightShowCaseBuilder(),
113 | getListenerShowCaseBuilder()
114 | ))
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/BubbleShowCaseBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import android.app.Activity
4 | import android.graphics.drawable.Drawable
5 | import android.support.v4.content.ContextCompat
6 | import android.view.View
7 | import android.view.ViewTreeObserver
8 | import java.lang.ref.WeakReference
9 | import java.util.ArrayList
10 |
11 | /**
12 | * Created by jcampos on 04/09/2018.
13 | */
14 | class BubbleShowCaseBuilder{
15 |
16 | internal var mActivity: WeakReference? = null
17 | internal var mImage: Drawable? = null
18 | internal var mTitle: String? = null
19 | internal var mSubtitle: String? = null
20 | internal var mCloseAction: Drawable? = null
21 | internal var mBackgroundColor: Int? = null
22 | internal var mTextColor: Int? = null
23 | internal var mTitleTextSize: Int? = null
24 | internal var mSubtitleTextSize: Int? = null
25 | internal var mHighlightMode: BubbleShowCase.HighlightMode? = null
26 | internal var mDisableTargetClick: Boolean = false
27 | internal var mDisableCloseAction: Boolean = false
28 | internal var mShowOnce: String? = null
29 | internal var mIsFirstOfSequence: Boolean? = null
30 | internal var mIsLastOfSequence: Boolean? = null
31 | internal val mArrowPositionList = ArrayList()
32 | internal var mTargetView: WeakReference? = null
33 | internal var mBubbleShowCaseListener: BubbleShowCaseListener? = null
34 | internal var mSequenceShowCaseListener: SequenceShowCaseListener? = null
35 |
36 | private var onGlobalLayoutListenerTargetView: ViewTreeObserver.OnGlobalLayoutListener? = null
37 |
38 | /**
39 | * Builder constructor. It needs an instance of the current activity to convert it to a weak reference in order to avoid memory leaks
40 | */
41 | constructor(activity: Activity){
42 | mActivity = WeakReference(activity)
43 | }
44 |
45 | /**
46 | * Title of the BubbleShowCase. This text is bolded in the view.
47 | */
48 | fun title(title: String): BubbleShowCaseBuilder {
49 | mTitle = title
50 | return this
51 | }
52 |
53 | /**
54 | * Additional description of the BubbleShowCase. This text has a regular format
55 | */
56 | fun description(subtitle: String): BubbleShowCaseBuilder {
57 | mSubtitle = subtitle
58 | return this
59 | }
60 |
61 | /**
62 | * Image drawable to inserted as main image in the BubbleShowCase
63 | * - If this param is not passed, the BubbleShowCase will not have main image
64 | */
65 | fun image(image: Drawable): BubbleShowCaseBuilder {
66 | mImage = image
67 | return this
68 | }
69 |
70 | /**
71 | * Image resource id to insert the corresponding drawable as main image in the BubbleShowCase
72 | * - If this param is not passed, the BubbleShowCase will not have main image
73 | */
74 | fun imageResourceId(resId: Int): BubbleShowCaseBuilder {
75 | mImage = ContextCompat.getDrawable(mActivity!!.get(), resId)
76 | return this
77 | }
78 |
79 | /**
80 | * Image drawable to be inserted as close icon in the BubbleShowCase.
81 | * - If this param is not defined, a default close icon is displayed
82 | */
83 | fun closeActionImage(image: Drawable?): BubbleShowCaseBuilder {
84 | mCloseAction = image
85 | return this
86 | }
87 |
88 | /**
89 | * Image resource id to insert the corresponding drawable as close icon in the BubbleShowCase.
90 | * - If this param is not defined, a default close icon is displayed
91 | */
92 | fun closeActionImageResourceId(resId: Int): BubbleShowCaseBuilder {
93 | mCloseAction = ContextCompat.getDrawable(mActivity!!.get(), resId)
94 | return this
95 | }
96 |
97 |
98 | /**
99 | * Background color of the BubbleShowCase.
100 | * - #3F51B5 color will be set if this param is not defined
101 | */
102 | fun backgroundColor(color: Int): BubbleShowCaseBuilder {
103 | mBackgroundColor = color
104 | return this
105 | }
106 |
107 | /**
108 | * Background color of the BubbleShowCase.
109 | * - #3F51B5 color will be set if this param is not defined
110 | */
111 | fun backgroundColorResourceId(colorResId: Int): BubbleShowCaseBuilder {
112 | mBackgroundColor = ContextCompat.getColor(mActivity!!.get(), colorResId)
113 | return this
114 | }
115 |
116 | /**
117 | * Text color of the BubbleShowCase.
118 | * - White color will be set if this param is not defined
119 | */
120 | fun textColor(color: Int): BubbleShowCaseBuilder {
121 | mTextColor = color
122 | return this
123 | }
124 |
125 | /**
126 | * Text color of the BubbleShowCase.
127 | * - White color will be set if this param is not defined
128 | */
129 | fun textColorResourceId(colorResId: Int): BubbleShowCaseBuilder {
130 | mTextColor = ContextCompat.getColor(mActivity!!.get(), colorResId)
131 | return this
132 | }
133 |
134 | /**
135 | * Title text size in SP.
136 | * - Default value -> 16 sp
137 | */
138 | fun titleTextSize(textSize: Int): BubbleShowCaseBuilder {
139 | mTitleTextSize = textSize
140 | return this
141 | }
142 |
143 | /**
144 | * Description text size in SP.
145 | * - Default value -> 14 sp
146 | */
147 | fun descriptionTextSize(textSize: Int): BubbleShowCaseBuilder {
148 | mSubtitleTextSize = textSize
149 | return this
150 | }
151 |
152 | /**
153 | * If an unique id is passed in this function, this BubbleShowCase will only be showed once
154 | * - ID to identify the BubbleShowCase
155 | */
156 | fun showOnce(id: String): BubbleShowCaseBuilder {
157 | mShowOnce = id
158 | return this
159 | }
160 |
161 | /**
162 | * Target view to be highlighted. Set a TargetView is essential to figure out BubbleShowCase position
163 | * - If a target view is not defined, the BubbleShowCase final position will be the center of the screen
164 | */
165 | fun targetView(targetView: View): BubbleShowCaseBuilder {
166 | mTargetView = WeakReference(targetView)
167 | return this
168 | }
169 |
170 | /**
171 | * If this variable is true, when user clicks on the target, the showcase will not be dismissed
172 | * Default value -> false
173 | */
174 | fun disableTargetClick(isDisabled: Boolean): BubbleShowCaseBuilder{
175 | mDisableTargetClick = isDisabled
176 | return this
177 | }
178 |
179 | /**
180 | * If this variable is true, close action button will be gone
181 | * Default value -> false
182 | */
183 | fun disableCloseAction(isDisabled: Boolean): BubbleShowCaseBuilder{
184 | mDisableCloseAction = isDisabled
185 | return this
186 | }
187 |
188 | /**
189 | * Insert an arrowPosition to force the position of the BubbleShowCase.
190 | * - ArrowPosition enum values: LEFT, RIGHT, TOP and DOWN
191 | * - If an arrow position is not defined, the BubbleShowCase will be set in a default position depending on the targetView
192 | */
193 | fun arrowPosition(arrowPosition: BubbleShowCase.ArrowPosition): BubbleShowCaseBuilder {
194 | mArrowPositionList.clear()
195 | mArrowPositionList.add(arrowPosition)
196 | return this
197 | }
198 |
199 | /**
200 | * Insert a set of arrowPosition to force the position of the BubbleShowCase.
201 | * - ArrowPosition enum values: LEFT, RIGHT, TOP and DOWN
202 | * - If the number of arrow positions is 0 or more than 1, BubbleShowCase will be set on the center of the screen
203 | */
204 | fun arrowPosition(arrowPosition: List): BubbleShowCaseBuilder {
205 | mArrowPositionList.clear()
206 | mArrowPositionList.addAll(arrowPosition)
207 | return this
208 | }
209 |
210 | /**
211 | * Highlight mode. It represents the way that the target view will be highlighted
212 | * - VIEW_LAYOUT: Default value. All the view box is highlighted (the rectangle where the view is contained). Example: For a TextView, all the element is highlighted (characters and background)
213 | * - VIEW_SURFACE: Only the view surface is highlighted, but not the background. Example: For a TextView, only the characters will be highlighted
214 | */
215 | fun highlightMode(highlightMode: BubbleShowCase.HighlightMode): BubbleShowCaseBuilder {
216 | mHighlightMode = highlightMode
217 | return this
218 | }
219 |
220 | /**
221 | * Add a BubbleShowCaseListener in order to listen the user actions:
222 | * - onTargetClick -> It is triggered when the user clicks on the target view
223 | * - onCloseClick -> It is triggered when the user clicks on the close icon
224 | */
225 | fun listener(bubbleShowCaseListener: BubbleShowCaseListener): BubbleShowCaseBuilder {
226 | mBubbleShowCaseListener = bubbleShowCaseListener
227 | return this
228 | }
229 |
230 | /**
231 | * Add a sequence listener in order to know when a BubbleShowCase has been dismissed to show another one
232 | */
233 | internal fun sequenceListener(sequenceShowCaseListener: SequenceShowCaseListener): BubbleShowCaseBuilder {
234 | mSequenceShowCaseListener = sequenceShowCaseListener
235 | return this
236 | }
237 |
238 | internal fun isFirstOfSequence(isFirst: Boolean): BubbleShowCaseBuilder{
239 | mIsFirstOfSequence = isFirst
240 | return this
241 | }
242 |
243 | internal fun isLastOfSequence(isLast: Boolean): BubbleShowCaseBuilder{
244 | mIsLastOfSequence = isLast
245 | return this
246 | }
247 |
248 | /**
249 | * Build the BubbleShowCase object from the builder one
250 | */
251 | private fun build(): BubbleShowCase {
252 | if(mIsFirstOfSequence ==null)
253 | mIsFirstOfSequence = true
254 | if(mIsLastOfSequence ==null)
255 | mIsLastOfSequence = true
256 |
257 | return BubbleShowCase(this)
258 | }
259 |
260 | /**
261 | * Show the BubbleShowCase using the params added previously
262 | */
263 | fun show(): BubbleShowCase{
264 | val bubbleShowCase = build()
265 | if (mTargetView != null) {
266 | val targetView = mTargetView!!.get()
267 | if (targetView!!.height == 0 || targetView.width == 0) {
268 | //If the view is not already painted, we wait for it waiting for view changes using OnGlobalLayoutListener
269 | onGlobalLayoutListenerTargetView = ViewTreeObserver.OnGlobalLayoutListener {
270 | bubbleShowCase.show()
271 | targetView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListenerTargetView)
272 | }
273 | targetView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListenerTargetView)
274 | } else {
275 | bubbleShowCase.show()
276 | }
277 | } else {
278 | bubbleShowCase.show()
279 | }
280 | return bubbleShowCase
281 | }
282 |
283 | }
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/BubbleMessageView.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Paint
6 | import android.graphics.Path
7 | import android.graphics.RectF
8 | import android.graphics.drawable.Drawable
9 | import android.support.constraint.ConstraintLayout
10 | import android.support.v4.content.ContextCompat
11 | import android.util.AttributeSet
12 | import android.util.TypedValue
13 | import android.view.View
14 | import android.widget.ImageView
15 | import android.widget.TextView
16 | import java.lang.ref.WeakReference
17 |
18 | import java.util.ArrayList
19 |
20 | /**
21 | * Created by jcampos on 05/09/2018.
22 | */
23 |
24 | class BubbleMessageView : ConstraintLayout {
25 |
26 | private val WIDTH_ARROW = 20
27 |
28 | private var itemView: View? = null
29 |
30 | private var imageViewIcon: ImageView? = null
31 | private var textViewTitle: TextView? = null
32 | private var textViewSubtitle: TextView? = null
33 | private var imageViewClose: ImageView? = null
34 | private var showCaseMessageViewLayout: ConstraintLayout? = null
35 |
36 | private var targetViewScreenLocation: RectF? = null
37 | private var mBackgroundColor: Int = ContextCompat.getColor(context, R.color.blue_default)
38 | private var arrowPositionList = ArrayList()
39 |
40 | private var paint: Paint? = null
41 |
42 | constructor(context: Context) : super(context) {
43 | initView()
44 | }
45 |
46 | constructor(context: Context, builder: Builder) : super(context) {
47 | initView()
48 | setAttributes(builder)
49 | setBubbleListener(builder)
50 | }
51 |
52 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
53 | initView()
54 | }
55 |
56 | private fun initView() {
57 | setWillNotDraw(false)
58 |
59 | inflateXML()
60 | bindViews()
61 | }
62 |
63 | private fun inflateXML() {
64 | itemView = inflate(context, R.layout.view_bubble_message, this)
65 | }
66 |
67 | private fun bindViews() {
68 | imageViewIcon = findViewById(R.id.imageViewShowCase)
69 | imageViewClose = findViewById(R.id.imageViewShowCaseClose)
70 | textViewTitle = findViewById(R.id.textViewShowCaseTitle)
71 | textViewSubtitle = findViewById(R.id.textViewShowCaseText)
72 | showCaseMessageViewLayout = findViewById(R.id.showCaseMessageViewLayout)
73 | }
74 |
75 | private fun setAttributes(builder: Builder){
76 | if(builder.mImage!=null){
77 | imageViewIcon?.visibility = View.VISIBLE
78 | imageViewIcon?.setImageDrawable(builder.mImage!!)
79 | }
80 | if(builder.mCloseAction!=null){
81 | imageViewClose?.visibility = View.VISIBLE
82 | imageViewClose?.setImageDrawable(builder.mCloseAction!!)
83 | }
84 |
85 | if(builder.mDisableCloseAction!=null && builder.mDisableCloseAction!!){
86 | imageViewClose?.visibility = View.INVISIBLE
87 | }
88 |
89 | builder.mTitle?.let {
90 | textViewTitle?.visibility = View.VISIBLE
91 | textViewTitle?.text = builder.mTitle
92 | }
93 | builder.mSubtitle?.let {
94 | textViewSubtitle?.visibility = View.VISIBLE
95 | textViewSubtitle?.text = builder.mSubtitle
96 | }
97 | builder.mTextColor?.let {
98 | textViewTitle?.setTextColor(builder.mTextColor!!)
99 | textViewSubtitle?.setTextColor(builder.mTextColor!!)
100 | }
101 | builder.mTitleTextSize?.let {
102 | textViewTitle?.setTextSize(TypedValue.COMPLEX_UNIT_SP, builder.mTitleTextSize!!.toFloat())
103 | }
104 | builder.mSubtitleTextSize?.let {
105 | textViewSubtitle?.setTextSize(TypedValue.COMPLEX_UNIT_SP, builder.mSubtitleTextSize!!.toFloat())
106 | }
107 | builder.mBackgroundColor?.let { mBackgroundColor = builder.mBackgroundColor!! }
108 | arrowPositionList = builder.mArrowPosition
109 | targetViewScreenLocation = builder.mTargetViewScreenLocation
110 | }
111 |
112 | private fun setBubbleListener(builder: Builder){
113 | imageViewClose?.setOnClickListener {builder.mListener?.onCloseActionImageClick()}
114 | itemView?.setOnClickListener {builder.mListener?.onBubbleClick()}
115 | }
116 |
117 |
118 | //END REGION
119 |
120 | //REGION AUX FUNCTIONS
121 |
122 | private fun getViewWidth(): Int = width
123 |
124 | private fun getMargin(): Int = ScreenUtils.dpToPx(20)
125 |
126 | private fun getSecurityArrowMargin(): Int = getMargin() + ScreenUtils.dpToPx(2 * WIDTH_ARROW / 3)
127 |
128 | //END REGION
129 |
130 | //REGION SHOW ITEM
131 |
132 | override fun onDraw(canvas: Canvas) {
133 | super.onDraw(canvas)
134 |
135 | prepareToDraw()
136 | drawRectangle(canvas)
137 |
138 | for (arrowPosition in arrowPositionList) {
139 | drawArrow(canvas, arrowPosition, targetViewScreenLocation)
140 | }
141 | }
142 |
143 | private fun prepareToDraw() {
144 | paint = Paint(Paint.ANTI_ALIAS_FLAG)
145 | paint!!.color = mBackgroundColor
146 | paint!!.style = Paint.Style.FILL
147 | paint!!.strokeWidth = 4.0f
148 | }
149 |
150 | private fun drawRectangle(canvas: Canvas) {
151 | val rect = RectF(getMargin().toFloat(),
152 | getMargin().toFloat(),
153 | getViewWidth() - getMargin().toFloat(),
154 | height - getMargin().toFloat())
155 | canvas.drawRoundRect(rect, 10f, 10f, paint!!)
156 | }
157 |
158 | private fun drawArrow(canvas: Canvas, arrowPosition: BubbleShowCase.ArrowPosition, targetViewLocationOnScreen: RectF?) {
159 | val xPosition: Int
160 | val yPosition: Int
161 |
162 | when (arrowPosition) {
163 | BubbleShowCase.ArrowPosition.LEFT -> {
164 | xPosition = getMargin()
165 | yPosition = if(targetViewLocationOnScreen!=null) getArrowVerticalPositionDependingOnTarget(targetViewLocationOnScreen) else height / 2
166 | }
167 | BubbleShowCase.ArrowPosition.RIGHT -> {
168 | xPosition = getViewWidth() - getMargin()
169 | yPosition = if(targetViewLocationOnScreen!=null) getArrowVerticalPositionDependingOnTarget(targetViewLocationOnScreen) else height / 2
170 | }
171 | BubbleShowCase.ArrowPosition.TOP -> {
172 | xPosition = if(targetViewLocationOnScreen!=null) getArrowHorizontalPositionDependingOnTarget(targetViewLocationOnScreen) else width / 2
173 | yPosition = getMargin()
174 | }
175 | BubbleShowCase.ArrowPosition.BOTTOM -> {
176 | xPosition = if(targetViewLocationOnScreen!=null) getArrowHorizontalPositionDependingOnTarget(targetViewLocationOnScreen) else width / 2
177 | yPosition = height - getMargin()
178 | }
179 | }
180 |
181 | drawRhombus(canvas, paint, xPosition, yPosition, ScreenUtils.dpToPx(WIDTH_ARROW))
182 | }
183 |
184 | private fun getArrowHorizontalPositionDependingOnTarget(targetViewLocationOnScreen: RectF?): Int {
185 | val xPosition: Int
186 | when {
187 | isOutOfRightBound(targetViewLocationOnScreen) -> xPosition = width - getSecurityArrowMargin()
188 | isOutOfLeftBound(targetViewLocationOnScreen) -> xPosition = getSecurityArrowMargin()
189 | else -> xPosition = Math.round(targetViewLocationOnScreen!!.centerX() - ScreenUtils.getAxisXpositionOfViewOnScreen(this))
190 | }
191 | return xPosition
192 | }
193 |
194 | private fun getArrowVerticalPositionDependingOnTarget(targetViewLocationOnScreen: RectF?): Int {
195 | val yPosition: Int
196 | when {
197 | isOutOfBottomBound(targetViewLocationOnScreen) -> yPosition = height - getSecurityArrowMargin()
198 | isOutOfTopBound(targetViewLocationOnScreen) -> yPosition = getSecurityArrowMargin()
199 | else -> yPosition = Math.round(targetViewLocationOnScreen!!.centerY() + ScreenUtils.getStatusBarHeight(context) - ScreenUtils.getAxisYpositionOfViewOnScreen(this))
200 | }
201 | return yPosition
202 | }
203 |
204 | private fun isOutOfRightBound(targetViewLocationOnScreen: RectF?): Boolean {
205 | return targetViewLocationOnScreen!!.centerX() > ScreenUtils.getAxisXpositionOfViewOnScreen(this) + width - getSecurityArrowMargin()
206 | }
207 |
208 | private fun isOutOfLeftBound(targetViewLocationOnScreen: RectF?): Boolean {
209 | return targetViewLocationOnScreen!!.centerX() < ScreenUtils.getAxisXpositionOfViewOnScreen(this) + getSecurityArrowMargin()
210 | }
211 |
212 | private fun isOutOfBottomBound(targetViewLocationOnScreen: RectF?): Boolean {
213 | return targetViewLocationOnScreen!!.centerY() > ScreenUtils.getAxisYpositionOfViewOnScreen(this) + height - getSecurityArrowMargin() - ScreenUtils.getStatusBarHeight(context)
214 | }
215 |
216 | private fun isOutOfTopBound(targetViewLocationOnScreen: RectF?): Boolean {
217 | return targetViewLocationOnScreen!!.centerY() < ScreenUtils.getAxisYpositionOfViewOnScreen(this) + getSecurityArrowMargin() - ScreenUtils.getStatusBarHeight(context)
218 | }
219 |
220 |
221 | private fun drawRhombus(canvas: Canvas, paint: Paint?, x: Int, y: Int, width: Int) {
222 | val halfRhombusWidth = width / 2
223 |
224 | val path = Path()
225 | path.moveTo(x.toFloat(), (y + halfRhombusWidth).toFloat()) // Top
226 | path.lineTo((x - halfRhombusWidth).toFloat(), y.toFloat()) // Left
227 | path.lineTo(x.toFloat(), (y - halfRhombusWidth).toFloat()) // Bottom
228 | path.lineTo((x + halfRhombusWidth).toFloat(), y.toFloat()) // Right
229 | path.lineTo(x.toFloat(), (y + halfRhombusWidth).toFloat()) // Back to Top
230 | path.close()
231 |
232 | canvas.drawPath(path, paint!!)
233 | }
234 |
235 |
236 | //END REGION
237 |
238 | /**
239 | * Builder for BubbleMessageView class
240 | */
241 | class Builder{
242 | lateinit var mContext: WeakReference
243 | var mTargetViewScreenLocation: RectF? = null
244 | var mImage: Drawable? = null
245 | var mDisableCloseAction: Boolean? = null
246 | var mTitle: String? = null
247 | var mSubtitle: String? = null
248 | var mCloseAction: Drawable? = null
249 | var mBackgroundColor: Int? = null
250 | var mTextColor: Int? = null
251 | var mTitleTextSize: Int? = null
252 | var mSubtitleTextSize: Int? = null
253 | var mArrowPosition = ArrayList()
254 | var mListener: OnBubbleMessageViewListener? = null
255 |
256 | fun from(context: Context): Builder{
257 | mContext = WeakReference(context)
258 | return this
259 | }
260 |
261 | fun title(title: String?): Builder {
262 | mTitle = title
263 | return this
264 | }
265 |
266 | fun subtitle(subtitle: String?): Builder {
267 | mSubtitle = subtitle
268 | return this
269 | }
270 |
271 | fun image(image: Drawable?): Builder {
272 | mImage = image
273 | return this
274 | }
275 |
276 | fun closeActionImage(image: Drawable?): Builder {
277 | mCloseAction = image
278 | return this
279 | }
280 |
281 | fun disableCloseAction(isDisabled: Boolean): Builder {
282 | mDisableCloseAction = isDisabled
283 | return this
284 | }
285 |
286 | fun targetViewScreenLocation(targetViewLocationOnScreen: RectF): Builder{
287 | mTargetViewScreenLocation = targetViewLocationOnScreen
288 | return this
289 | }
290 |
291 | fun backgroundColor(backgroundColor: Int?): Builder {
292 | mBackgroundColor = backgroundColor
293 | return this
294 | }
295 |
296 | fun textColor(textColor: Int?): Builder {
297 | mTextColor = textColor
298 | return this
299 | }
300 |
301 | fun titleTextSize(textSize: Int?): Builder {
302 | mTitleTextSize = textSize
303 | return this
304 | }
305 |
306 | fun subtitleTextSize(textSize: Int?): Builder {
307 | mSubtitleTextSize = textSize
308 | return this
309 | }
310 |
311 | fun arrowPosition(arrowPosition: List): Builder {
312 | mArrowPosition.clear()
313 | mArrowPosition.addAll(arrowPosition)
314 | return this
315 | }
316 |
317 | fun listener(listener: OnBubbleMessageViewListener?): Builder {
318 | mListener = listener
319 | return this
320 | }
321 |
322 | fun build(): BubbleMessageView{
323 | return BubbleMessageView(mContext.get()!!, this)
324 | }
325 | }
326 | }
--------------------------------------------------------------------------------
/bubbleshowcase/src/main/java/com/elconfidencial/bubbleshowcase/BubbleShowCase.kt:
--------------------------------------------------------------------------------
1 | package com.elconfidencial.bubbleshowcase
2 |
3 |
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.Context.MODE_PRIVATE
7 | import android.content.SharedPreferences
8 | import android.graphics.Bitmap
9 | import android.graphics.RectF
10 | import android.graphics.drawable.Drawable
11 | import android.os.Build
12 | import android.os.Handler
13 | import android.support.v4.content.ContextCompat
14 | import android.view.View
15 | import android.view.ViewGroup
16 | import android.widget.ImageView
17 | import android.widget.RelativeLayout
18 | import java.lang.ref.WeakReference
19 |
20 |
21 | /**
22 | * Created by jcampos on 04/09/2018.
23 | */
24 |
25 | class BubbleShowCase(builder: BubbleShowCaseBuilder){
26 | private val SHARED_PREFS_NAME = "BubbleShowCasePrefs"
27 |
28 | private val FOREGROUND_LAYOUT_ID = 731
29 |
30 | private val DURATION_SHOW_CASE_ANIMATION = 200 //ms
31 | private val DURATION_BACKGROUND_ANIMATION = 700 //ms
32 | private val DURATION_BEATING_ANIMATION = 700 //ms
33 |
34 | private val MAX_WIDTH_MESSAGE_VIEW_TABLET = 420 //dp
35 |
36 | /**
37 | * Enum class which corresponds to each valid position for the BubbleMessageView arrow
38 | */
39 | enum class ArrowPosition {
40 | TOP, BOTTOM, LEFT, RIGHT
41 | }
42 |
43 | /**
44 | * Highlight mode. It represents the way that the target view will be highlighted
45 | * - VIEW_LAYOUT: Default value. All the view box is highlighted (the rectangle where the view is contained). Example: For a TextView, all the element is highlighted (characters and background)
46 | * - VIEW_SURFACE: Only the view surface is highlighted, but not the background. Example: For a TextView, only the characters will be highlighted
47 | */
48 | enum class HighlightMode {
49 | VIEW_LAYOUT, VIEW_SURFACE
50 | }
51 |
52 |
53 | private val mActivity: WeakReference = builder.mActivity!!
54 |
55 | //BubbleMessageView params
56 | private val mImage: Drawable? = builder.mImage
57 | private val mTitle: String? = builder.mTitle
58 | private val mSubtitle: String? = builder.mSubtitle
59 | private val mCloseAction: Drawable? = builder.mCloseAction
60 | private val mBackgroundColor: Int? = builder.mBackgroundColor
61 | private val mTextColor: Int? = builder.mTextColor
62 | private val mTitleTextSize: Int? = builder.mTitleTextSize
63 | private val mSubtitleTextSize: Int? = builder.mSubtitleTextSize
64 | private val mShowOnce: String? = builder.mShowOnce
65 | private val mDisableTargetClick: Boolean = builder.mDisableTargetClick
66 | private val mDisableCloseAction: Boolean = builder.mDisableCloseAction
67 | private val mHighlightMode: BubbleShowCase.HighlightMode? = builder.mHighlightMode
68 | private val mArrowPositionList: MutableList = builder.mArrowPositionList
69 | private val mTargetView: WeakReference? = builder.mTargetView
70 | private val mBubbleShowCaseListener: BubbleShowCaseListener? = builder.mBubbleShowCaseListener
71 |
72 | //Sequence params
73 | private val mSequenceListener: SequenceShowCaseListener? = builder.mSequenceShowCaseListener
74 | private val isFirstOfSequence: Boolean = builder.mIsFirstOfSequence!!
75 | private val isLastOfSequence: Boolean = builder.mIsLastOfSequence!!
76 |
77 | //References
78 | private var backgroundDimLayout: RelativeLayout? = null
79 | private var bubbleMessageViewBuilder: BubbleMessageView.Builder? = null
80 |
81 | fun show(){
82 | if(mShowOnce != null){
83 | if(isBubbleShowCaseHasBeenShowedPreviously(mShowOnce)){
84 | notifyDismissToSequenceListener()
85 | return
86 | } else{
87 | registerBubbleShowCaseInPreferences(mShowOnce)
88 | }
89 | }
90 |
91 | val rootView = getViewRoot(mActivity.get()!!)
92 | backgroundDimLayout = getBackgroundDimLayout()
93 | setBackgroundDimListener(backgroundDimLayout)
94 | bubbleMessageViewBuilder = getBubbleMessageViewBuilder()
95 |
96 | if (mTargetView != null && mArrowPositionList.size <= 1) {
97 | //Wait until the end of the layout animation, to avoid problems with pending scrolls or view movements
98 | val handler = Handler()
99 | handler.postDelayed({
100 | val target = mTargetView.get()!!
101 | //If the arrow list is empty, the arrow position is set by default depending on the targetView position on the screen
102 | if(mArrowPositionList.isEmpty()){
103 | if(ScreenUtils.isViewLocatedAtHalfTopOfTheScreen(mActivity.get()!!, target)) mArrowPositionList.add(ArrowPosition.TOP) else mArrowPositionList.add(ArrowPosition.BOTTOM)
104 | bubbleMessageViewBuilder = getBubbleMessageViewBuilder()
105 | }
106 |
107 | if (isVisibleOnScreen(target)) {
108 | addTargetViewAtBackgroundDimLayout(target, backgroundDimLayout)
109 | addBubbleMessageViewDependingOnTargetView(target, bubbleMessageViewBuilder!!, backgroundDimLayout)
110 | } else {
111 | dismiss()
112 | }
113 | }, DURATION_BACKGROUND_ANIMATION.toLong())
114 | } else {
115 | addBubbleMessageViewOnScreenCenter(bubbleMessageViewBuilder!!, backgroundDimLayout)
116 | }
117 | if(isFirstOfSequence){
118 | //Add the background dim layout above the root view
119 | val animation = AnimationUtils.getFadeInAnimation(0, DURATION_BACKGROUND_ANIMATION)
120 | backgroundDimLayout?.let { rootView.addView(AnimationUtils.setAnimationToView(backgroundDimLayout!!, animation)) }
121 | }
122 | }
123 |
124 | fun dismiss() {
125 | if (backgroundDimLayout != null && isLastOfSequence) {
126 | //Remove background dim layout if the BubbleShowCase is the last of the sequence
127 | finishSequence()
128 | } else {
129 | //Remove all the views created over the background dim layout waiting for the next BubbleShowCsse in the sequence
130 | backgroundDimLayout?.removeAllViews()
131 | }
132 | notifyDismissToSequenceListener()
133 | }
134 |
135 | fun finishSequence() {
136 | val rootView = getViewRoot(mActivity.get()!!)
137 | rootView.removeView(backgroundDimLayout)
138 | backgroundDimLayout = null
139 | }
140 |
141 | private fun notifyDismissToSequenceListener(){
142 | mSequenceListener?.let { mSequenceListener.onDismiss() }
143 | }
144 |
145 | private fun getViewRoot(activity: Activity): ViewGroup {
146 | val androidContent = activity.findViewById(android.R.id.content)
147 | return androidContent.parent.parent as ViewGroup
148 | }
149 |
150 | private fun getBackgroundDimLayout(): RelativeLayout {
151 | if(mActivity.get()!!.findViewById(FOREGROUND_LAYOUT_ID) != null)
152 | return mActivity.get()!!.findViewById(FOREGROUND_LAYOUT_ID)
153 | val backgroundLayout = RelativeLayout(mActivity.get()!!)
154 | backgroundLayout.id = FOREGROUND_LAYOUT_ID
155 | backgroundLayout.layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
156 | backgroundLayout.setBackgroundColor(ContextCompat.getColor(mActivity.get()!!, R.color.transparent_grey))
157 | backgroundLayout.isClickable = true
158 | return backgroundLayout
159 | }
160 |
161 | private fun setBackgroundDimListener(backgroundDimLayout: RelativeLayout?){
162 | backgroundDimLayout?.setOnClickListener { mBubbleShowCaseListener?.onBackgroundDimClick(this) }
163 | }
164 |
165 | private fun getBubbleMessageViewBuilder(): BubbleMessageView.Builder{
166 | return BubbleMessageView.Builder()
167 | .from(mActivity.get()!!)
168 | .arrowPosition(mArrowPositionList)
169 | .backgroundColor(mBackgroundColor)
170 | .textColor(mTextColor)
171 | .titleTextSize(mTitleTextSize)
172 | .subtitleTextSize(mSubtitleTextSize)
173 | .title(mTitle)
174 | .subtitle(mSubtitle)
175 | .image(mImage)
176 | .closeActionImage(mCloseAction)
177 | .disableCloseAction(mDisableCloseAction)
178 | .listener(object : OnBubbleMessageViewListener {
179 | override fun onBubbleClick() {
180 | mBubbleShowCaseListener?.onBubbleClick(this@BubbleShowCase)
181 | }
182 |
183 | override fun onCloseActionImageClick() {
184 | dismiss()
185 | mBubbleShowCaseListener?.onCloseActionImageClick(this@BubbleShowCase)
186 | }
187 | })
188 | }
189 |
190 | private fun isBubbleShowCaseHasBeenShowedPreviously(id: String): Boolean{
191 | val mPrefs = mActivity.get()!!.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE)
192 | return getString(mPrefs, id)!=null
193 | }
194 |
195 | private fun registerBubbleShowCaseInPreferences(id: String){
196 | val mPrefs = mActivity.get()!!.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE)
197 | setString(mPrefs, id, id)
198 | }
199 |
200 | private fun getString(mPrefs: SharedPreferences, key: String): String? {
201 | return mPrefs.getString(key, null)
202 | }
203 |
204 | private fun setString(mPrefs: SharedPreferences, key: String, value: String) {
205 | val editor = mPrefs.edit()
206 | editor.putString(key, value)
207 | editor.apply()
208 | }
209 |
210 |
211 | /**
212 | * This function takes a screenshot of the targetView, creating an ImageView from it. This new ImageView is also set on the layout passed by param
213 | */
214 | private fun addTargetViewAtBackgroundDimLayout(targetView: View?, backgroundDimLayout: RelativeLayout?) {
215 | if(targetView==null) return
216 |
217 | val targetScreenshot = takeScreenshot(targetView, mHighlightMode)
218 | val targetScreenshotView = ImageView(mActivity.get()!!)
219 | targetScreenshotView.setImageBitmap(targetScreenshot)
220 | targetScreenshotView.setOnClickListener {
221 | if(!mDisableTargetClick)
222 | dismiss()
223 | mBubbleShowCaseListener?.onTargetClick(this)
224 | }
225 |
226 | val targetViewParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
227 | targetViewParams.setMargins(getXposition(targetView), getYposition(targetView), getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width), 0)
228 | backgroundDimLayout?.addView(AnimationUtils.setBouncingAnimation(targetScreenshotView, 0, DURATION_BEATING_ANIMATION), targetViewParams)
229 | }
230 |
231 | /**
232 | * This function creates the BubbleMessageView depending the position of the target and the desired arrow position. This new view is also set on the layout passed by param
233 | */
234 | private fun addBubbleMessageViewDependingOnTargetView(targetView: View?, bubbleMessageViewBuilder: BubbleMessageView.Builder, backgroundDimLayout: RelativeLayout?) {
235 | if(targetView==null) return
236 | val showCaseParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
237 |
238 | when (bubbleMessageViewBuilder.mArrowPosition[0]) {
239 | ArrowPosition.LEFT -> {
240 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
241 | if(ScreenUtils.isViewLocatedAtHalfTopOfTheScreen(mActivity.get()!!, targetView)){
242 | showCaseParams.setMargins(
243 | getXposition(targetView) + targetView.width,
244 | getYposition(targetView),
245 | if(isTablet()) getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width) - getMessageViewWidthOnTablet(getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width)) else 0,
246 | 0)
247 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
248 | } else{
249 | showCaseParams.setMargins(
250 | getXposition(targetView) + targetView.width,
251 | 0,
252 | if(isTablet()) getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width) - getMessageViewWidthOnTablet(getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width)) else 0,
253 | getScreenHeight(mActivity.get()!!) - getYposition(targetView) - targetView.height)
254 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
255 | }
256 | }
257 | ArrowPosition.RIGHT -> {
258 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
259 | if(ScreenUtils.isViewLocatedAtHalfTopOfTheScreen(mActivity.get()!!, targetView)){
260 | showCaseParams.setMargins(
261 | if(isTablet()) getXposition(targetView) - getMessageViewWidthOnTablet(getXposition(targetView)) else 0,
262 | getYposition(targetView),
263 | getScreenWidth(mActivity.get()!!) - getXposition(targetView),
264 | 0)
265 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
266 | } else{
267 | showCaseParams.setMargins(
268 | if(isTablet()) getXposition(targetView) - getMessageViewWidthOnTablet(getXposition(targetView)) else 0,
269 | 0,
270 | getScreenWidth(mActivity.get()!!) - getXposition(targetView),
271 | getScreenHeight(mActivity.get()!!) - getYposition(targetView) - targetView.height)
272 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
273 | }
274 | }
275 | ArrowPosition.TOP -> {
276 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
277 | if(ScreenUtils.isViewLocatedAtHalfLeftOfTheScreen(mActivity.get()!!, targetView)){
278 | showCaseParams.setMargins(
279 | if (isTablet()) getXposition(targetView) else 0,
280 | getYposition(targetView) + targetView.height,
281 | if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(targetView) - getMessageViewWidthOnTablet(getScreenWidth(mActivity.get()!!) - getXposition(targetView)) else 0,
282 | 0)
283 | } else{
284 | showCaseParams.setMargins(
285 | if (isTablet()) getXposition(targetView) + targetView.width - getMessageViewWidthOnTablet(getXposition(targetView)) else 0,
286 | getYposition(targetView) + targetView.height,
287 | if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(targetView) - targetView.width else 0,
288 | 0)
289 | }
290 | }
291 | ArrowPosition.BOTTOM -> {
292 | showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
293 | if(ScreenUtils.isViewLocatedAtHalfLeftOfTheScreen(mActivity.get()!!, targetView)){
294 | showCaseParams.setMargins(
295 | if (isTablet()) getXposition(targetView) else 0,
296 | 0,
297 | if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(targetView) - getMessageViewWidthOnTablet(getScreenWidth(mActivity.get()!!) - getXposition(targetView)) else 0,
298 | getScreenHeight(mActivity.get()!!) - getYposition(targetView))
299 | } else {
300 | showCaseParams.setMargins(
301 | if (isTablet()) getXposition(targetView) + targetView.width - getMessageViewWidthOnTablet(getXposition(targetView)) else 0,
302 | 0,
303 | if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(targetView) - targetView.width else 0,
304 | getScreenHeight(mActivity.get()!!) - getYposition(targetView))
305 | }
306 | }
307 | }
308 |
309 | val bubbleMessageView = bubbleMessageViewBuilder.targetViewScreenLocation(RectF(
310 | getXposition(targetView).toFloat(),
311 | getYposition(targetView).toFloat(),
312 | getXposition(targetView).toFloat() + targetView.width,
313 | getYposition(targetView).toFloat() + targetView.height))
314 | .build()
315 |
316 | bubbleMessageView.id = createViewId()
317 | val animation = AnimationUtils.getScaleAnimation(0, DURATION_SHOW_CASE_ANIMATION)
318 | backgroundDimLayout?.addView(AnimationUtils.setAnimationToView(bubbleMessageView, animation), showCaseParams)
319 | }
320 |
321 | /**
322 | * This function creates a BubbleMessageView and it is set on the center of the layout passed by param
323 | */
324 | private fun addBubbleMessageViewOnScreenCenter(bubbleMessageViewBuilder: BubbleMessageView.Builder, backgroundDimLayout: RelativeLayout?) {
325 | val showCaseParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
326 | showCaseParams.addRule(RelativeLayout. CENTER_VERTICAL)
327 | val bubbleMessageView: BubbleMessageView = bubbleMessageViewBuilder.build()
328 | bubbleMessageView.id = createViewId()
329 | if(isTablet()) showCaseParams.setMargins(
330 | if (isTablet()) getScreenWidth(mActivity.get()!!)/2 - ScreenUtils.dpToPx(MAX_WIDTH_MESSAGE_VIEW_TABLET)/2 else 0,
331 | 0,
332 | if (isTablet()) getScreenWidth(mActivity.get()!!)/2 - ScreenUtils.dpToPx(MAX_WIDTH_MESSAGE_VIEW_TABLET)/2 else 0,
333 | 0)
334 | val animation = AnimationUtils.getScaleAnimation(0, DURATION_SHOW_CASE_ANIMATION)
335 | backgroundDimLayout?.addView(AnimationUtils.setAnimationToView(bubbleMessageView, animation), showCaseParams)
336 | }
337 |
338 | private fun createViewId(): Int {
339 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
340 | View.generateViewId()
341 | } else {
342 | System.currentTimeMillis().toInt() / 1000
343 | }
344 | }
345 |
346 | private fun takeScreenshot(targetView: View, highlightMode: HighlightMode?): Bitmap? {
347 | if (highlightMode==null || highlightMode == HighlightMode.VIEW_LAYOUT)
348 | return takeScreenshotOfLayoutView(targetView)
349 | return takeScreenshotOfSurfaceView(targetView)
350 | }
351 |
352 | private fun takeScreenshotOfLayoutView(targetView: View): Bitmap? {
353 | if (targetView.width == 0 || targetView.height == 0) {
354 | return null
355 | }
356 |
357 | val rootView = getViewRoot(mActivity.get()!!)
358 | val currentScreenView = rootView.getChildAt(0)
359 | currentScreenView.buildDrawingCache()
360 | val bitmap: Bitmap
361 | bitmap = Bitmap.createBitmap(currentScreenView.drawingCache, getXposition(targetView), getYposition(targetView), targetView.width, targetView.height)
362 | currentScreenView.isDrawingCacheEnabled = false
363 | currentScreenView.destroyDrawingCache()
364 | return bitmap
365 | }
366 |
367 | private fun takeScreenshotOfSurfaceView(targetView: View): Bitmap? {
368 | if (targetView.width == 0 || targetView.height == 0) {
369 | return null
370 | }
371 |
372 | targetView.isDrawingCacheEnabled = true
373 | val bitmap: Bitmap = Bitmap.createBitmap(targetView.drawingCache)
374 | targetView.isDrawingCacheEnabled = false
375 | return bitmap
376 | }
377 |
378 | private fun isVisibleOnScreen(targetView: View?): Boolean {
379 | if(targetView!=null){
380 | if(getXposition(targetView) >= 0 && getYposition(targetView) >= 0){
381 | return getXposition(targetView) != 0 || getYposition(targetView) != 0
382 | }
383 | }
384 | return false
385 | }
386 |
387 | private fun getXposition(targetView: View): Int{
388 | return ScreenUtils.getAxisXpositionOfViewOnScreen(targetView) - getScreenHorizontalOffset()
389 | }
390 |
391 | private fun getYposition(targetView: View): Int{
392 | return ScreenUtils.getAxisYpositionOfViewOnScreen(targetView) - getScreenVerticalOffset()
393 | }
394 |
395 | private fun getScreenHeight(context: Context): Int{
396 | return ScreenUtils.getScreenHeight(context) - getScreenVerticalOffset()
397 | }
398 |
399 | private fun getScreenWidth(context: Context): Int{
400 | return ScreenUtils.getScreenWidth(context) - getScreenHorizontalOffset()
401 | }
402 |
403 | private fun getScreenVerticalOffset(): Int{
404 | return if(backgroundDimLayout !=null) ScreenUtils.getAxisYpositionOfViewOnScreen(backgroundDimLayout!!) else 0
405 | }
406 |
407 | private fun getScreenHorizontalOffset(): Int{
408 | return if(backgroundDimLayout !=null) ScreenUtils.getAxisXpositionOfViewOnScreen(backgroundDimLayout!!) else 0
409 | }
410 |
411 | private fun getMessageViewWidthOnTablet(availableSpace: Int): Int{
412 | return if(availableSpace > ScreenUtils.dpToPx(MAX_WIDTH_MESSAGE_VIEW_TABLET)) ScreenUtils.dpToPx(MAX_WIDTH_MESSAGE_VIEW_TABLET) else availableSpace
413 | }
414 |
415 | private fun isTablet(): Boolean = mActivity.get()!!.resources.getBoolean(R.bool.isTablet)
416 |
417 |
418 | }
--------------------------------------------------------------------------------