├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.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 │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── dizcoding │ │ │ └── sample │ │ │ └── MainActivity.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── dizcoding │ │ │ └── sample │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── dizcoding │ │ └── sample │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── showcase ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── dizcoding │ │ │ │ └── showcase │ │ │ │ ├── ShowCaseListener.java │ │ │ │ ├── ShowCaseContentPosition.java │ │ │ │ ├── ShowCasePreference.java │ │ │ │ ├── ShowCaseObject.java │ │ │ │ ├── ViewHelper.java │ │ │ │ ├── ShowCaseBuilder.java │ │ │ │ ├── ShowCaseDialog.java │ │ │ │ └── ShowCaseLayout.java │ │ └── res │ │ │ ├── values │ │ │ ├── public.xml │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ └── styles.xml │ │ │ ├── drawable │ │ │ ├── rectangle_rounded.xml │ │ │ ├── background_round_white_10.xml │ │ │ ├── selector_white.xml │ │ │ ├── selector_circle_blue.xml │ │ │ ├── selector_circle_green.xml │ │ │ ├── ic_button_prev.xml │ │ │ ├── ic_button_finish.xml │ │ │ ├── ic_button_next.xml │ │ │ └── ic_button_skip.xml │ │ │ ├── layout │ │ │ ├── circle_green_view.xml │ │ │ └── tutorial_view.xml │ │ │ └── drawable-v21 │ │ │ └── selector_white.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── dizcoding │ │ │ └── showcase │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── dizcoding │ │ └── showcase │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── compiler.xml ├── runConfigurations.xml ├── misc.xml ├── gradle.xml └── jarRepositories.xml ├── Showcase.gif ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /showcase/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /showcase/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /Showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/Showcase.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "sample" 2 | include ':app' 3 | include ':showcase' 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | sample 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/showcase/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /showcase/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCaseListener.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | public interface ShowCaseListener { 4 | void onPrevious(); 5 | void onNext(); 6 | 7 | void onComplete(); 8 | } 9 | -------------------------------------------------------------------------------- /showcase/src/main/res/values/public.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/rectangle_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 07 00:02:43 WIB 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /showcase/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | tutors 3 | 4 | < 5 | > 6 | OK 7 | Skip 8 | 9 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/background_round_white_10.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /showcase/src/main/res/layout/circle_green_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /showcase/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #B0000000 4 | #42b549 5 | #1B4DB3 6 | #FFFFFF 7 | #DDDDDD 8 | #434655 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/dizcoding/sample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.sample 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 | } -------------------------------------------------------------------------------- /showcase/src/test/java/com/dizcoding/showcase/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase 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 | } -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCaseContentPosition.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | public enum ShowCaseContentPosition { 4 | UNDEFINED (0), 5 | TOP (1), 6 | RIGHT (2), 7 | BOTTOM (3), 8 | LEFT (4); 9 | 10 | private final int position; 11 | 12 | ShowCaseContentPosition(int position) { 13 | this.position = position; 14 | } 15 | 16 | public int getPosition() { 17 | return position; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /showcase/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 18sp 5 | 14sp 6 | 1dp 7 | 8 | 10dp 9 | 16dp 10 | 2dp 11 | 12 | 85dp 13 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable-v21/selector_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/selector_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/selector_circle_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/selector_circle_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /showcase/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dizcoding/sample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.sample 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.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.getInstrumentation().targetContext 22 | assertEquals("com.dizcoding.sample", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /showcase/src/androidTest/java/com/dizcoding/showcase/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.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.getInstrumentation().targetContext 22 | assertEquals("com.dizcoding.showcase.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /showcase/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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCasePreference.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | public class ShowCasePreference { 7 | private static final String SHOWCASE_PREFERENCES = "show_case_pref"; 8 | 9 | public static boolean hasShown(Context context, String tag){ 10 | SharedPreferences sharedPreferences = context.getSharedPreferences(SHOWCASE_PREFERENCES, 11 | Context.MODE_PRIVATE); 12 | return sharedPreferences.getBoolean(tag, false); 13 | } 14 | 15 | public static void setShown(Context context, String tag, boolean hasShown){ 16 | SharedPreferences.Editor sharedPreferencesEditor = context.getSharedPreferences(SHOWCASE_PREFERENCES, 17 | Context.MODE_PRIVATE).edit(); 18 | sharedPreferencesEditor.putBoolean (tag, hasShown); 19 | sharedPreferencesEditor.apply(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/ic_button_prev.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/ic_button_finish.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /showcase/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 31 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 16 12 | targetSdkVersion 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 38 | implementation 'androidx.core:core-ktx:1.7.0' 39 | implementation 'androidx.appcompat:appcompat:1.4.0' 40 | implementation 'com.google.android.material:material:1.4.0' 41 | testImplementation 'junit:junit:4.+' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 44 | } -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/ic_button_next.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 31 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | applicationId "com.dizcoding.sample" 12 | minSdkVersion 16 13 | targetSdkVersion 31 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | debug { 22 | debuggable true 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | 40 | } 41 | 42 | dependencies { 43 | 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 45 | implementation 'androidx.core:core-ktx:1.7.0' 46 | implementation 'androidx.appcompat:appcompat:1.4.0' 47 | implementation 'com.google.android.material:material:1.4.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2' 49 | 50 | implementation project(path: ':showcase') 51 | 52 | testImplementation 'junit:junit:4.+' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 55 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 29 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/dizcoding/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.sample 2 | 3 | import android.os.Bundle 4 | import android.widget.TextView 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.dizcoding.showcase.ShowCaseBuilder 7 | import com.dizcoding.showcase.ShowCaseContentPosition 8 | import com.dizcoding.showcase.ShowCaseObject 9 | 10 | class MainActivity : AppCompatActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_main) 14 | 15 | val showCaseDialog = ShowCaseBuilder() 16 | .titleTextColorRes(R.color.blue) 17 | .titleTextSizeRes(R.dimen.text_title) 18 | .shadowColorRes(R.color.shadow) 19 | .textColorRes(R.color.dark_gray) 20 | .textSizeRes(R.dimen.text_normal) 21 | .spacingRes(R.dimen.spacing_normal) 22 | .backgroundContentColorRes(R.color.white) 23 | .circleIndicatorBackgroundDrawableRes(R.drawable.selector_circle_blue) 24 | .useSkipWord(true) 25 | .setLineColorRes(R.color.blue) 26 | .useCircleIndicator(true) 27 | .clickable(false) 28 | .build() 29 | val textView = findViewById(R.id.tv) 30 | val tvOne = findViewById(R.id.tvOne) 31 | val tvTwo = findViewById(R.id.tvTwo) 32 | val showCaseList = arrayListOf( 33 | ShowCaseObject( 34 | textView, 35 | "Akun Anda", 36 | "Lengkapi profil anda dan lihat nomor VA dan e-sertifikat kepesertaan disini.", 37 | ShowCaseContentPosition.TOP 38 | ), 39 | ShowCaseObject( 40 | tvOne, 41 | "Akun Anda", 42 | "Lengkapi profil anda dan lihat nomor VA dan e-sertifikat kepesertaan disini.", 43 | ShowCaseContentPosition.BOTTOM 44 | ),ShowCaseObject( 45 | tvTwo, 46 | "Akun Anda", 47 | "Lengkapi profil anda dan lihat nomor VA dan e-sertifikat kepesertaan disini.", 48 | ShowCaseContentPosition.TOP 49 | ) 50 | ) 51 | 52 | showCaseDialog.show(this, "", showCaseList) 53 | 54 | } 55 | } -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCaseObject.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | public class ShowCaseObject { 9 | 10 | private View view; 11 | protected String title; 12 | protected String text; 13 | protected ShowCaseContentPosition showCaseContentPosition; 14 | protected int tintBackgroundColor; 15 | private ViewGroup scrollView; 16 | 17 | public ShowCaseObject(@Nullable View view, @Nullable String title, String text ) { 18 | this(view, title, text, ShowCaseContentPosition.UNDEFINED); 19 | } 20 | public ShowCaseObject(@Nullable View view, @Nullable String title, 21 | String text, ShowCaseContentPosition showCaseContentPosition) { 22 | this(view, title, text, showCaseContentPosition, 0); 23 | } 24 | 25 | public ShowCaseObject(@Nullable View view, @Nullable String title, 26 | String text, ShowCaseContentPosition showCaseContentPosition, 27 | int tintBackgroundColor) { 28 | this(view, title, text, showCaseContentPosition, tintBackgroundColor, null); 29 | } 30 | public ShowCaseObject(@Nullable View view, @Nullable String title, 31 | String text, ShowCaseContentPosition showCaseContentPosition, 32 | int tintBackgroundColor, ViewGroup scrollView) { 33 | this.view = view; 34 | this.title = title; 35 | this.text = text; 36 | this.showCaseContentPosition = showCaseContentPosition; 37 | this.tintBackgroundColor = tintBackgroundColor; 38 | this.scrollView = scrollView; 39 | } 40 | 41 | public String getTitle() { 42 | return title; 43 | } 44 | 45 | public String getText() { 46 | return text; 47 | } 48 | 49 | public int getTintBackgroundColor() { 50 | return tintBackgroundColor; 51 | } 52 | 53 | public ShowCaseContentPosition getShowCaseContentPosition() { 54 | return showCaseContentPosition; 55 | } 56 | 57 | private int[] location; 58 | private int radius; 59 | public ShowCaseObject withCustomTarget(int[] location, int radius){ 60 | if (location.length != 2) { 61 | return this; 62 | } 63 | this.location = location; 64 | this.radius = radius; 65 | return this; 66 | } 67 | 68 | public ShowCaseObject withCustomTarget(int[] location){ 69 | if (location.length!= 4) { 70 | return this; 71 | } 72 | this.location = location; 73 | return this; 74 | } 75 | 76 | public int[] getLocation() { 77 | return location; 78 | } 79 | 80 | public int getRadius() { 81 | return radius; 82 | } 83 | 84 | public View getView() { 85 | return view; 86 | } 87 | public ViewGroup getScrollView() { 88 | return scrollView; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /showcase/src/main/res/layout/tutorial_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 24 | 25 | 31 | 32 | 37 | 38 | 45 | 46 | 49 | 50 | 57 | 58 | 65 | 66 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/ic_button_skip.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ViewHelper.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.PorterDuff; 8 | import android.graphics.PorterDuffXfermode; 9 | import android.graphics.Rect; 10 | import android.graphics.drawable.Drawable; 11 | import android.graphics.drawable.GradientDrawable; 12 | import android.graphics.drawable.ShapeDrawable; 13 | import android.view.View; 14 | import android.view.ViewParent; 15 | 16 | public class ViewHelper { 17 | static public void getRelativePositionRec(View myView, ViewParent root, int[] location) { 18 | if (myView.getParent() == root) { 19 | location[0] += myView.getLeft(); 20 | location[1] += myView.getTop(); 21 | } else { 22 | location[0] += myView.getLeft(); 23 | location[1] += myView.getTop(); 24 | getRelativePositionRec((View) myView.getParent(), root, location); 25 | } 26 | } 27 | 28 | public static void setBackgroundColor(View v, int color){ 29 | Drawable background = v.getBackground(); 30 | if (background instanceof ShapeDrawable) { 31 | ShapeDrawable shapeDrawable = (ShapeDrawable)background; 32 | shapeDrawable.getPaint().setColor(color); 33 | } else if (background instanceof GradientDrawable) { 34 | GradientDrawable gradientDrawable = (GradientDrawable)background; 35 | gradientDrawable.setColor(color); 36 | } else { 37 | v.setBackgroundColor(color); 38 | } 39 | } 40 | 41 | public static Bitmap getCroppedBitmap(Bitmap bitmap, int centerLocation[], int radius) { 42 | Bitmap output = Bitmap.createBitmap(2*radius, 43 | 2*radius, Bitmap.Config.ARGB_8888); 44 | Canvas canvas = new Canvas(output); 45 | 46 | final int color = 0xff424242; 47 | final Paint paint = new Paint(); 48 | 49 | Rect sourceRect = new Rect(centerLocation[0] - radius, 50 | centerLocation[1] - radius, 51 | centerLocation[0] + radius, 52 | centerLocation[1] + radius); 53 | Rect destRect = new Rect(0,0,2*radius, 2*radius); 54 | 55 | paint.setAntiAlias(true); 56 | canvas.drawARGB(0, 0, 0, 0); 57 | paint.setColor(color); 58 | canvas.drawCircle(radius, radius, 59 | radius, paint); 60 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 61 | canvas.drawBitmap(bitmap, sourceRect, destRect, paint); 62 | return output; 63 | } 64 | 65 | public static Bitmap getCroppedBitmap(Bitmap bitmap, int rectLocation[]) { 66 | int xStart = rectLocation[0]; 67 | int yStart = rectLocation[1]; 68 | int xEnd = rectLocation[2]; 69 | int yEnd = rectLocation[3]; 70 | int width = xEnd - xStart; 71 | int height = yEnd - yStart; 72 | 73 | Bitmap output = Bitmap.createBitmap(width, 74 | height, Bitmap.Config.ARGB_8888); 75 | Canvas canvas = new Canvas(output); 76 | 77 | final int color = 0xff424242; 78 | final Paint paint = new Paint(); 79 | 80 | Rect sourceRect = new Rect(xStart, 81 | yStart, 82 | xEnd, 83 | yEnd); 84 | Rect destRect = new Rect(0,0,width, height); 85 | 86 | paint.setAntiAlias(true); 87 | canvas.drawARGB(0, 0, 0, 0); 88 | paint.setColor(color); 89 | canvas.drawRect(destRect, paint); 90 | // canvas.drawCircle(radius, radius, 91 | // radius, paint); 92 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 93 | canvas.drawBitmap(bitmap, sourceRect, destRect, paint); 94 | return output; 95 | } 96 | 97 | public static int getStatusBarHeight(Context context) { 98 | int height = 0; 99 | int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 100 | if (resId > 0) { 101 | height = context.getResources().getDimensionPixelSize(resId); 102 | } 103 | return height; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## !!!NOTE!!! 2 | this source code is clone from https://github.com/tokopedia/ShowCase and i just edit a layout so its compaitible with my case. 3 | 4 | # ShowCase 5 | 6 | This library is used to showcase chain of views to guide the users about the feature on the screen. 7 | 8 | 9 | 10 | 13 | 14 |
11 | 12 |
15 | 16 | Dependencies 17 | ------- 18 | ``` 19 | dependencies { 20 | implementation 'com.github.gandhi-wibowo:showcase:v1.0.0' 21 | } 22 | ``` 23 | 24 | Usage 25 | ------- 26 | 27 | First, create the `ShowCaseDialog`. Use Builder Pattern. 28 | 29 | As an example: 30 | 31 | ```java 32 | showCaseDialog = new ShowCaseBuilder() 33 | .textColorRes(android.R.color.white) 34 | .shadowColorRes(R.color.shadow) 35 | .titleTextSizeRes(R.dimen.text_title) 36 | .textSizeRes(R.dimen.text_normal) 37 | .spacingRes(R.dimen.spacing_normal) 38 | .backgroundContentColorRes(R.color.blue) 39 | .circleIndicatorBackgroundDrawableRes(R.drawable.selector_circle_green) 40 | .setLineColorRes(R.color.blue) 41 | .prevDrawableRes(R.drawable.ic_btn_prev) 42 | .nextDrawableRes(R.drawable.ic_btn_next) 43 | .skipDrawableRes(R.drawable.ic_btn_skip) 44 | .finishDrawableRes(R.drawable.ic_btn_finish) 45 | .useCircleIndicator(true) 46 | .clickable(true) 47 | .build(); 48 | ``` 49 | 50 | ### this section is what i just edit. 51 | ```java 52 | .setLineColorRes(R.color.blue) 53 | .prevDrawableRes(R.drawable.ic_btn_prev) 54 | .nextDrawableRes(R.drawable.ic_btn_next) 55 | .skipDrawableRes(R.drawable.ic_btn_skip) 56 | .finishDrawableRes(R.drawable.ic_btn_finish) 57 | ``` 58 | After the dialog has been created, provide the `ArrayList` of `ShowCaseObject` 59 | 60 | As an example to showcase 2 views: 61 | 62 | ``` 63 | ArrayList showCaseList = new ArrayList<>(); 64 | showCaseList.add(new ShowCaseObject( 65 | viewToHighLight, 66 | "Title", 67 | "Description")); 68 | showCaseList.add(new ShowCaseObject( 69 | view2, 70 | null, 71 | "Description 2"); 72 | showCaseDialog.show(this, showCaseList); 73 | ``` 74 | 75 | Item Customization 76 | ------- 77 | ``` 78 | showCaseList.add(new ShowCaseObject(@Nullable View view, 79 | @Nullable String title, 80 | String text, 81 | ShowCaseContentPosition showCaseContentPosition, 82 | int tintBackgroundColor) ); 83 | ``` 84 | View 85 | is the view to anchor. Fill `null` if no view to anchor. 86 | 87 | Title 88 | is the title to show, will be bold in default. Fill `null` if you don't use title. 89 | 90 | text 91 | is the text to show. By default will parse HTML. 92 | 93 | position (optional) 94 | default is `UNDEFINED` and will position the content automatically. 95 | Other option is `LEFT`,`TOP`,`RIGHT`,`BOTTOM` to position manually. 96 | 97 | tintBackgroundColor (optional) 98 | to override the backgroundColor to the view. The default is transparent. 99 | 100 | Custom Target 101 | ------- 102 | ``` 103 | showCaseList.add( 104 | new ShowCaseObject( 105 | findViewById(android.R.id.content), 106 | "Show case using custom target", 107 | "This is highlighted using custom target") 108 | .withCustomTarget(new int[]{ xCenter, yCenter} 109 | , radius) ); 110 | ``` 111 | Use ```.withCustomTarget``` to highlight circle area for the specific coordinate (x,y) and radius. 112 | ``` 113 | showCaseList.add( 114 | new ShowCaseObject( 115 | findViewById(android.R.id.content), 116 | "Show case using custom target", 117 | "This is highlighted using custom target") 118 | .withCustomTarget(new int[]{ left, top, right, bottom} ) ); 119 | ``` 120 | Use ```.withCustomTarget``` to highlight rectangle area for the specific coordinate (x,y) and radius. 121 | 122 | Custom Layout Customization 123 | ------- 124 | ```java 125 | showCaseDialog = new ShowCaseBuilder() 126 | .customView(R.layout.customView) 127 | .build(); 128 | ``` 129 | 130 | Note: To use most of default functionality, the views in the custom layout should correspond with the same id with the id in ```R.layout.tutorial_view.xml``` 131 | 132 | License 133 | ------- 134 | 135 | Copyright 2019 Tokopedia 136 | 137 | Licensed under the Apache License, Version 2.0 (the "License"); 138 | you may not use this file except in compliance with the License. 139 | You may obtain a copy of the License at 140 | 141 | http://www.apache.org/licenses/LICENSE-2.0 142 | 143 | Unless required by applicable law or agreed to in writing, software 144 | distributed under the License is distributed on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 146 | See the License for the specific language governing permissions and 147 | limitations under the License. 148 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCaseBuilder.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | 4 | import android.os.Parcel; 5 | import android.os.Parcelable; 6 | 7 | public class ShowCaseBuilder implements Parcelable { 8 | 9 | private int layoutRes; 10 | private int titleTextColorRes; 11 | private int textColorRes; 12 | private int shadowColorRes; 13 | private int titleTextSizeRes; 14 | private int textSizeRes; 15 | private int spacingRes; 16 | private int backgroundContentColorRes; 17 | private int circleIndicatorBackgroundDrawableRes; 18 | private int prevDrawableRes; 19 | private int nextDrawableRes; 20 | private int finishDrawableRes; 21 | private boolean useCircleIndicator = true; 22 | private boolean clickable = false; 23 | private boolean useArrow = true; 24 | private int arrowWidth; 25 | private String packageName; 26 | 27 | private int skipDrawableRes; 28 | private int lineColorRes; 29 | private boolean useSkipWord; 30 | 31 | public ShowCaseBuilder setPackageName(String packageName) { 32 | this.packageName = packageName; 33 | return this; 34 | } 35 | 36 | public ShowCaseBuilder customView(int customViewRes) { 37 | this.layoutRes = customViewRes; 38 | return this; 39 | } 40 | 41 | public ShowCaseBuilder textColorRes(int textColorRes) { 42 | this.textColorRes = textColorRes; 43 | return this; 44 | } 45 | 46 | public ShowCaseBuilder titleTextColorRes(int titleTextColorRes) { 47 | this.titleTextColorRes = titleTextColorRes; 48 | return this; 49 | } 50 | 51 | public ShowCaseBuilder shadowColorRes(int shadowColorRes) { 52 | this.shadowColorRes = shadowColorRes; 53 | return this; 54 | } 55 | 56 | public ShowCaseBuilder useArrow(boolean useArrow) { 57 | this.useArrow = useArrow; 58 | return this; 59 | } 60 | 61 | public ShowCaseBuilder useSkipWord(boolean useSkipWord) { 62 | this.useSkipWord = useSkipWord; 63 | return this; 64 | } 65 | 66 | public ShowCaseBuilder textSizeRes(int textSizeRes) { 67 | this.textSizeRes = textSizeRes; 68 | return this; 69 | } 70 | 71 | public ShowCaseBuilder titleTextSizeRes(int titleTextSizeRes) { 72 | this.titleTextSizeRes = titleTextSizeRes; 73 | return this; 74 | } 75 | 76 | public ShowCaseBuilder spacingRes(int spacingRes) { 77 | this.spacingRes = spacingRes; 78 | return this; 79 | } 80 | 81 | public ShowCaseBuilder arrowWidth(int arrowWidth) { 82 | this.arrowWidth = arrowWidth; 83 | return this; 84 | } 85 | 86 | public ShowCaseBuilder backgroundContentColorRes(int backgroundContentColorRes) { 87 | this.backgroundContentColorRes = backgroundContentColorRes; 88 | return this; 89 | } 90 | 91 | public ShowCaseBuilder circleIndicatorBackgroundDrawableRes(int circleIndicatorBackgroundDrawableRes) { 92 | this.circleIndicatorBackgroundDrawableRes = circleIndicatorBackgroundDrawableRes; 93 | return this; 94 | } 95 | 96 | public ShowCaseBuilder setLineColorRes(int lineColorRes) { 97 | this.lineColorRes = lineColorRes; 98 | return this; 99 | } 100 | 101 | public ShowCaseBuilder finishDrawableRes(int finishDrawableRes) { 102 | this.finishDrawableRes = finishDrawableRes; 103 | return this; 104 | } 105 | 106 | public ShowCaseBuilder prevDrawableRes(int prevDrawableRes) { 107 | this.prevDrawableRes = prevDrawableRes; 108 | return this; 109 | } 110 | 111 | public ShowCaseBuilder nextDrawableRes(int nextDrawableRes) { 112 | this.nextDrawableRes = nextDrawableRes; 113 | return this; 114 | } 115 | 116 | public ShowCaseBuilder skipDrawableRes(int skipDrawableRes) { 117 | this.skipDrawableRes = skipDrawableRes; 118 | return this; 119 | } 120 | 121 | 122 | public ShowCaseBuilder clickable(boolean clickable) { 123 | this.clickable = clickable; 124 | return this; 125 | } 126 | 127 | public ShowCaseBuilder useCircleIndicator(boolean useCircleIndicator) { 128 | this.useCircleIndicator = useCircleIndicator; 129 | return this; 130 | } 131 | 132 | public String getPackageName() { 133 | return packageName; 134 | } 135 | public int getLineColorRes() { 136 | return lineColorRes; 137 | } 138 | 139 | public int getTextColorRes() { 140 | return textColorRes; 141 | } 142 | 143 | public int getTitleTextColorRes() { 144 | return titleTextColorRes; 145 | } 146 | 147 | public int getTitleTextSizeRes() { 148 | return titleTextSizeRes; 149 | } 150 | 151 | public int getFinishDrawableRes() { 152 | return finishDrawableRes; 153 | } 154 | 155 | public int getNextDrawableRes() { 156 | return nextDrawableRes; 157 | } 158 | 159 | public int getSkipDrawableRes() { 160 | return skipDrawableRes; 161 | } 162 | 163 | public int getPrevDrawableRes() { 164 | return prevDrawableRes; 165 | } 166 | 167 | public boolean useCircleIndicator() { 168 | return useCircleIndicator; 169 | } 170 | 171 | public int getShadowColorRes() { 172 | return shadowColorRes; 173 | } 174 | 175 | public int getTextSizeRes() { 176 | return textSizeRes; 177 | } 178 | 179 | public int getBackgroundContentColorRes() { 180 | return backgroundContentColorRes; 181 | } 182 | 183 | public int getCircleIndicatorBackgroundDrawableRes() { 184 | return circleIndicatorBackgroundDrawableRes; 185 | } 186 | 187 | public boolean isUseArrow() { 188 | return useArrow; 189 | } 190 | 191 | public boolean isUseSkipWord(){ 192 | return useSkipWord; 193 | } 194 | 195 | public int getLayoutRes() { 196 | return layoutRes; 197 | } 198 | 199 | public int getArrowWidth() { 200 | return arrowWidth; 201 | } 202 | 203 | public int getSpacingRes() { 204 | return spacingRes; 205 | } 206 | 207 | public boolean isClickable() { 208 | return clickable; 209 | } 210 | 211 | public ShowCaseDialog build() { 212 | return ShowCaseDialog.newInstance(this); 213 | } 214 | 215 | @Override 216 | public int describeContents() { 217 | return 0; 218 | } 219 | 220 | @Override 221 | public void writeToParcel(Parcel dest, int flags) { 222 | dest.writeInt(this.layoutRes); 223 | dest.writeInt(this.titleTextColorRes); 224 | dest.writeInt(this.textColorRes); 225 | dest.writeInt(this.shadowColorRes); 226 | dest.writeInt(this.titleTextSizeRes); 227 | dest.writeInt(this.textSizeRes); 228 | dest.writeInt(this.spacingRes); 229 | dest.writeInt(this.backgroundContentColorRes); 230 | dest.writeInt(this.circleIndicatorBackgroundDrawableRes); 231 | dest.writeInt(this.prevDrawableRes); 232 | dest.writeInt(this.nextDrawableRes); 233 | dest.writeInt(this.finishDrawableRes); 234 | dest.writeByte(this.useCircleIndicator ? (byte) 1 : (byte) 0); 235 | dest.writeByte(this.clickable ? (byte) 1 : (byte) 0); 236 | dest.writeByte(this.useArrow ? (byte) 1 : (byte) 0); 237 | dest.writeInt(this.arrowWidth); 238 | dest.writeInt(this.skipDrawableRes); 239 | dest.writeInt(this.lineColorRes); 240 | dest.writeByte(this.useSkipWord ? (byte) 1 : (byte) 0); 241 | } 242 | 243 | public ShowCaseBuilder() { 244 | } 245 | 246 | protected ShowCaseBuilder(Parcel in) { 247 | this.layoutRes = in.readInt(); 248 | this.titleTextColorRes = in.readInt(); 249 | this.textColorRes = in.readInt(); 250 | this.shadowColorRes = in.readInt(); 251 | this.titleTextSizeRes = in.readInt(); 252 | this.textSizeRes = in.readInt(); 253 | this.spacingRes = in.readInt(); 254 | this.backgroundContentColorRes = in.readInt(); 255 | this.circleIndicatorBackgroundDrawableRes = in.readInt(); 256 | this.prevDrawableRes = in.readInt(); 257 | this.nextDrawableRes = in.readInt(); 258 | this.finishDrawableRes = in.readInt(); 259 | this.useCircleIndicator = in.readByte() != 0; 260 | this.clickable = in.readByte() != 0; 261 | this.useArrow = in.readByte() != 0; 262 | this.arrowWidth = in.readInt(); 263 | this.skipDrawableRes = in.readInt(); 264 | this.lineColorRes = in.readInt(); 265 | this.useSkipWord = in.readByte() != 0; 266 | } 267 | 268 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 269 | @Override 270 | public ShowCaseBuilder createFromParcel(Parcel source) { 271 | return new ShowCaseBuilder(source); 272 | } 273 | 274 | @Override 275 | public ShowCaseBuilder[] newArray(int size) { 276 | return new ShowCaseBuilder[size]; 277 | } 278 | }; 279 | } 280 | 281 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCaseDialog.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.text.TextUtils; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.Window; 12 | import android.widget.ScrollView; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | import androidx.appcompat.app.AppCompatActivity; 17 | import androidx.core.widget.NestedScrollView; 18 | import androidx.fragment.app.DialogFragment; 19 | import androidx.fragment.app.FragmentActivity; 20 | import androidx.fragment.app.FragmentManager; 21 | import androidx.fragment.app.FragmentTransaction; 22 | 23 | import java.util.ArrayList; 24 | 25 | public class ShowCaseDialog extends DialogFragment { 26 | private static final String ARG_BUILDER = "BUILDER"; 27 | public static final int DELAY_SCROLLING = 350; 28 | public static final String TAG = ShowCaseDialog.class.getSimpleName(); 29 | public static final int MAX_RETRY_LAYOUT = 3; 30 | 31 | private ArrayList tutorsList; 32 | private int currentTutorIndex = -1; 33 | private ShowCaseBuilder builder; 34 | private String tag; 35 | 36 | boolean hasViewGroupHandled = false; 37 | 38 | private OnShowCaseStepListener listener; 39 | 40 | private int retryCounter = 0; 41 | 42 | public interface OnShowCaseStepListener { 43 | /** 44 | * @param previousStep 45 | * @param nextStep 46 | * @param showCaseObject 47 | * @return true if already fully handled show case step inthis function 48 | */ 49 | boolean onShowCaseGoTo(int previousStep, int nextStep, ShowCaseObject showCaseObject); 50 | } 51 | 52 | public void setShowCaseStepListener(OnShowCaseStepListener listener) { 53 | this.listener = listener; 54 | } 55 | 56 | static ShowCaseDialog newInstance(ShowCaseBuilder builder) { 57 | final Bundle args = new Bundle(); 58 | final ShowCaseDialog fragment = new ShowCaseDialog(); 59 | args.putParcelable(ARG_BUILDER, builder); 60 | fragment.setArguments(args); 61 | return fragment; 62 | } 63 | 64 | @Override 65 | public void onCreate(@Nullable Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | getArgs(getArguments()); 68 | setRetainInstance(true); 69 | } 70 | 71 | private void getArgs(Bundle args) { 72 | builder = (ShowCaseBuilder) args.get(ARG_BUILDER); 73 | } 74 | 75 | @Override 76 | @NonNull 77 | public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { 78 | final Dialog dialog = new Dialog(getActivity(), R.style.ShowCase) { 79 | @Override 80 | public void onBackPressed() { 81 | if (builder.isClickable()) { 82 | previous(); 83 | } 84 | } 85 | }; 86 | dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 87 | return dialog; 88 | } 89 | 90 | @Override 91 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 92 | @Nullable Bundle savedInstanceState) { 93 | final View view = new ShowCaseLayout(getActivity(), builder); 94 | initViews(((ShowCaseLayout) view)); 95 | return view; 96 | } 97 | 98 | private void initViews(ShowCaseLayout view) { 99 | view.setShowCaseListener(new ShowCaseListener() { 100 | @Override 101 | public void onPrevious() { 102 | previous(); 103 | } 104 | 105 | @Override 106 | public void onNext() { 107 | next(); 108 | } 109 | 110 | @Override 111 | public void onComplete() { 112 | if (!TextUtils.isEmpty(tag)) { 113 | ShowCasePreference.setShown(getActivity(), tag, true); 114 | } 115 | ShowCaseDialog.this.close(); 116 | } 117 | }); 118 | 119 | setCancelable(builder.isClickable()); 120 | } 121 | 122 | public void next() { 123 | if ((currentTutorIndex + 1) >= tutorsList.size()) { 124 | this.close(); 125 | } else { 126 | ShowCaseDialog.this.show(this.getActivity(), tag, tutorsList, currentTutorIndex + 1); 127 | } 128 | } 129 | 130 | public void previous() { 131 | if ((currentTutorIndex - 1) < 0) { 132 | currentTutorIndex = 0; 133 | } else { 134 | ShowCaseDialog.this.show(getActivity(), tag, tutorsList, currentTutorIndex - 1); 135 | } 136 | } 137 | 138 | @Override 139 | public void onStart() { 140 | super.onStart(); 141 | final Window window = getDialog().getWindow(); 142 | if (window != null) { 143 | window.setBackgroundDrawableResource(android.R.color.transparent); 144 | window.setDimAmount(0f); 145 | window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 146 | } 147 | } 148 | 149 | public boolean hasShown(AppCompatActivity activity, String tag) { 150 | return ShowCasePreference.hasShown(activity, tag); 151 | } 152 | 153 | public void show(AppCompatActivity activity, @Nullable String tag, final ArrayList tutorList) { 154 | show(activity, tag, tutorList, 0); 155 | } 156 | 157 | public void show(final FragmentActivity activity, @Nullable String tag, final ArrayList tutorList, int indexToShow) { 158 | if (activity == null || activity.isFinishing()) { 159 | return; 160 | } 161 | try { 162 | this.tutorsList = tutorList; 163 | this.tag = tag; 164 | if (indexToShow < 0 || indexToShow >= tutorList.size()) { 165 | indexToShow = 0; 166 | } 167 | int previousIndex = currentTutorIndex; 168 | currentTutorIndex = indexToShow; 169 | 170 | hasViewGroupHandled = false; 171 | if (listener != null) { 172 | hasViewGroupHandled = listener.onShowCaseGoTo(previousIndex, currentTutorIndex, tutorList.get(currentTutorIndex)); 173 | } 174 | 175 | // has been handled by listener 176 | if (hasViewGroupHandled) return; 177 | 178 | final ShowCaseObject showCaseObject = tutorList.get(currentTutorIndex); 179 | final ViewGroup viewGroup = showCaseObject.getScrollView(); 180 | if (viewGroup != null) { 181 | final View viewToFocus = showCaseObject.getView(); 182 | if (viewToFocus != null) { 183 | hideLayout(); 184 | viewGroup.post(new Runnable() { 185 | @Override 186 | public void run() { 187 | if (viewGroup instanceof ScrollView) { 188 | ScrollView scrollView = (ScrollView) viewGroup; 189 | int relativeLocation[] = new int[2]; 190 | ViewHelper.getRelativePositionRec(viewToFocus, viewGroup, relativeLocation); 191 | scrollView.smoothScrollTo(0, relativeLocation[1]); 192 | scrollView.postDelayed(new Runnable() { 193 | @Override 194 | public void run() { 195 | showLayout(activity, showCaseObject); 196 | } 197 | }, DELAY_SCROLLING); 198 | } else if (viewGroup instanceof NestedScrollView) { 199 | NestedScrollView scrollView = (NestedScrollView) viewGroup; 200 | int relativeLocation[] = new int[2]; 201 | ViewHelper.getRelativePositionRec(viewToFocus, viewGroup, relativeLocation); 202 | scrollView.smoothScrollTo(0, relativeLocation[1]); 203 | scrollView.postDelayed(new Runnable() { 204 | @Override 205 | public void run() { 206 | showLayout(activity, showCaseObject); 207 | } 208 | }, DELAY_SCROLLING); 209 | } 210 | } 211 | }); 212 | hasViewGroupHandled = true; 213 | } else { 214 | hasViewGroupHandled = false; 215 | } 216 | } 217 | 218 | if (!hasViewGroupHandled) { 219 | showLayout(activity, tutorsList.get(currentTutorIndex)); 220 | } 221 | } catch (Exception e) { 222 | // to Handle the unknown exception. 223 | // Since this only for first guide, if any error appears, just don't show the guide 224 | try { 225 | ShowCaseDialog.this.dismiss(); 226 | } catch (Exception e2) { 227 | // no op 228 | } 229 | } 230 | } 231 | 232 | public void showLayout(FragmentActivity activity, ShowCaseObject showCaseObject) { 233 | if (activity == null || activity.isFinishing()) { 234 | return; 235 | } 236 | 237 | 238 | FragmentManager fm = activity.getSupportFragmentManager(); 239 | if (!isVisible()) { 240 | try { 241 | if (!isAdded()) { 242 | show(fm, TAG); 243 | } else if (isHidden()) { 244 | FragmentTransaction ft = fm.beginTransaction(); 245 | ft.show(ShowCaseDialog.this); 246 | ft.commit(); 247 | } 248 | } catch (IllegalStateException e) { 249 | // called in illegal state. just return. 250 | return; 251 | } 252 | } 253 | 254 | final View view = showCaseObject.getView(); 255 | final String title = showCaseObject.getTitle(); 256 | final String text = showCaseObject.getText(); 257 | final ShowCaseContentPosition showCaseContentPosition = showCaseObject.getShowCaseContentPosition(); 258 | final int tintBackgroundColor = showCaseObject.getTintBackgroundColor(); 259 | final int[] location = showCaseObject.getLocation(); 260 | final int radius = showCaseObject.getRadius(); 261 | 262 | if (view == null) { 263 | layoutShowTutorial(null, title, text, showCaseContentPosition, 264 | tintBackgroundColor, location, radius); 265 | } else { 266 | view.post(() -> layoutShowTutorial(view, title, text, showCaseContentPosition, 267 | tintBackgroundColor, location, radius)); 268 | } 269 | } 270 | 271 | public void hideLayout() { 272 | final ShowCaseLayout layout = (ShowCaseLayout) ShowCaseDialog.this.getView(); 273 | if (layout == null) { 274 | return; 275 | } 276 | layout.hideTutorial(); 277 | } 278 | 279 | private void layoutShowTutorial(final View view, final String title, final String text, 280 | final ShowCaseContentPosition showCaseContentPosition, 281 | final int tintBackgroundColor, final int[] customTarget, final int radius) { 282 | 283 | try { 284 | final ShowCaseLayout layout = (ShowCaseLayout) ShowCaseDialog.this.getView(); 285 | if (layout == null) { 286 | if (retryCounter >= MAX_RETRY_LAYOUT) { 287 | retryCounter = 0; 288 | return; 289 | } 290 | // wait until the layout is ready, and call itself 291 | new Handler().postDelayed(new Runnable() { 292 | @Override 293 | public void run() { 294 | retryCounter++; 295 | layoutShowTutorial(view, title, text, 296 | showCaseContentPosition, tintBackgroundColor, customTarget, radius); 297 | } 298 | }, 1000); 299 | return; 300 | } 301 | retryCounter = 0; 302 | layout.showTutorial(view, title, text, currentTutorIndex, tutorsList.size(), 303 | showCaseContentPosition, tintBackgroundColor, customTarget, radius); 304 | } catch (Throwable t) { 305 | // do nothing 306 | } 307 | 308 | } 309 | 310 | public void close() { 311 | try { 312 | dismiss(); 313 | final ShowCaseLayout layout = (ShowCaseLayout) ShowCaseDialog.this.getView(); 314 | if (layout == null) { 315 | return; 316 | } 317 | layout.closeTutorial(); 318 | } catch (Exception e) { 319 | // no op 320 | } 321 | } 322 | 323 | } 324 | 325 | -------------------------------------------------------------------------------- /showcase/src/main/java/com/dizcoding/showcase/ShowCaseLayout.java: -------------------------------------------------------------------------------- 1 | package com.dizcoding.showcase; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.graphics.drawable.Drawable; 12 | import android.os.Build; 13 | import android.text.Html; 14 | import android.text.Spanned; 15 | import android.text.TextUtils; 16 | import android.util.AttributeSet; 17 | import android.util.TypedValue; 18 | import android.view.Gravity; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.view.ViewTreeObserver; 23 | import android.widget.FrameLayout; 24 | import android.widget.ImageView; 25 | import android.widget.TextView; 26 | 27 | import androidx.annotation.Nullable; 28 | import androidx.annotation.RequiresApi; 29 | import androidx.core.content.ContextCompat; 30 | 31 | public class ShowCaseLayout extends FrameLayout { 32 | 33 | // customized attribute 34 | private int layoutRes; 35 | private int textColor; 36 | private int titleTextColor; 37 | private int shadowColor; 38 | private float textSize; 39 | private float textTitleSize; 40 | private int spacing; 41 | private int arrowMargin; 42 | private int arrowWidth; 43 | private int lineColorRes; 44 | private boolean useCircleIndicator; 45 | 46 | private boolean isCancelable; 47 | private boolean hasSkipWord; 48 | 49 | private Drawable prevDrawable; 50 | private Drawable nextDrawable; 51 | private Drawable finishDrawable; 52 | private Drawable skipDrawable; 53 | 54 | private int backgroundContentColor; 55 | private int circleBackgroundDrawableRes; 56 | 57 | // View 58 | private ViewGroup viewGroup; 59 | private Bitmap bitmap; 60 | private View lastTutorialView; 61 | private Paint viewPaint; 62 | 63 | // listener 64 | private ShowCaseListener showCaseListener; 65 | 66 | ShowCaseContentPosition showCaseContentPosition; 67 | 68 | private int highlightLocX; 69 | private int highlightLocY; 70 | 71 | // determined if this is last chain 72 | private boolean isStart; 73 | private boolean isLast; 74 | 75 | // path for arrow 76 | private Path path; 77 | private Paint arrowPaint; 78 | private TextView textViewTitle; 79 | private TextView textViewDesc; 80 | private ImageView prevButton; 81 | private ImageView nextButton; 82 | private ImageView skipButton; 83 | private ViewGroup viewGroupIndicator; 84 | 85 | public ShowCaseLayout(Context context, @Nullable ShowCaseBuilder builder) { 86 | super(context); 87 | init(context, builder); 88 | } 89 | 90 | public ShowCaseLayout(Context context) { 91 | super(context); 92 | init(context, null); 93 | } 94 | 95 | public ShowCaseLayout(Context context, AttributeSet attrs) { 96 | super(context, attrs); 97 | init(context, null); 98 | } 99 | 100 | public ShowCaseLayout(Context context, AttributeSet attrs, int defStyleAttr) { 101 | super(context, attrs, defStyleAttr); 102 | init(context, null); 103 | } 104 | 105 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 106 | public ShowCaseLayout(Context context, 107 | AttributeSet attrs, 108 | int defStyleAttr, 109 | int defStyleRes) { 110 | super(context, attrs, defStyleAttr, defStyleRes); 111 | init(context, null); 112 | } 113 | 114 | private void init(Context context, @Nullable ShowCaseBuilder builder) { 115 | setVisibility(View.GONE); 116 | 117 | if (isInEditMode()) { 118 | return; 119 | } 120 | 121 | applyAttrs(context, builder); 122 | 123 | //setBackground, color 124 | initFrame(); 125 | 126 | // setContentView 127 | initContent(context, builder); 128 | 129 | setClickable(this.isCancelable); 130 | setFocusable(this.isCancelable); 131 | 132 | if (this.isCancelable) { 133 | this.setOnClickListener(new OnClickListener() { 134 | @Override 135 | public void onClick(View v) { 136 | onNextClicked(); 137 | } 138 | }); 139 | } 140 | } 141 | 142 | private void onNextClicked() { 143 | if (showCaseListener != null) { 144 | if (this.isLast) { 145 | ShowCaseLayout.this.showCaseListener.onComplete(); 146 | } else { 147 | ShowCaseLayout.this.showCaseListener.onNext(); 148 | } 149 | } 150 | } 151 | 152 | private void initFrame() { 153 | setWillNotDraw(false); 154 | 155 | viewPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 156 | viewPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 157 | 158 | arrowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 159 | arrowPaint.setColor(backgroundContentColor); 160 | arrowPaint.setStyle(Paint.Style.FILL); 161 | 162 | setBackgroundColor(shadowColor); 163 | } 164 | 165 | public void setShowCaseListener(ShowCaseListener showCaseListener) { 166 | this.showCaseListener = showCaseListener; 167 | } 168 | 169 | public void showTutorial(View view, 170 | String title, 171 | String text, 172 | int currentTutorIndex, 173 | int tutorsListSize, 174 | ShowCaseContentPosition showCaseContentPosition, 175 | int tintBackgroundColor, 176 | final int[] customTarget, final int radius) throws Throwable { 177 | 178 | this.isStart = currentTutorIndex == 0; 179 | 180 | this.isLast = currentTutorIndex == tutorsListSize - 1; 181 | this.showCaseContentPosition = showCaseContentPosition; 182 | 183 | if (this.bitmap != null) { 184 | this.bitmap.recycle(); 185 | } 186 | if (this.lastTutorialView != null) { 187 | this.lastTutorialView.setDrawingCacheEnabled(false); 188 | } 189 | 190 | if (TextUtils.isEmpty(title)) { 191 | textViewTitle.setVisibility(View.GONE); 192 | } else { 193 | textViewTitle.setText(fromHtml(title)); 194 | textViewTitle.setVisibility(View.VISIBLE); 195 | } 196 | 197 | textViewDesc.setText(fromHtml(text)); 198 | 199 | if (skipButton != null){ 200 | if (hasSkipWord){ 201 | skipButton.setImageDrawable(skipDrawable); 202 | skipButton.setVisibility(VISIBLE); 203 | }else { 204 | skipButton.setImageDrawable(skipDrawable); 205 | skipButton.setVisibility(GONE); 206 | } 207 | } 208 | 209 | if (prevButton != null) { 210 | if (isStart) { 211 | prevButton.setVisibility(View.GONE); 212 | } else { 213 | prevButton.setImageDrawable(prevDrawable); 214 | prevButton.setVisibility(View.VISIBLE); 215 | } 216 | } 217 | 218 | if (nextButton != null) { 219 | if (isLast) { 220 | nextButton.setImageDrawable(finishDrawable); 221 | } else if (currentTutorIndex < tutorsListSize - 1) { // has next 222 | nextButton.setImageDrawable(nextDrawable); 223 | } 224 | } 225 | 226 | makeCircleIndicator(!isStart || !isLast, currentTutorIndex, tutorsListSize); 227 | 228 | if (view == null) { 229 | this.lastTutorialView = null; 230 | this.bitmap = null; 231 | this.highlightLocX = 0; 232 | this.highlightLocY = 0; 233 | moveViewToCenter(); 234 | } else { 235 | this.lastTutorialView = view; 236 | if (view.willNotCacheDrawing()) { 237 | view.setWillNotCacheDrawing(false); 238 | } 239 | view.setDrawingCacheEnabled(true); 240 | view.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW); 241 | if (tintBackgroundColor == 0) { 242 | this.bitmap = view.getDrawingCache(); 243 | } else { 244 | Bitmap bitmapTemp = view.getDrawingCache(); 245 | 246 | Bitmap bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), 247 | view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); 248 | Canvas bigCanvas = new Canvas(bigBitmap); 249 | bigCanvas.drawColor(tintBackgroundColor); 250 | Paint paint = new Paint(); 251 | bigCanvas.drawBitmap(bitmapTemp, 0f, 0f, paint); 252 | 253 | this.bitmap = bigBitmap; 254 | } 255 | 256 | //set custom target to view 257 | if (customTarget != null) { 258 | if (customTarget.length == 2) { 259 | this.bitmap = ViewHelper.getCroppedBitmap(bitmap, customTarget, radius); 260 | } else if (customTarget.length == 4) { 261 | this.bitmap = ViewHelper.getCroppedBitmap(bitmap, customTarget); 262 | } 263 | 264 | this.highlightLocX = customTarget[0] - radius; 265 | this.highlightLocY = customTarget[1] - radius; 266 | } else { // use view location as target 267 | final int[] location = new int[2]; 268 | view.getLocationInWindow(location); 269 | 270 | this.highlightLocX = location[0]; 271 | this.highlightLocY = location[1] - ViewHelper.getStatusBarHeight(getContext()); 272 | } 273 | 274 | this.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 275 | @Override 276 | public void onGlobalLayout() { 277 | if (ShowCaseLayout.this.bitmap != null) { 278 | moveViewBasedHighlight(ShowCaseLayout.this.highlightLocX, 279 | ShowCaseLayout.this.highlightLocY, 280 | ShowCaseLayout.this.highlightLocX + ShowCaseLayout.this.bitmap.getWidth(), 281 | ShowCaseLayout.this.highlightLocY + ShowCaseLayout.this.bitmap.getHeight()); 282 | 283 | if (Build.VERSION.SDK_INT < 16) { 284 | ShowCaseLayout.this.getViewTreeObserver().removeGlobalOnLayoutListener(this); 285 | } else { 286 | ShowCaseLayout.this.getViewTreeObserver().removeOnGlobalLayoutListener(this); 287 | } 288 | invalidate(); 289 | } 290 | } 291 | }); 292 | } 293 | 294 | this.setVisibility(View.VISIBLE); 295 | } 296 | 297 | public void hideTutorial() { 298 | this.setVisibility(View.INVISIBLE); 299 | } 300 | 301 | private void makeCircleIndicator(boolean hasMoreOneCircle, 302 | int currentTutorIndex, 303 | int tutorsListSize) { 304 | if (useCircleIndicator && this.viewGroupIndicator != null) { 305 | if (hasMoreOneCircle) { // has more than 1 circle 306 | // already has circle indicator 307 | if (this.viewGroupIndicator.getChildCount() == tutorsListSize) { 308 | for (int i = 0; i < tutorsListSize; i++) { 309 | View viewCircle = this.viewGroupIndicator.getChildAt(i); 310 | if (i == currentTutorIndex) { 311 | viewCircle.setSelected(true); 312 | } else { 313 | viewCircle.setSelected(false); 314 | } 315 | } 316 | } else { //reinitialize, the size is different 317 | this.viewGroupIndicator.removeAllViews(); 318 | LayoutInflater inflater = LayoutInflater.from(getContext()); 319 | for (int i = 0; i < tutorsListSize; i++) { 320 | View viewCircle = inflater.inflate(R.layout.circle_green_view, 321 | viewGroupIndicator, 322 | false); 323 | viewCircle.setBackgroundResource(this.circleBackgroundDrawableRes); 324 | if (i == currentTutorIndex) { 325 | viewCircle.setSelected(true); 326 | } 327 | this.viewGroupIndicator.addView(viewCircle); 328 | } 329 | } 330 | } else { 331 | this.viewGroupIndicator.removeAllViews(); 332 | } 333 | } 334 | } 335 | 336 | public void closeTutorial() { 337 | setVisibility(View.GONE); 338 | if (this.bitmap != null) { 339 | this.bitmap.recycle(); 340 | this.bitmap = null; 341 | } 342 | if (lastTutorialView != null) { 343 | this.lastTutorialView.setDrawingCacheEnabled(false); 344 | this.lastTutorialView = null; 345 | } 346 | } 347 | 348 | @Override 349 | public void onDetachedFromWindow() { 350 | super.onDetachedFromWindow(); 351 | recycleResources(); 352 | } 353 | 354 | @Override 355 | public void onDraw(Canvas canvas) { 356 | if (bitmap == null || bitmap.isRecycled()) { 357 | return; 358 | } 359 | super.onDraw(canvas); 360 | canvas.drawBitmap(this.bitmap, this.highlightLocX, this.highlightLocY, viewPaint); 361 | 362 | // drawArrow 363 | if (path != null && this.viewGroup.getVisibility() == View.VISIBLE) { 364 | canvas.drawPath(path, arrowPaint); 365 | } 366 | } 367 | 368 | private void applyAttrs(Context context, @Nullable ShowCaseBuilder builder) { 369 | // set Default value before check builder (might be null) 370 | this.layoutRes = R.layout.tutorial_view; 371 | 372 | this.textColor = Color.WHITE; 373 | this.titleTextColor = Color.WHITE; 374 | this.textTitleSize = getResources().getDimension(R.dimen.text_title); 375 | this.textSize = getResources().getDimension(R.dimen.text_normal); 376 | 377 | this.shadowColor = ContextCompat.getColor(context, R.color.shadow); 378 | this.spacing = (int) getResources().getDimension(R.dimen.spacing_normal); 379 | 380 | this.arrowMargin = this.spacing / 3; 381 | this.arrowWidth = (int) (1.5 * this.spacing); 382 | 383 | this.backgroundContentColor = Color.BLACK; 384 | this.circleBackgroundDrawableRes = R.drawable.selector_circle_green; 385 | 386 | this.prevDrawable = ContextCompat.getDrawable(context, R.drawable.ic_button_prev); 387 | this.nextDrawable = ContextCompat.getDrawable(context, R.drawable.ic_button_next); 388 | this.finishDrawable = ContextCompat.getDrawable(context, R.drawable.ic_button_finish); 389 | this.skipDrawable = ContextCompat.getDrawable(context, R.drawable.ic_button_skip); 390 | 391 | this.lineColorRes = ContextCompat.getColor(context,R.color.blue); 392 | 393 | if (builder == null) { 394 | return; 395 | } 396 | 397 | this.layoutRes = builder.getLayoutRes() != 0 ? 398 | builder.getLayoutRes() 399 | : this.layoutRes; 400 | 401 | this.textColor = builder.getTextColorRes() != 0 ? 402 | ContextCompat.getColor(context, builder.getTextColorRes()) 403 | : this.textColor; 404 | 405 | this.lineColorRes = builder.getLineColorRes() != 0 ? 406 | ContextCompat.getColor(context, builder.getLineColorRes()) 407 | : this.lineColorRes; 408 | 409 | this.titleTextColor = builder.getTitleTextColorRes() != 0 ? 410 | ContextCompat.getColor(context, builder.getTitleTextColorRes()) 411 | : this.titleTextColor; 412 | 413 | this.textTitleSize = builder.getTitleTextSizeRes() != 0 ? 414 | getResources().getDimension(builder.getTitleTextSizeRes()) 415 | : this.textTitleSize; 416 | 417 | this.textSize = builder.getTextSizeRes() != 0 ? 418 | getResources().getDimension(builder.getTextSizeRes()) 419 | : this.textSize; 420 | 421 | this.backgroundContentColor = builder.getBackgroundContentColorRes() != 0 ? 422 | ContextCompat.getColor(context, builder.getBackgroundContentColorRes()) 423 | : this.backgroundContentColor; 424 | 425 | this.shadowColor = builder.getShadowColorRes() != 0 ? 426 | ContextCompat.getColor(context, builder.getShadowColorRes()) : 427 | this.shadowColor; 428 | 429 | this.spacing = builder.getSpacingRes() != 0 ? 430 | (int) getResources().getDimension(builder.getSpacingRes()) 431 | : this.spacing; 432 | 433 | this.circleBackgroundDrawableRes = builder.getCircleIndicatorBackgroundDrawableRes() != 0 ? 434 | builder.getCircleIndicatorBackgroundDrawableRes() 435 | : this.circleBackgroundDrawableRes; 436 | 437 | this.prevDrawable = builder.getPrevDrawableRes() != 0 ? 438 | getResources().getDrawable(builder.getPrevDrawableRes()) 439 | : this.prevDrawable; 440 | 441 | this.nextDrawable = builder.getNextDrawableRes() != 0 ? 442 | getResources().getDrawable(builder.getNextDrawableRes()) 443 | : this.nextDrawable; 444 | 445 | this.finishDrawable = builder.getFinishDrawableRes() != 0 ? 446 | getResources().getDrawable(builder.getFinishDrawableRes()) 447 | : this.finishDrawable; 448 | 449 | this.skipDrawable = builder.getSkipDrawableRes() != 0 ? 450 | getResources().getDrawable(builder.getSkipDrawableRes()) 451 | : this.skipDrawable; 452 | 453 | this.useCircleIndicator = builder.useCircleIndicator(); 454 | this.hasSkipWord = builder.isUseSkipWord(); 455 | 456 | this.isCancelable = builder.isClickable(); 457 | 458 | if (builder.isUseArrow()) { 459 | this.arrowMargin = this.spacing / 3; 460 | this.arrowWidth = builder.getArrowWidth() != 0 ? 461 | (int) getResources().getDimension(builder.getArrowWidth()) 462 | : this.arrowWidth; 463 | } else { 464 | this.arrowMargin = 0; 465 | this.arrowWidth = 0; 466 | } 467 | 468 | } 469 | 470 | private void initContent(Context context, ShowCaseBuilder builder) { 471 | this.viewGroup = (ViewGroup) 472 | LayoutInflater.from(context).inflate(this.layoutRes, this, false); 473 | 474 | int view_group_tutor_content = getResources().getIdentifier("view_group_tutor_content", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 475 | int text_title = getResources().getIdentifier("text_title", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 476 | int text_description = getResources().getIdentifier("text_description", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 477 | int view_line = getResources().getIdentifier("view_line", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 478 | int iv_previous = getResources().getIdentifier("iv_prev", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 479 | int iv_skip = getResources().getIdentifier("iv_skip", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 480 | int iv_next = getResources().getIdentifier("iv_next", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 481 | int view_group_indicator = getResources().getIdentifier("view_group_indicator", "id", (builder.getPackageName() != null) ? builder.getPackageName() : context.getPackageName()); 482 | 483 | View viewGroupTutorContent = viewGroup.findViewById(view_group_tutor_content); 484 | ViewHelper.setBackgroundColor(viewGroupTutorContent, this.backgroundContentColor); 485 | 486 | textViewTitle = (TextView) viewGroupTutorContent.findViewById(text_title); 487 | 488 | textViewTitle = (TextView) viewGroupTutorContent.findViewById(text_title); 489 | textViewTitle.setTextColor(this.titleTextColor); 490 | textViewTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, this.textTitleSize); 491 | 492 | textViewDesc = (TextView) viewGroupTutorContent.findViewById(text_description); 493 | textViewDesc.setTextColor(this.textColor); 494 | textViewDesc.setTextSize(TypedValue.COMPLEX_UNIT_PX, this.textSize); 495 | 496 | View line = viewGroupTutorContent.findViewById(view_line); 497 | if (line != null) { 498 | line.setBackgroundColor(lineColorRes); 499 | } 500 | 501 | prevButton = (ImageView) viewGroupTutorContent.findViewById(iv_previous); 502 | nextButton = (ImageView) viewGroupTutorContent.findViewById(iv_next); 503 | skipButton = (ImageView) viewGroupTutorContent.findViewById(iv_skip); 504 | 505 | viewGroupIndicator = (ViewGroup) viewGroupTutorContent.findViewById(view_group_indicator); 506 | 507 | if (prevButton != null) { 508 | prevButton.setImageDrawable(prevDrawable); 509 | prevButton.setOnClickListener(v -> { 510 | if (showCaseListener != null) ShowCaseLayout.this.showCaseListener.onPrevious(); 511 | }); 512 | } 513 | if (nextButton != null) { 514 | nextButton.setImageDrawable(nextDrawable); 515 | nextButton.setOnClickListener(v -> onNextClicked()); 516 | } 517 | 518 | if (skipButton != null){ 519 | skipButton.setImageDrawable(skipDrawable); 520 | skipButton.setOnClickListener(view -> { 521 | if (showCaseListener != null) ShowCaseLayout.this.showCaseListener.onComplete(); 522 | }); 523 | } 524 | 525 | this.addView(viewGroup); 526 | } 527 | 528 | private void moveViewBasedHighlight(int highlightXstart, 529 | int highlightYstart, 530 | int highlightXend, 531 | int highlightYend) { 532 | if (showCaseContentPosition == ShowCaseContentPosition.UNDEFINED) { 533 | int widthCenter = this.getWidth() / 2; 534 | int heightCenter = this.getHeight() / 2; 535 | if (highlightYend <= heightCenter) { 536 | showCaseContentPosition = ShowCaseContentPosition.BOTTOM; 537 | } else if (highlightYstart >= heightCenter) { 538 | showCaseContentPosition = ShowCaseContentPosition.TOP; 539 | } else if (highlightXend <= widthCenter) { 540 | showCaseContentPosition = ShowCaseContentPosition.RIGHT; 541 | } else if (highlightXstart >= widthCenter) { 542 | showCaseContentPosition = ShowCaseContentPosition.LEFT; 543 | } else { // not fit anywhere 544 | // if bottom is bigger, put to bottom, else put it on top 545 | if ((this.getHeight() - highlightYend) > highlightYstart) { 546 | showCaseContentPosition = ShowCaseContentPosition.BOTTOM; 547 | } else { 548 | showCaseContentPosition = ShowCaseContentPosition.TOP; 549 | } 550 | } 551 | } 552 | 553 | LayoutParams layoutParams; 554 | switch (showCaseContentPosition) { 555 | case RIGHT: { 556 | int expectedWidth = getWidth() - highlightXend - 2 * this.spacing; 557 | 558 | viewGroup.measure(MeasureSpec.makeMeasureSpec(expectedWidth, MeasureSpec.EXACTLY), 559 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 560 | int viewGroupHeight = viewGroup.getMeasuredHeight(); 561 | 562 | layoutParams = new LayoutParams( 563 | expectedWidth, 564 | LayoutParams.WRAP_CONTENT, 565 | Gravity.RIGHT); 566 | layoutParams.rightMargin = this.spacing; 567 | layoutParams.leftMargin = this.spacing; 568 | layoutParams.bottomMargin = 0; 569 | 570 | // calculate diff top height between object and the content; 571 | int hightLightHeight = (highlightYend - highlightYstart); 572 | int diffHeight = hightLightHeight - viewGroupHeight; 573 | 574 | // check top margin. top should not out of window 575 | int expectedTopMargin = highlightYstart + diffHeight / 2; 576 | checkMarginTopBottom(expectedTopMargin, layoutParams, viewGroupHeight); 577 | 578 | setLayoutViewGroup(layoutParams); 579 | 580 | if (arrowWidth == 0) { 581 | path = null; 582 | } else { 583 | int highLightCenterY = (highlightYend + highlightYstart) / 2; 584 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterY, getHeight()); 585 | if (recalcArrowWidth == 0) { 586 | path = null; 587 | } else { 588 | path = new Path(); 589 | path.moveTo(highlightXend + this.arrowMargin, highLightCenterY); 590 | path.lineTo(highlightXend + this.spacing, 591 | highLightCenterY - arrowWidth / 2); 592 | path.lineTo(highlightXend + this.spacing, 593 | highLightCenterY + arrowWidth / 2); 594 | path.close(); 595 | } 596 | } 597 | } 598 | break; 599 | case LEFT: { 600 | int expectedWidth = highlightXstart - 2 * this.spacing; 601 | 602 | viewGroup.measure(View.MeasureSpec.makeMeasureSpec(expectedWidth, View.MeasureSpec.EXACTLY), 603 | View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); 604 | int viewGroupHeight = viewGroup.getMeasuredHeight(); 605 | 606 | layoutParams = new LayoutParams( 607 | expectedWidth, 608 | LayoutParams.WRAP_CONTENT, 609 | Gravity.LEFT); 610 | layoutParams.leftMargin = this.spacing; 611 | layoutParams.rightMargin = this.spacing; 612 | layoutParams.bottomMargin = 0; 613 | 614 | // calculate diff top height between object and the content; 615 | int hightLightHeight = (highlightYend - highlightYstart); 616 | int diffHeight = hightLightHeight - viewGroupHeight; 617 | 618 | // check top margin. top should not out of window 619 | int expectedTopMargin = highlightYstart + diffHeight / 2; 620 | checkMarginTopBottom(expectedTopMargin, layoutParams, viewGroupHeight); 621 | 622 | setLayoutViewGroup(layoutParams); 623 | 624 | if (arrowWidth == 0) { 625 | path = null; 626 | } else { 627 | int highLightCenterY = (highlightYend + highlightYstart) / 2; 628 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterY, getHeight()); 629 | if (recalcArrowWidth == 0) { 630 | path = null; 631 | } else { 632 | path = new Path(); 633 | path.moveTo(highlightXstart - this.arrowMargin, highLightCenterY); 634 | path.lineTo(highlightXstart - this.spacing, 635 | highLightCenterY - arrowWidth / 2); 636 | path.lineTo(highlightXstart - this.spacing, 637 | highLightCenterY + arrowWidth / 2); 638 | path.close(); 639 | } 640 | } 641 | } 642 | break; 643 | case BOTTOM: { 644 | layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 645 | LayoutParams.WRAP_CONTENT, 646 | Gravity.TOP); 647 | layoutParams.topMargin = highlightYend + this.spacing; 648 | layoutParams.leftMargin = this.spacing; 649 | layoutParams.rightMargin = this.spacing; 650 | layoutParams.bottomMargin = 0; 651 | 652 | setLayoutViewGroup(layoutParams); 653 | 654 | if (arrowWidth == 0) { 655 | path = null; 656 | } else { 657 | int highLightCenterX = (highlightXend + highlightXstart) / 2; 658 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterX, getWidth()); 659 | if (recalcArrowWidth == 0) { 660 | path = null; 661 | } else { 662 | path = new Path(); 663 | path.moveTo(highLightCenterX, highlightYend + this.arrowMargin); 664 | path.lineTo(highLightCenterX - recalcArrowWidth / 2, 665 | highlightYend + this.spacing); 666 | path.lineTo(highLightCenterX + recalcArrowWidth / 2, 667 | highlightYend + this.spacing); 668 | path.close(); 669 | } 670 | } 671 | } 672 | break; 673 | case TOP: { 674 | layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 675 | LayoutParams.WRAP_CONTENT, 676 | Gravity.BOTTOM); 677 | layoutParams.bottomMargin = getHeight() - highlightYstart + this.spacing; 678 | layoutParams.topMargin = 0; 679 | layoutParams.leftMargin = this.spacing; 680 | layoutParams.rightMargin = this.spacing; 681 | 682 | setLayoutViewGroup(layoutParams); 683 | 684 | if (arrowWidth == 0) { 685 | path = null; 686 | } else { 687 | int highLightCenterX = (highlightXend + highlightXstart) / 2; 688 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterX, getWidth()); 689 | if (recalcArrowWidth == 0) { 690 | path = null; 691 | } else { 692 | path = new Path(); 693 | path.moveTo(highLightCenterX, highlightYstart - this.arrowMargin); 694 | path.lineTo(highLightCenterX - recalcArrowWidth / 2, 695 | highlightYstart - this.spacing); 696 | path.lineTo(highLightCenterX + recalcArrowWidth / 2, 697 | highlightYstart - this.spacing); 698 | path.close(); 699 | } 700 | } 701 | } 702 | break; 703 | case UNDEFINED: 704 | moveViewToCenter(); 705 | break; 706 | } 707 | } 708 | 709 | private void setLayoutViewGroup(LayoutParams params) { 710 | this.viewGroup.setVisibility(View.INVISIBLE); 711 | 712 | this.viewGroup.addOnLayoutChangeListener(new OnLayoutChangeListener() { 713 | @Override 714 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 715 | ShowCaseLayout.this.viewGroup.setVisibility(View.VISIBLE); 716 | ShowCaseLayout.this.viewGroup.removeOnLayoutChangeListener(this); 717 | } 718 | }); 719 | this.viewGroup.setLayoutParams(params); 720 | invalidate(); 721 | } 722 | 723 | private int getRecalculateArrowWidth(int highlightCenter, int maxWidthOrHeight) { 724 | int recalcArrowWidth = arrowWidth; 725 | int safeArrowWidth = this.spacing + (arrowWidth / 2); 726 | if (highlightCenter < safeArrowWidth || 727 | highlightCenter > (maxWidthOrHeight - safeArrowWidth)) { 728 | recalcArrowWidth = 0; 729 | } 730 | return recalcArrowWidth; 731 | } 732 | 733 | private void moveViewToCenter() { 734 | showCaseContentPosition = ShowCaseContentPosition.UNDEFINED; 735 | 736 | LayoutParams layoutParams = new LayoutParams( 737 | LayoutParams.MATCH_PARENT, 738 | LayoutParams.WRAP_CONTENT, 739 | Gravity.CENTER); 740 | layoutParams.rightMargin = this.spacing; 741 | layoutParams.leftMargin = this.spacing; 742 | layoutParams.bottomMargin = this.spacing; 743 | layoutParams.topMargin = this.spacing; 744 | 745 | setLayoutViewGroup(layoutParams); 746 | this.path = null; 747 | } 748 | 749 | private void checkMarginTopBottom(int expectedTopMargin, 750 | LayoutParams layoutParams, 751 | int viewHeight) { 752 | if (expectedTopMargin < this.spacing) { 753 | layoutParams.topMargin = this.spacing; 754 | } else { 755 | // check bottom margin. bottom should not out of window 756 | int prevActualHeight = expectedTopMargin + viewHeight + this.spacing; 757 | if (prevActualHeight > getHeight()) { 758 | int diff = prevActualHeight - getHeight(); 759 | layoutParams.topMargin = expectedTopMargin - diff; 760 | } else { 761 | layoutParams.topMargin = expectedTopMargin; 762 | } 763 | } 764 | } 765 | 766 | private void recycleResources() { 767 | if (this.bitmap != null) { 768 | this.bitmap.recycle(); 769 | } 770 | this.bitmap = null; 771 | if (this.lastTutorialView != null) { 772 | this.lastTutorialView.setDrawingCacheEnabled(false); 773 | } 774 | this.lastTutorialView = null; 775 | this.viewPaint = null; 776 | } 777 | 778 | private Spanned fromHtml(String html) { 779 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 780 | return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); 781 | } else { 782 | return Html.fromHtml(html); 783 | } 784 | } 785 | } 786 | --------------------------------------------------------------------------------