├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ └── activity_main.xml │ │ ├── values-night │ │ │ └── themes.xml │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ └── drawable │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ └── com │ │ │ └── dizcoding │ │ │ └── collection │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── base ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ ├── themes.xml │ │ │ └── dimens.xml │ │ └── drawable │ │ │ └── dizcoding_base_white_background_with_top_corner_15dp.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── dizcoding │ │ └── base │ │ ├── extension │ │ ├── ViewExtension.kt │ │ ├── DialogExtension.kt │ │ └── ContextExtension.kt │ │ ├── BaseMapper.kt │ │ ├── BaseFragment.kt │ │ ├── BaseActivity.kt │ │ ├── BaseBottomSheetDialogFragment.kt │ │ ├── BaseDialog.kt │ │ └── BaseDialogFragment.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 ├── proguard-rules.pro └── build.gradle ├── README.md ├── adapterdelegate ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── dizcoding │ │ │ └── adapterdelegate │ │ │ ├── LayoutContainer.kt │ │ │ ├── ItemDelegate.kt │ │ │ ├── CreateItemDelegate.kt │ │ │ ├── ClickableItemDelegate.kt │ │ │ ├── BindItemDelegate.kt │ │ │ ├── ExtensionViewHolder.kt │ │ │ ├── LayoutItemDelegate.kt │ │ │ ├── ExtensionAdapterDelegate.kt │ │ │ ├── EndlessRecyclerViewScrollListener.kt │ │ │ └── DelegatesAdapter.kt │ │ └── res │ │ └── drawable │ │ └── vector_ic_peoples_blue.xml ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── misc.xml └── gradle.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /base/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /base/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /showcase/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /showcase/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-collection -------------------------------------------------------------------------------- /adapterdelegate/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /adapterdelegate/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Collection 3 | -------------------------------------------------------------------------------- /base/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Collection 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gandhi-wibowo/android-collection/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /base/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /showcase/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /adapterdelegate/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/LayoutContainer.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.view.View 4 | 5 | interface LayoutContainer { 6 | val containerView: View 7 | } -------------------------------------------------------------------------------- /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 | #Fri Apr 15 23:20:29 WIB 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-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 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/extension/ViewExtension.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base.extension 2 | 3 | import android.view.View 4 | 5 | fun View?.avoidDoubleClicks() { 6 | this ?: return 7 | if (!this.isClickable) { 8 | return 9 | } 10 | this.isClickable = false 11 | this.postDelayed({ this.isClickable = true }, 1000L) 12 | } -------------------------------------------------------------------------------- /showcase/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #B0000000 4 | #42b549 5 | #1B4DB3 6 | #FFFFFF 7 | #DDDDDD 8 | #434655 9 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/BaseMapper.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base 2 | 3 | abstract class BaseMapper { 4 | fun apply(oldItem: T): R { 5 | return map(oldItem) 6 | } 7 | 8 | fun apply(oldItem: List): List { 9 | return oldItem.map { 10 | apply(it) 11 | } 12 | } 13 | 14 | protected abstract fun map(oldItem: T): R 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dizcoding/collection/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.collection 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | 6 | class MainActivity : AppCompatActivity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | setContentView(R.layout.activity_main) 10 | } 11 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/ItemDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | interface ItemDelegate { 7 | fun itemType(): Class 8 | fun createViewHolder(parent: ViewGroup): H 9 | fun bindView(position: Int, item: I, holder: H) 10 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | } 8 | } 9 | rootProject.name = "Collection" 10 | include ':app' 11 | include ':adapterdelegate' 12 | include ':showcase' 13 | include ':base' 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /base/src/main/res/drawable/dizcoding_base_white_background_with_top_corner_15dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /base/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/CreateItemDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.view.ViewGroup 4 | 5 | class CreateItemDelegate( 6 | private val origin: ExtensionItemDelegate, 7 | private val createBlock: ExtensionViewHolder.() -> Unit 8 | ) : ExtensionItemDelegate by origin { 9 | 10 | override fun createViewHolder(parent: ViewGroup) = origin.createViewHolder(parent).apply(createBlock) 11 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/ClickableItemDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.view.ViewGroup 4 | 5 | class ClickableItemDelegate( 6 | private val origin: ExtensionItemDelegate, 7 | private val onClick: (I) -> Unit 8 | ) : ExtensionItemDelegate by origin { 9 | override fun createViewHolder(parent: ViewGroup) = 10 | origin.createViewHolder(parent).apply { 11 | itemView bindClick onClick 12 | } 13 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/BindItemDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | class BindItemDelegate( 4 | private val origin: ExtensionItemDelegate, 5 | private val bindBlock: ExtensionViewHolder.(I) -> Unit 6 | ) : ExtensionItemDelegate by origin { 7 | 8 | override fun bindView(position: Int, item: I, holder: ExtensionViewHolder) { 9 | origin.bindView(position, item, holder) 10 | holder.holdItem?.let { holder.bindBlock(it) } 11 | } 12 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/ExtensionViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | class ExtensionViewHolder(override val containerView: View) : 7 | RecyclerView.ViewHolder(containerView), LayoutContainer { 8 | var holdItem: I? = null 9 | 10 | inline infix fun View.bindClick( 11 | crossinline onClick: (I) -> Unit 12 | ) = setOnClickListener { holdItem?.let(onClick) } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /base/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/LayoutItemDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.annotation.LayoutRes 6 | 7 | class LayoutItemDelegate( 8 | private val type: Class, 9 | @LayoutRes private val layoutId: Int 10 | ) : ExtensionItemDelegate { 11 | 12 | override fun itemType() = type 13 | 14 | override fun createViewHolder(parent: ViewGroup) = 15 | ExtensionViewHolder(LayoutInflater.from(parent.context).inflate(layoutId, parent, false)) 16 | 17 | override fun bindView(position: Int, item: I, holder: ExtensionViewHolder) { 18 | holder.holdItem = item 19 | } 20 | } -------------------------------------------------------------------------------- /showcase/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /base/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/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 -------------------------------------------------------------------------------- /adapterdelegate/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/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/extension/DialogExtension.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base.extension 2 | 3 | import android.app.Dialog 4 | import android.util.DisplayMetrics 5 | 6 | var deviceWidth = 0 7 | var deviceHeight = 0 8 | 9 | fun Dialog.getWidth(): Int { 10 | return if (deviceWidth == 0) { 11 | val displayMetrics = DisplayMetrics() 12 | this.window?.windowManager?.defaultDisplay?.getMetrics(displayMetrics) 13 | deviceWidth = displayMetrics.widthPixels 14 | deviceWidth 15 | } else { 16 | deviceWidth 17 | } 18 | } 19 | 20 | fun Dialog.getHigh(): Int { 21 | return if (deviceHeight == 0) { 22 | val displayMetrics = DisplayMetrics() 23 | this.window?.windowManager?.defaultDisplay?.getMetrics(displayMetrics) 24 | deviceHeight = displayMetrics.heightPixels 25 | deviceHeight 26 | } else { 27 | deviceHeight 28 | } 29 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | -------------------------------------------------------------------------------- /showcase/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | android { 5 | compileSdkVersion 30 6 | buildToolsVersion "30.0.2" 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 30 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | implementation "androidx.appcompat:appcompat:1.2.0" 33 | implementation "com.google.android.material:material:1.2.1" 34 | testImplementation 'junit:junit:4.+' 35 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 37 | } -------------------------------------------------------------------------------- /adapterdelegate/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion "30.0.2" 8 | 9 | defaultConfig { 10 | minSdkVersion 16 11 | targetSdkVersion 30 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation "androidx.appcompat:appcompat:1.2.0" 36 | implementation "com.google.android.material:material:1.2.1" 37 | testImplementation 'junit:junit:4.+' 38 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 40 | } -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | 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 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.dizcoding.collection" 11 | minSdk 21 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | // enable viewbinding 33 | buildFeatures { 34 | viewBinding = true 35 | } 36 | } 37 | 38 | dependencies { 39 | 40 | implementation 'androidx.core:core-ktx:1.7.0' 41 | implementation 'androidx.appcompat:appcompat:1.4.1' 42 | implementation 'com.google.android.material:material:1.5.0' 43 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 44 | 45 | implementation project(path: ':adapterdelegate') 46 | implementation project(path: ':showcase') 47 | 48 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/ExtensionAdapterDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.annotation.LayoutRes 5 | 6 | typealias ExtensionItemDelegate = ItemDelegate> 7 | 8 | 9 | inline fun itemDelegate( 10 | @LayoutRes layoutId: Int 11 | ) = LayoutItemDelegate(T::class.java, layoutId) 12 | 13 | fun ExtensionItemDelegate.click( 14 | onClick: (T) -> Unit 15 | ) = ClickableItemDelegate(this, onClick) 16 | 17 | fun ExtensionItemDelegate.create( 18 | block: ExtensionViewHolder.() -> Unit 19 | ) = CreateItemDelegate(this, block) 20 | 21 | fun ExtensionItemDelegate.bind( 22 | block: ExtensionViewHolder.(item: T) -> Unit 23 | ) = BindItemDelegate(this, block) 24 | 25 | 26 | fun DelegatesAdapter.getItem( 27 | predict: (I) -> Boolean 28 | ): I? { 29 | return this.items.find(predict) 30 | } 31 | 32 | @SuppressLint("NotifyDataSetChanged") 33 | fun DelegatesAdapter.updateItems( 34 | item: List 35 | ) { 36 | this.items = item 37 | notifyDataSetChanged() 38 | } 39 | 40 | fun DelegatesAdapter.updateItem( 41 | item: I, itemPosition: Int 42 | ) { 43 | this.items.toMutableList()[itemPosition] = item 44 | notifyItemChanged(itemPosition) 45 | } -------------------------------------------------------------------------------- /base/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | android { 6 | compileSdk 31 7 | compileSdkVersion 31 8 | buildToolsVersion '30.0.3' 9 | 10 | defaultConfig { 11 | minSdk 16 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | targetSdkVersion 31 19 | minSdkVersion 16 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | } 35 | // enable viewbinding 36 | buildFeatures { 37 | viewBinding = true 38 | } 39 | 40 | } 41 | 42 | dependencies { 43 | 44 | implementation 'androidx.core:core-ktx:1.7.0' 45 | implementation "androidx.appcompat:appcompat:1.2.0" 46 | implementation "com.google.android.material:material:1.2.1" 47 | 48 | testImplementation 'junit:junit:4.+' 49 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 50 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 51 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/EndlessRecyclerViewScrollListener.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import androidx.recyclerview.widget.LinearLayoutManager 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | 7 | abstract class EndlessRecyclerViewScrollListener(layoutManager: LinearLayoutManager) : 8 | RecyclerView.OnScrollListener() { 9 | private val visibleThreshold = 5 10 | private var previousTotalItemCount = 0 11 | private var loading = true 12 | var mLayoutManager: RecyclerView.LayoutManager = layoutManager 13 | override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { 14 | var lastVisibleItemPosition = 0 15 | val totalItemCount = mLayoutManager.itemCount 16 | lastVisibleItemPosition = 17 | (mLayoutManager as LinearLayoutManager).findLastVisibleItemPosition() 18 | if (totalItemCount < previousTotalItemCount) { 19 | previousTotalItemCount = totalItemCount 20 | if (totalItemCount == 0) { 21 | loading = true 22 | } 23 | } 24 | if (loading && totalItemCount > previousTotalItemCount) { 25 | loading = false 26 | previousTotalItemCount = totalItemCount 27 | } 28 | if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) { 29 | onLoadMore() 30 | loading = true 31 | } 32 | } 33 | 34 | fun resetState() { 35 | previousTotalItemCount = 0 36 | loading = true 37 | } 38 | 39 | abstract fun onLoadMore() 40 | 41 | } -------------------------------------------------------------------------------- /showcase/src/main/res/drawable/ic_button_next.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.core.os.bundleOf 8 | import androidx.fragment.app.DialogFragment 9 | import androidx.fragment.app.Fragment 10 | import androidx.viewbinding.ViewBinding 11 | import com.dizcoding.base.extension.avoidDoubleClicks 12 | 13 | 14 | abstract class BaseFragment : Fragment(), View.OnClickListener { 15 | 16 | private var _binding: ViewBinding? = null 17 | abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB 18 | 19 | @Suppress("UNCHECKED_CAST") 20 | protected val binding: VB 21 | get() = _binding as VB 22 | 23 | override fun onCreateView( 24 | inflater: LayoutInflater, 25 | container: ViewGroup?, 26 | savedInstanceState: Bundle? 27 | ): View? { 28 | _binding = bindingInflater.invoke(inflater, container, false) 29 | return requireNotNull(_binding).root 30 | } 31 | 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | super.onViewCreated(view, savedInstanceState) 34 | 35 | setup() 36 | } 37 | 38 | abstract fun setup() 39 | 40 | override fun onDestroyView() { 41 | super.onDestroyView() 42 | _binding = null 43 | } 44 | 45 | override fun onClick(v: View?) { 46 | v.avoidDoubleClicks() 47 | } 48 | 49 | fun showDialogFragment(dialogFragment: DialogFragment, bundle: Bundle = bundleOf()) { 50 | if (!bundle.isEmpty) dialogFragment.arguments = bundle 51 | dialogFragment.show(childFragmentManager, "Tag") 52 | } 53 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/java/com/dizcoding/adapterdelegate/DelegatesAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.adapterdelegate 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class DelegatesAdapter( 8 | vararg delegates: ItemDelegate 9 | ) : RecyclerView.Adapter() { 10 | 11 | private val delegatesIndexMap: Map, Int> 12 | private val delegatesList: List> 13 | 14 | var items = emptyList() 15 | 16 | init { 17 | val map = mutableMapOf, Int>() 18 | delegates.forEachIndexed { index, delegate -> 19 | if (map.put(delegate.itemType(), index) != null) { 20 | throw IllegalArgumentException() 21 | } 22 | } 23 | 24 | delegatesIndexMap = map 25 | delegatesList = delegates.map { 26 | @Suppress("UNCHECKED_CAST") 27 | it as? ItemDelegate 28 | ?: throw IllegalArgumentException() 29 | } 30 | } 31 | 32 | override fun getItemViewType(position: Int) = 33 | delegatesIndexMap[items[position]::class.java] ?: throw IllegalArgumentException() 34 | 35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 36 | delegatesList[viewType].createViewHolder(parent) 37 | 38 | override fun getItemCount() = items.size 39 | 40 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 41 | delegatesList[getItemViewType(position)].bindView(position, items[position], holder) 42 | } 43 | 44 | @SuppressLint("NotifyDataSetChanged") 45 | fun submitList(list: List) { 46 | items = list 47 | notifyDataSetChanged() 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.os.bundleOf 9 | import androidx.fragment.app.DialogFragment 10 | import androidx.viewbinding.ViewBinding 11 | import com.dizcoding.base.extension.avoidDoubleClicks 12 | 13 | abstract class BaseActivity : AppCompatActivity(), View.OnClickListener { 14 | private var _binding: ViewBinding? = null 15 | abstract val bindingInflater: (LayoutInflater) -> VB 16 | 17 | @Suppress("UNCHECKED_CAST") 18 | protected val binding: VB 19 | get() = _binding as VB 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | _binding = bindingInflater.invoke(layoutInflater) 24 | setContentView(requireNotNull(_binding).root) 25 | setup() 26 | } 27 | 28 | abstract fun setup() 29 | 30 | override fun onDestroy() { 31 | super.onDestroy() 32 | _binding = null 33 | } 34 | 35 | override fun onClick(v: View?) { 36 | v.avoidDoubleClicks() 37 | } 38 | 39 | fun intentTo(cls: Class<*>, vararg bundle: Bundle) { 40 | startActivity( 41 | Intent(this, cls) 42 | .apply { 43 | if (bundle.isNotEmpty()) { 44 | bundle.forEach { 45 | putExtras(it) 46 | } 47 | } 48 | } 49 | ) 50 | } 51 | 52 | fun showDialogFragment(dialogFragment: DialogFragment, bundle: Bundle = bundleOf()) { 53 | if (!bundle.isEmpty) dialogFragment.arguments = bundle 54 | dialogFragment.show(supportFragmentManager, "Tag") 55 | } 56 | } -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/BaseBottomSheetDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.viewbinding.ViewBinding 9 | import com.dizcoding.base.extension.avoidDoubleClicks 10 | import com.google.android.material.bottomsheet.BottomSheetBehavior 11 | import com.google.android.material.bottomsheet.BottomSheetDialog 12 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 13 | 14 | 15 | abstract class BaseBottomSheetDialogFragment : BottomSheetDialogFragment(), 16 | View.OnClickListener { 17 | 18 | private var _binding: ViewBinding? = null 19 | abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB 20 | 21 | @Suppress("UNCHECKED_CAST") 22 | protected val binding: VB 23 | get() = _binding as VB 24 | 25 | protected abstract fun isFullHeigh(): Boolean 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, 29 | container: ViewGroup?, 30 | savedInstanceState: Bundle? 31 | ): View { 32 | _binding = bindingInflater.invoke(inflater, container, false) 33 | return requireNotNull(_binding).root 34 | } 35 | 36 | override fun getTheme(): Int = R.style.DizcodingBaseBottomSheetDialog 37 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 38 | val dialog = BottomSheetDialog(requireContext(), theme) 39 | if (isFullHeigh()) dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED 40 | return dialog 41 | } 42 | 43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 44 | super.onViewCreated(view, savedInstanceState) 45 | setup() 46 | } 47 | 48 | abstract fun setup() 49 | 50 | override fun onDestroyView() { 51 | super.onDestroyView() 52 | _binding = null 53 | } 54 | 55 | override fun onClick(v: View?) { 56 | v.avoidDoubleClicks() 57 | } 58 | } -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/BaseDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.drawable.ColorDrawable 7 | import android.os.Bundle 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.WindowManager 11 | import androidx.viewbinding.ViewBinding 12 | import com.dizcoding.base.extension.avoidDoubleClicks 13 | import com.dizcoding.base.extension.getWidth 14 | 15 | abstract class BaseDialog(context: Context) : Dialog(context), 16 | View.OnClickListener { 17 | 18 | private var _binding: ViewBinding? = null 19 | abstract val bindingInflater: (LayoutInflater) -> VB 20 | abstract val dialogCanCancle: Boolean 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | protected val binding: VB 24 | get() = _binding as VB 25 | 26 | override fun onStart() { 27 | super.onStart() 28 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 29 | } 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | _binding = bindingInflater.invoke(layoutInflater) 34 | setContentView(requireNotNull(_binding).root) 35 | setCancelable(dialogCanCancle) 36 | 37 | window?.setLayout( 38 | WindowManager.LayoutParams.MATCH_PARENT, 39 | WindowManager.LayoutParams.WRAP_CONTENT 40 | ) 41 | val layoutParams = window?.attributes 42 | val width = getWidth() 43 | val dimens = context.resources?.getDimension(R.dimen.dizcoding_base_dimens_space_60) 44 | 45 | if (dimens != null) { 46 | layoutParams?.width = width - dimens.toInt() 47 | window?.attributes = layoutParams 48 | } 49 | setup() 50 | } 51 | 52 | abstract fun setup() 53 | 54 | override fun onDetachedFromWindow() { 55 | super.onDetachedFromWindow() 56 | _binding = null 57 | } 58 | 59 | override fun onClick(v: View?) { 60 | v.avoidDoubleClicks() 61 | } 62 | } -------------------------------------------------------------------------------- /adapterdelegate/src/main/res/drawable/vector_ic_peoples_blue.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/BaseDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.ColorDrawable 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.view.WindowManager 10 | import androidx.fragment.app.DialogFragment 11 | import androidx.viewbinding.ViewBinding 12 | import com.dizcoding.base.extension.avoidDoubleClicks 13 | import com.dizcoding.base.extension.getWidth 14 | 15 | 16 | abstract class BaseDialogFragment : DialogFragment(), View.OnClickListener { 17 | 18 | private var _binding: ViewBinding? = null 19 | abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB 20 | 21 | @Suppress("UNCHECKED_CAST") 22 | protected val binding: VB 23 | get() = _binding as VB 24 | 25 | override fun onCreateView( 26 | inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? { 30 | _binding = bindingInflater.invoke(inflater, container, false) 31 | return requireNotNull(_binding).root 32 | } 33 | 34 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 35 | super.onViewCreated(view, savedInstanceState) 36 | dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 37 | dialog?.window?.setLayout( 38 | WindowManager.LayoutParams.MATCH_PARENT, 39 | WindowManager.LayoutParams.WRAP_CONTENT 40 | ) 41 | val layoutParams = dialog?.window?.attributes 42 | val width = dialog?.getWidth() ?: 0 43 | val dimens = context?.resources?.getDimension(R.dimen.dizcoding_base_dimens_space_16) 44 | 45 | if (dimens != null) { 46 | layoutParams?.width = width - dimens.toInt() 47 | dialog?.window?.attributes = layoutParams 48 | } 49 | setup() 50 | } 51 | 52 | override fun onDestroyView() { 53 | super.onDestroyView() 54 | _binding = null 55 | } 56 | 57 | override fun onClick(v: View?) { 58 | v.avoidDoubleClicks() 59 | } 60 | 61 | abstract fun setup() 62 | 63 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /base/src/main/java/com/dizcoding/base/extension/ContextExtension.kt: -------------------------------------------------------------------------------- 1 | package com.dizcoding.base.extension 2 | 3 | import android.app.DatePickerDialog 4 | import android.app.TimePickerDialog 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.database.Cursor 8 | import android.net.Uri 9 | import android.provider.MediaStore 10 | import java.util.* 11 | 12 | 13 | fun Context.openChatWhatsApp(number: String, messages: String? = null) { 14 | var url = "https://api.whatsapp.com/send?phone=+$number" 15 | if (!messages.isNullOrEmpty()) { 16 | url += "&text=$messages" 17 | } 18 | val i = Intent(Intent.ACTION_VIEW) 19 | i.data = Uri.parse(url) 20 | this.startActivity(i) 21 | } 22 | 23 | fun Context.openSmsApp(number: String) { 24 | val i = Intent(Intent.ACTION_VIEW) 25 | i.data = Uri.parse("smsto:$number") 26 | this.startActivity(i) 27 | } 28 | 29 | fun Context.openCallApp(number: String) { 30 | val i = Intent(Intent.ACTION_DIAL) 31 | i.data = Uri.parse("tel:$number") 32 | this.startActivity(i) 33 | } 34 | 35 | fun Context.openBrowser(url: String?) { 36 | val i = Intent(Intent.ACTION_VIEW) 37 | i.data = Uri.parse(url) 38 | this.startActivity(i) 39 | } 40 | 41 | fun Context.openMailApp(email: String, hashMap: HashMap = hashMapOf()) { 42 | val i = Intent(Intent.ACTION_SENDTO) 43 | i.data = Uri.parse("mailto:$email") 44 | if (hashMap.isNotEmpty()) { 45 | hashMap.forEach { 46 | i.putExtra(it.key, it.value) 47 | } 48 | } 49 | this.startActivity(i) 50 | } 51 | 52 | fun Context.showDatePickerDialog(selectedDate: (Int, Int, Int) -> Unit) { 53 | val calendar = Calendar.getInstance(Locale.getDefault()) 54 | DatePickerDialog( 55 | this, 56 | { _, p1, p2, p3 -> 57 | selectedDate.invoke(p1, p2, p3) 58 | }, 59 | calendar.get(Calendar.YEAR), 60 | calendar.get(Calendar.MONTH), 61 | calendar.get(Calendar.DAY_OF_MONTH) 62 | ).show() 63 | } 64 | 65 | fun Context.showTimePickerDialog(selectedTime: (Int, Int) -> Unit) { 66 | val calendar = Calendar.getInstance(Locale.getDefault()) 67 | TimePickerDialog( 68 | this, 69 | { _, p1, p2 -> 70 | selectedTime.invoke(p1, p2) 71 | }, 72 | calendar.get(Calendar.HOUR_OF_DAY), 73 | calendar.get(Calendar.MINUTE), 74 | true 75 | ).show() 76 | } 77 | 78 | fun Context.getRealPath(file: Uri, result: (String) -> Unit) { 79 | var cursor: Cursor? = null 80 | try { 81 | val proj = arrayOf(MediaStore.Images.Media.DATA) 82 | cursor = this.contentResolver.query(file, proj, null, null, null) 83 | val columnIndex: Int? = cursor?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) 84 | cursor?.moveToFirst() 85 | columnIndex?.let { i -> 86 | cursor?.getString(i)?.let { 87 | result.invoke(it) 88 | } 89 | } 90 | } catch (e: Exception) { 91 | result.invoke("") 92 | } finally { 93 | cursor?.close() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /base/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2sp 6 | 11sp 7 | 12sp 8 | 13sp 9 | 14sp 10 | 15sp 11 | 16sp 12 | 18sp 13 | 21sp 14 | 24sp 15 | 30sp 16 | 36sp 17 | 18 | 4dp 19 | 5dp 20 | 10dp 21 | 12dp 22 | 15dp 23 | 20dp 24 | 26dp 25 | 26 | 0dp 27 | 0.3dp 28 | 1dp 29 | 2dp 30 | 3dp 31 | 4dp 32 | 5dp 33 | 6dp 34 | 7dp 35 | 8dp 36 | 10dp 37 | 11dp 38 | 12dp 39 | 14dp 40 | 15dp 41 | 16dp 42 | 18dp 43 | 20dp 44 | 23dp 45 | 24dp 46 | 25dp 47 | 27dp 48 | 30dp 49 | 32dp 50 | 33dp 51 | 36dp 52 | 38dp 53 | 40dp 54 | 48dp 55 | 50dp 56 | 52dp 57 | 55dp 58 | 56dp 59 | 60dp 60 | 66dp 61 | 70dp 62 | 78dp 63 | 84dp 64 | 100dp 65 | 115dp 66 | 120dp 67 | 160dp 68 | 200dp 69 | 300dp 70 | 500dp 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 Creator CREATOR = new 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 | } /* 540 | TODO 541 | this blocked code is should be not commented 542 | 543 | else if (highlightXend <= widthCenter) { 544 | showCaseContentPosition = ShowCaseContentPosition.RIGHT; 545 | } else if (highlightXstart >= widthCenter) { 546 | showCaseContentPosition = ShowCaseContentPosition.LEFT; 547 | }*/ else { // not fit anywhere 548 | // if bottom is bigger, put to bottom, else put it on top 549 | if ((this.getHeight() - highlightYend) > highlightYstart) { 550 | showCaseContentPosition = ShowCaseContentPosition.BOTTOM; 551 | } else { 552 | showCaseContentPosition = ShowCaseContentPosition.TOP; 553 | } 554 | } 555 | } 556 | 557 | LayoutParams layoutParams; 558 | switch (showCaseContentPosition) { 559 | case RIGHT: { 560 | int expectedWidth = getWidth() - highlightXend - 2 * this.spacing; 561 | 562 | viewGroup.measure(MeasureSpec.makeMeasureSpec(expectedWidth, MeasureSpec.EXACTLY), 563 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 564 | int viewGroupHeight = viewGroup.getMeasuredHeight(); 565 | 566 | layoutParams = new LayoutParams( 567 | expectedWidth, 568 | LayoutParams.WRAP_CONTENT, 569 | Gravity.RIGHT); 570 | layoutParams.rightMargin = this.spacing; 571 | layoutParams.leftMargin = this.spacing; 572 | layoutParams.bottomMargin = 0; 573 | 574 | // calculate diff top height between object and the content; 575 | int hightLightHeight = (highlightYend - highlightYstart); 576 | int diffHeight = hightLightHeight - viewGroupHeight; 577 | 578 | // check top margin. top should not out of window 579 | int expectedTopMargin = highlightYstart + diffHeight / 2; 580 | checkMarginTopBottom(expectedTopMargin, layoutParams, viewGroupHeight); 581 | 582 | setLayoutViewGroup(layoutParams); 583 | 584 | if (arrowWidth == 0) { 585 | path = null; 586 | } else { 587 | int highLightCenterY = (highlightYend + highlightYstart) / 2; 588 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterY, getHeight()); 589 | if (recalcArrowWidth == 0) { 590 | path = null; 591 | } else { 592 | path = new Path(); 593 | path.moveTo(highlightXend + this.arrowMargin, highLightCenterY); 594 | path.lineTo(highlightXend + this.spacing, 595 | highLightCenterY - arrowWidth / 2); 596 | path.lineTo(highlightXend + this.spacing, 597 | highLightCenterY + arrowWidth / 2); 598 | path.close(); 599 | } 600 | } 601 | } 602 | break; 603 | case LEFT: { 604 | int expectedWidth = highlightXstart - 2 * this.spacing; 605 | 606 | viewGroup.measure(MeasureSpec.makeMeasureSpec(expectedWidth, MeasureSpec.EXACTLY), 607 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 608 | int viewGroupHeight = viewGroup.getMeasuredHeight(); 609 | 610 | layoutParams = new LayoutParams( 611 | expectedWidth, 612 | LayoutParams.WRAP_CONTENT, 613 | Gravity.LEFT); 614 | layoutParams.leftMargin = this.spacing; 615 | layoutParams.rightMargin = this.spacing; 616 | layoutParams.bottomMargin = 0; 617 | 618 | // calculate diff top height between object and the content; 619 | int hightLightHeight = (highlightYend - highlightYstart); 620 | int diffHeight = hightLightHeight - viewGroupHeight; 621 | 622 | // check top margin. top should not out of window 623 | int expectedTopMargin = highlightYstart + diffHeight / 2; 624 | checkMarginTopBottom(expectedTopMargin, layoutParams, viewGroupHeight); 625 | 626 | setLayoutViewGroup(layoutParams); 627 | 628 | if (arrowWidth == 0) { 629 | path = null; 630 | } else { 631 | int highLightCenterY = (highlightYend + highlightYstart) / 2; 632 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterY, getHeight()); 633 | if (recalcArrowWidth == 0) { 634 | path = null; 635 | } else { 636 | path = new Path(); 637 | path.moveTo(highlightXstart - this.arrowMargin, highLightCenterY); 638 | path.lineTo(highlightXstart - this.spacing, 639 | highLightCenterY - arrowWidth / 2); 640 | path.lineTo(highlightXstart - this.spacing, 641 | highLightCenterY + arrowWidth / 2); 642 | path.close(); 643 | } 644 | } 645 | } 646 | break; 647 | case BOTTOM: { 648 | layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 649 | LayoutParams.WRAP_CONTENT, 650 | Gravity.TOP); 651 | layoutParams.topMargin = highlightYend + this.spacing; 652 | layoutParams.leftMargin = this.spacing; 653 | layoutParams.rightMargin = this.spacing; 654 | layoutParams.bottomMargin = 0; 655 | 656 | setLayoutViewGroup(layoutParams); 657 | 658 | if (arrowWidth == 0) { 659 | path = null; 660 | } else { 661 | int highLightCenterX = (highlightXend + highlightXstart) / 2; 662 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterX, getWidth()); 663 | if (recalcArrowWidth == 0) { 664 | path = null; 665 | } else { 666 | path = new Path(); 667 | path.moveTo(highLightCenterX, highlightYend + this.arrowMargin); 668 | path.lineTo(highLightCenterX - recalcArrowWidth / 2, 669 | highlightYend + this.spacing); 670 | path.lineTo(highLightCenterX + recalcArrowWidth / 2, 671 | highlightYend + this.spacing); 672 | path.close(); 673 | } 674 | } 675 | } 676 | break; 677 | case TOP: { 678 | layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 679 | LayoutParams.WRAP_CONTENT, 680 | Gravity.BOTTOM); 681 | layoutParams.bottomMargin = getHeight() - highlightYstart + this.spacing; 682 | layoutParams.topMargin = 0; 683 | layoutParams.leftMargin = this.spacing; 684 | layoutParams.rightMargin = this.spacing; 685 | 686 | setLayoutViewGroup(layoutParams); 687 | 688 | if (arrowWidth == 0) { 689 | path = null; 690 | } else { 691 | int highLightCenterX = (highlightXend + highlightXstart) / 2; 692 | int recalcArrowWidth = getRecalculateArrowWidth(highLightCenterX, getWidth()); 693 | if (recalcArrowWidth == 0) { 694 | path = null; 695 | } else { 696 | path = new Path(); 697 | path.moveTo(highLightCenterX, highlightYstart - this.arrowMargin); 698 | path.lineTo(highLightCenterX - recalcArrowWidth / 2, 699 | highlightYstart - this.spacing); 700 | path.lineTo(highLightCenterX + recalcArrowWidth / 2, 701 | highlightYstart - this.spacing); 702 | path.close(); 703 | } 704 | } 705 | } 706 | break; 707 | case UNDEFINED: 708 | moveViewToCenter(); 709 | break; 710 | } 711 | } 712 | 713 | private void setLayoutViewGroup(LayoutParams params) { 714 | this.viewGroup.setVisibility(View.INVISIBLE); 715 | 716 | this.viewGroup.addOnLayoutChangeListener(new OnLayoutChangeListener() { 717 | @Override 718 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 719 | ShowCaseLayout.this.viewGroup.setVisibility(View.VISIBLE); 720 | ShowCaseLayout.this.viewGroup.removeOnLayoutChangeListener(this); 721 | } 722 | }); 723 | this.viewGroup.setLayoutParams(params); 724 | invalidate(); 725 | } 726 | 727 | private int getRecalculateArrowWidth(int highlightCenter, int maxWidthOrHeight) { 728 | int recalcArrowWidth = arrowWidth; 729 | int safeArrowWidth = this.spacing + (arrowWidth / 2); 730 | if (highlightCenter < safeArrowWidth || 731 | highlightCenter > (maxWidthOrHeight - safeArrowWidth)) { 732 | recalcArrowWidth = 0; 733 | } 734 | return recalcArrowWidth; 735 | } 736 | 737 | private void moveViewToCenter() { 738 | showCaseContentPosition = ShowCaseContentPosition.UNDEFINED; 739 | 740 | LayoutParams layoutParams = new LayoutParams( 741 | LayoutParams.MATCH_PARENT, 742 | LayoutParams.WRAP_CONTENT, 743 | Gravity.CENTER); 744 | layoutParams.rightMargin = this.spacing; 745 | layoutParams.leftMargin = this.spacing; 746 | layoutParams.bottomMargin = this.spacing; 747 | layoutParams.topMargin = this.spacing; 748 | 749 | setLayoutViewGroup(layoutParams); 750 | this.path = null; 751 | } 752 | 753 | private void checkMarginTopBottom(int expectedTopMargin, 754 | LayoutParams layoutParams, 755 | int viewHeight) { 756 | if (expectedTopMargin < this.spacing) { 757 | layoutParams.topMargin = this.spacing; 758 | } else { 759 | // check bottom margin. bottom should not out of window 760 | int prevActualHeight = expectedTopMargin + viewHeight + this.spacing; 761 | if (prevActualHeight > getHeight()) { 762 | int diff = prevActualHeight - getHeight(); 763 | layoutParams.topMargin = expectedTopMargin - diff; 764 | } else { 765 | layoutParams.topMargin = expectedTopMargin; 766 | } 767 | } 768 | } 769 | 770 | private void recycleResources() { 771 | if (this.bitmap != null) { 772 | this.bitmap.recycle(); 773 | } 774 | this.bitmap = null; 775 | if (this.lastTutorialView != null) { 776 | this.lastTutorialView.setDrawingCacheEnabled(false); 777 | } 778 | this.lastTutorialView = null; 779 | this.viewPaint = null; 780 | } 781 | 782 | private Spanned fromHtml(String html) { 783 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 784 | return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); 785 | } else { 786 | return Html.fromHtml(html); 787 | } 788 | } 789 | } 790 | --------------------------------------------------------------------------------