diff --git a/.gitignore b/.gitignore index fd312dd..b0b6a47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,17 @@ -art-mp4/ +node_modules/ +package-lock.json +runtest.js +uploadapk.sh +uploadipa.sh -# git init defaults -.DS_Store -.externalNativeBuild +# Created by https://www.gitignore.io/api/macos,windows,android,jetbrains+all +# Edit at https://www.gitignore.io/?templates=macos,windows,android,jetbrains+all +### Android ### # Built application files *.apk *.ap_ +*.aab # Files for the ART/Dalvik VM *.dex @@ -38,11 +43,175 @@ proguard/ # Android Studio captures folder captures/ -# Intellij +# IntelliJ *.iml -.idea/ -# .idea/workspace.xml -# .idea/libraries +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches # Keystore files -*.jks +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +### Android Patch ### +gen-external-apklibs + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +modules.xml +.idea/misc.xml +*.ipr + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/macos,windows,android,jetbrains+all + + +realm/** \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 2a16af5..9d21618 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,9 @@ android: components: - tools - platform-tools - - build-tools-28.0.0 - - android-28 - - doc-28 + - build-tools-29.0.2 + - android-29 + - doc-29 cache: directories: diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f331654 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,68 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + + defaultConfig { + applicationId "com.otaliastudios" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + lintOptions { + abortOnError false + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0-rc01' + implementation 'androidx.appcompat:appcompat-resources:1.1.0-rc01' + implementation 'androidx.core:core-ktx:1.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' + implementation 'androidx.recyclerview:recyclerview:1.1.0-beta03' + implementation 'com.google.android.material:material:1.1.0-alpha09' + + implementation 'androidx.navigation:navigation-common-ktx:2.2.0-alpha01' + implementation 'androidx.navigation:navigation-fragment-ktx:2.2.0-alpha01' + implementation 'androidx.navigation:navigation-ui-ktx:2.2.0-alpha01' + implementation 'androidx.navigation:navigation-runtime-ktx:2.2.0-alpha01' + + implementation 'com.github.bumptech.glide:glide:4.9.0' + implementation 'com.github.bumptech.glide:okhttp3-integration:4.9.0' + kapt 'com.github.bumptech.glide:compiler:4.9.0' + + implementation 'com.github.kibotu:RecyclerViewPresenter:4.4.3' + implementation 'com.github.kibotu:StreamingAndroidLogger:4.3.8' + + implementation project(':library') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/otaliastudios/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/otaliastudios/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..249c3a2 --- /dev/null +++ b/app/src/androidTest/java/com/otaliastudios/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.otaliastudios + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.otaliastudios", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b3b1cef --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/otaliastudios/ColumnPresenter.kt b/app/src/main/java/com/otaliastudios/ColumnPresenter.kt new file mode 100644 index 0000000..b28f7e2 --- /dev/null +++ b/app/src/main/java/com/otaliastudios/ColumnPresenter.kt @@ -0,0 +1,31 @@ +package com.otaliastudios + +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions +import kotlinx.android.synthetic.main.item_icon_with_label.view.* +import net.kibotu.android.recyclerviewpresenter.Adapter +import net.kibotu.android.recyclerviewpresenter.Presenter +import net.kibotu.android.recyclerviewpresenter.PresenterModel + +data class Column(val image: String) + +class ColumnPresenter :Presenter (){ + + override val layout = R.layout.item_column + + override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder, item: PresenterModel, position: Int, payloads: MutableList?, adapter: Adapter) { + + with(viewHolder.itemView) { + Glide.with(this) + .asBitmap() + .load(item.model.image) + .transition(BitmapTransitionOptions.withCrossFade()) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(image) + .waitForLayout() + .clearOnDetach() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/otaliastudios/MainActivity.kt b/app/src/main/java/com/otaliastudios/MainActivity.kt new file mode 100644 index 0000000..3eec162 --- /dev/null +++ b/app/src/main/java/com/otaliastudios/MainActivity.kt @@ -0,0 +1,74 @@ +package com.otaliastudios + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.doOnTextChanged +import com.otaliastudios.bottomsheetcoordinatorlayout.BottomSheetCoordinatorBehavior +import kotlinx.android.synthetic.main.bottom_sheet_peek_search.* +import kotlinx.android.synthetic.main.bottom_sheet_search.* +import net.kibotu.android.recyclerviewpresenter.PresenterAdapter +import net.kibotu.android.recyclerviewpresenter.PresenterModel +import net.kibotu.logger.LogcatLogger +import net.kibotu.logger.Logger +import net.kibotu.logger.Logger.logv + + +class MainActivity : AppCompatActivity() { + + val filterAdapter = PresenterAdapter() + + val resultAdapter = PresenterAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + Logger.addLogger(LogcatLogger()) + + logv { "requestApplyInsets init" } + + search_bottom_sheet.setHideable(false) + search_bottom_sheet.state = BottomSheetCoordinatorBehavior.STATE_HALF_EXPANDED + search_bottom_sheet.setSkipCollapsed(false) + search_bottom_sheet.setFitContents(false) +// search_bottom_sheet.setExpandedOffset(resources.getDimension(R.dimen.peek_search_height).toInt()) + search_bottom_sheet.setPeekHeight(resources.getDimension(R.dimen.peek_search_height).toInt()) + + addFilterList() + addResultList() + + searchQuery.doOnTextChanged { text, start, count, after -> + addResults(text?.length ?: return@doOnTextChanged) + } + } + + private fun addFilterList() { + filterAdapter.registerPresenter(ColumnPresenter()) + filterList.adapter = filterAdapter + + addFilter(10) + } + + private fun addFilter(amount: Int) { + val items = (0 until amount).map { + PresenterModel(Column("https://lorempixel.com/200/3%02d/".format(it)), uuid = it.toString(), layout = R.layout.item_column) + } + + filterAdapter.submitList(items) + } + + private fun addResultList() { + resultAdapter.registerPresenter(RowPresenter()) + resultsList.adapter = resultAdapter + + addResults(100) + } + + private fun addResults(amount: Int) { + val items = (0 until amount).map { + PresenterModel(Row("https://lorempixel.com/2%02d/300/".format(it), "$it label"), uuid = it.toString(), layout = R.layout.item_icon_with_label) + } + + resultAdapter.submitList(items) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/otaliastudios/MapFragment.kt b/app/src/main/java/com/otaliastudios/MapFragment.kt new file mode 100644 index 0000000..d228293 --- /dev/null +++ b/app/src/main/java/com/otaliastudios/MapFragment.kt @@ -0,0 +1,24 @@ +package com.otaliastudios + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.bumptech.glide.Glide +import kotlinx.android.synthetic.main.fragment_map.* + +class MapFragment : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_map, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + Glide.with(this) + .load("https://lorempixel.com/1080/1920/") + .into(map) + .waitForLayout() + .clearOnDetach() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/otaliastudios/RowPresenter.kt b/app/src/main/java/com/otaliastudios/RowPresenter.kt new file mode 100644 index 0000000..9f38bde --- /dev/null +++ b/app/src/main/java/com/otaliastudios/RowPresenter.kt @@ -0,0 +1,35 @@ +package com.otaliastudios + +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions.withCrossFade +import kotlinx.android.synthetic.main.item_icon_with_label.view.* +import net.kibotu.android.recyclerviewpresenter.Adapter +import net.kibotu.android.recyclerviewpresenter.Presenter +import net.kibotu.android.recyclerviewpresenter.PresenterModel + +data class Row(val image: String, val label: String, val subTitle: String? = null) + +class RowPresenter : Presenter() { + + override val layout = R.layout.item_icon_with_label + + override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder, item: PresenterModel, position: Int, payloads: MutableList?, adapter: Adapter) { + + with(viewHolder.itemView) { + + title.text = item.model.label + subTitle.text = item.model.subTitle ?: "" + + Glide.with(this) + .asBitmap() + .load(item.model.image) + .transition(withCrossFade()) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(image) + .waitForLayout() + .clearOnDetach() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..e8b42a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/peek_background.xml b/app/src/main/res/drawable/peek_background.xml new file mode 100644 index 0000000..f017509 --- /dev/null +++ b/app/src/main/res/drawable/peek_background.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/peek_shadow.webp b/app/src/main/res/drawable/peek_shadow.webp new file mode 100644 index 0000000..75b320a Binary files /dev/null and b/app/src/main/res/drawable/peek_shadow.webp differ diff --git a/app/src/main/res/drawable/shape_search_input.xml b/app/src/main/res/drawable/shape_search_input.xml new file mode 100644 index 0000000..0979629 --- /dev/null +++ b/app/src/main/res/drawable/shape_search_input.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..cad95bf --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_peek_search.xml b/app/src/main/res/layout/bottom_sheet_peek_search.xml new file mode 100644 index 0000000..36c4644 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_peek_search.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_search.xml b/app/src/main/res/layout/bottom_sheet_search.xml new file mode 100644 index 0000000..1ca5840 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_search.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml new file mode 100644 index 0000000..3adda6c --- /dev/null +++ b/app/src/main/res/layout/fragment_map.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_column.xml b/app/src/main/res/layout/item_column.xml new file mode 100644 index 0000000..ab4086f --- /dev/null +++ b/app/src/main/res/layout/item_column.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_icon_with_label.xml b/app/src/main/res/layout/item_icon_with_label.xml new file mode 100644 index 0000000..66db1b4 --- /dev/null +++ b/app/src/main/res/layout/item_icon_with_label.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..898f3ed Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..dffca36 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..64ba76f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..dae5e08 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..e5ed465 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..14ed0af Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b0907ca Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..d8ae031 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..2c18de9 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..beed3cd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/navigation/navigation_controller.xml b/app/src/main/res/navigation/navigation_controller.xml new file mode 100644 index 0000000..a74412e --- /dev/null +++ b/app/src/main/res/navigation/navigation_controller.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..69b2233 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..01ce5cb --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,8 @@ + + + 108dp + 16dp + 12dp + 2dp + 6dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b2250a8 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Demo + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..fcefabb --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/test/java/com/otaliastudios/ExampleUnitTest.kt b/app/src/test/java/com/otaliastudios/ExampleUnitTest.kt new file mode 100644 index 0000000..e6fce13 --- /dev/null +++ b/app/src/test/java/com/otaliastudios/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.otaliastudios + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/build.gradle b/build.gradle index d80a780..a5ac2dc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,33 +1,38 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - buildscript { + ext.kotlin_version = '1.3.50' + repositories { - jcenter() + // https://storage-download.googleapis.com/maven-central/index.html + maven { url 'https://maven-central-eu.storage-download.googleapis.com/repos/central/data/' } google() + maven { url 'https://plugins.gradle.org/m2/' } + maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'com.android.tools.build:gradle:3.2.0-beta04' + classpath 'com.android.tools.build:gradle:3.5.0' // https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en // https://www.theguardian.com/technology/developer-blog/2016/dec/06/how-to-publish-an-android-library-a-mysterious-conversation classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { - jcenter() + maven { url 'https://maven-central-eu.storage-download.googleapis.com/repos/central/data/' } google() + maven { url 'https://plugins.gradle.org/m2/' } + maven { url 'https://jitpack.io' } } } ext { // Updating: check travis - compileSdkVersion = 28 - buildToolsVersion = "28.0.0" - supportLibVersion = '28.0.0-beta01' + compileSdkVersion = 29 + buildToolsVersion = "29.0.2" minSdkVersion = 14 - targetSdkVersion = 28 + targetSdkVersion = 29 } task clean(type: Delete) { diff --git a/gradle.properties b/gradle.properties index aac7c9b..0be8c26 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,32 @@ -# Project-wide Gradle settings. +# gralde config +org.gradle.jvmargs=-Xms8g +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.workers.max=32 +org.gradle.caching=true -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. +# kotlin config +#android.enableAapt2=false +kotlinOptions.allWarningsAsErrors=false +android.enableR8=true +android.enableR8.fullMode=true +android.enableD8=true +android.enableD8.desugaring=true +# https://developer.android.com/topic/libraries/support-library/androidx-overview +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html +#robolectric +android.enableUnitTestBinaryResources=true +# https://speakerdeck.com/snehpandya18/mastering-gradle-3 +gradle=build -x lint -x lintVitalRelease +# https://developer.android.com/studio/preview/features/?utm_source=android-studio#lazy_task_config +android.debug.obsoleteApi=false +#android.proguard.enableRulesExtraction=false -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +kapt.incremental.apt=true +android.enableSeparateAnnotationProcessing=true +javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372ae..5c2d1cf 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1808457..ef9a9e0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Apr 19 17:31:35 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/gradlew b/gradlew index 9d82f78..83f2acf 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -105,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -150,11 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +62,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +75,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/library/build.gradle b/library/build.gradle index 139d87b..23ac464 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.library' -apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'kotlin-android' apply plugin: 'com.jfrog.bintray' +apply plugin: 'com.github.dcendents.android-maven' // Required by bintray // archivesBaseName is required if artifactId is different from gradle module name @@ -28,10 +29,24 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + lintOptions { + abortOnError false + } } dependencies { - implementation "com.android.support:design:$supportLibVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'com.google.android.material:material:1.1.0-alpha09' testImplementation 'junit:junit:4.12' } @@ -99,35 +114,33 @@ bintray { } } -// From official sample https://github.com/bintray/bintray-examples/blob/master/gradle-aar-example/build.gradle task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs classifier = 'sources' - from android.sourceSets.main.java.sourceFiles } task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs + failOnError false + source = android.sourceSets.main.java.sourceFiles classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - classpath += project.files("${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar") - project.android.libraryVariants.all { variant -> - if (variant.name == 'release') { - classpath += files(variant.javaCompile.classpath) - } - } + classpath += configurations.compile + + exclude '**/BuildConfig.java' exclude '**/R.java' exclude '**/internal/**' } +// build a jar with javadoc task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' from javadoc.destinationDir } artifacts { - archives javadocJar archives sourcesJar + archives javadocJar } // export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home -// ./gradlew bintrayUpload +// ./gradlew bintrayUpload \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorBehavior.java b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorBehavior.java index 77f13ad..87beceb 100644 --- a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorBehavior.java +++ b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorBehavior.java @@ -1,21 +1,21 @@ package com.otaliastudios.bottomsheetcoordinatorlayout; import android.content.Context; -import android.support.design.widget.CoordinatorLayout; -import android.support.v4.view.MotionEventCompat; import android.util.AttributeSet; import android.view.MotionEvent; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + /** - * This {@code Behavior} allows {@link android.support.design.widget.AppBarLayout} to accept drag events. - * - * When {@code AppBarLayout} stands inside a {@link android.support.design.widget.BottomSheetBehavior}, + * This {@code Behavior} allows {@link com.google.android.material.appbar.AppBarLayout} to accept drag events. + *

+ * When {@code AppBarLayout} stands inside a {@link com.google.android.material.bottomsheet.BottomSheetBehavior}, * all touch events are stolen by the behavior, unless they focus on the nested scrolling child * (which the app bar isn't). (see BottomSheetBehavior.onInterceptTouchEvent, last line). - * - * Since no event goes to app bar, its own {@link android.support.design.widget.AppBarLayout.Behavior} + *

+ * Since no event goes to app bar, its own {@link com.google.android.material.appbar.AppBarLayout.Behavior} * cannot control drags on the view itself. We have to take care of this. - * + *

* A simple implementation for this touch policy could be just returning false when sheet is expanded, * so that {@code AppBarLayout} can catch the event. * Unfortunately, this is not enough, because if sheet is expanded **and** app bar is expanded @@ -35,7 +35,7 @@ public BottomSheetCoordinatorBehavior(Context context, AttributeSet attrs) { public static BottomSheetCoordinatorBehavior from(BottomSheetCoordinatorLayout view) { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) view.getLayoutParams(); - return (BottomSheetCoordinatorBehavior) params.getBehavior(); + return (BottomSheetCoordinatorBehavior) view.getBehavior(); } @Override @@ -47,7 +47,7 @@ public boolean onTouchEvent(CoordinatorLayout parent, BottomSheetCoordinatorLayo return super.onTouchEvent(parent, sheet, event); } updateDirection(event); - if (sheet.getState() == BottomSheetCoordinatorBehavior.STATE_EXPANDED && !sheet.canScrollUp() && !fingerDown) { + if ((sheet.getState() == BottomSheetCoordinatorBehavior.STATE_HALF_EXPANDED || sheet.getState() == BottomSheetCoordinatorBehavior.STATE_EXPANDED) && !sheet.canScrollUp() && !fingerDown) { // Release this. Doesn't work well because BottomSheetBehavior keeps being STATE_DRAGGING // even when we reached full height, as long as we keep the finger there. return false; @@ -65,7 +65,7 @@ public boolean onInterceptTouchEvent(CoordinatorLayout parent, BottomSheetCoordi } updateDirection(event); - if (sheet.getState() == BottomSheetCoordinatorBehavior.STATE_EXPANDED) { + if (sheet.getState() == BottomSheetCoordinatorBehavior.STATE_EXPANDED || sheet.getState() == BottomSheetCoordinatorBehavior.STATE_HALF_EXPANDED) { // If finger is going down and if (!sheet.canScrollUp()) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { diff --git a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorLayout.java b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorLayout.java index 8a05a97..977ef8b 100644 --- a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorLayout.java +++ b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetCoordinatorLayout.java @@ -1,60 +1,86 @@ package com.otaliastudios.bottomsheetcoordinatorlayout; import android.content.Context; -import android.os.Build; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.AppBarLayout; -import android.support.design.widget.BottomSheetBehavior;import android.support.design.widget.CoordinatorLayout; -import android.support.v4.view.ViewCompat; -import android.support.v4.widget.NestedScrollView; -import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.ScrollView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; /** * A hacky {@link CoordinatorLayout} that can act as a bottom sheet through * {@link BottomSheetBehavior}. - * + *

* This works by *not* reinventing the wheel and reusing the same nested scrolling logic implemented * by behaviors. There is a dummy view inside the sheet that is capable of getting nested scrolling * callbacks, and forward them to the *outer* behavior that they normally would never reach. - * + *

* Default behavior is {@link BottomSheetCoordinatorBehavior}, Which includes some workarounds * for window insets and app bar layout dragging. */ -@CoordinatorLayout.DefaultBehavior(BottomSheetCoordinatorBehavior.class) -public class BottomSheetCoordinatorLayout extends CoordinatorLayout implements - AppBarLayout.OnOffsetChangedListener { - - private static final String TAG = BottomSheetCoordinatorLayout.class.getSimpleName(); +public class BottomSheetCoordinatorLayout extends CoordinatorLayout implements AppBarLayout.OnOffsetChangedListener, CoordinatorLayout.AttachedBehavior { private BottomSheetCoordinatorBehavior bottomSheetBehavior; + + private BottomSheetBehavior.BottomSheetCallback appBarBottomSheetCallback; + private BottomSheetBehavior.BottomSheetCallback delayedBottomSheetCallback; + + private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() { + + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + + if (appBarBottomSheetCallback != null) + appBarBottomSheetCallback.onStateChanged(bottomSheet, newState); + + if (delayedBottomSheetCallback != null) + delayedBottomSheetCallback.onStateChanged(bottomSheet, newState); + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + + if (appBarBottomSheetCallback != null) + appBarBottomSheetCallback.onSlide(bottomSheet, slideOffset); + + if (delayedBottomSheetCallback != null) + delayedBottomSheetCallback.onSlide(bottomSheet, slideOffset); + } + }; + private Boolean delayedHideable; private Boolean delayedSkipCollapsed; private Integer delayedState; + private Integer delayedPeekHeight; + private Boolean delayedFitToContents; + private Integer delayedExpandedOffset; private AppBarLayout.Behavior appBarBehavior; private int appBarOffset = 0; private boolean hasAppBar = false; public BottomSheetCoordinatorLayout(Context context) { - super(context); i(); + super(context); + init(); } public BottomSheetCoordinatorLayout(Context context, AttributeSet attrs) { - super(context, attrs); i(); + super(context, attrs); + init(); } public BottomSheetCoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); i(); + super(context, attrs, defStyleAttr); + init(); } - private void i() { + private void init() { // Add a dummy view that will receive inner touch events. View dummyView = new View(getContext()); DummyBehavior dummyBehavior = new DummyBehavior(); @@ -63,8 +89,7 @@ private void i() { ViewCompat.setElevation(dummyView, ViewCompat.getElevation(this)); // Make sure it does not fit windows, or it will consume insets before the AppBarLayout. dummyView.setFitsSystemWindows(false); - LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); + LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); params.setBehavior(dummyBehavior); addView(dummyView, params); } @@ -75,7 +100,7 @@ private void i() { * We might be tempted to use onAttachedToWindow but that only works if the behavior was * declared through XML. * - * @param widthMeasureSpec width spec + * @param widthMeasureSpec width spec * @param heightMeasureSpec height spec */ @Override @@ -84,11 +109,10 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (bottomSheetBehavior != null) return; // Fetch our own behavior. - bottomSheetBehavior = BottomSheetCoordinatorBehavior.from(BottomSheetCoordinatorLayout.this); - if (delayedBottomSheetCallback != null) { - bottomSheetBehavior.setBottomSheetCallback(delayedBottomSheetCallback); - delayedBottomSheetCallback = null; - } + bottomSheetBehavior = (BottomSheetCoordinatorBehavior) ((LayoutParams) getLayoutParams()).getBehavior(); + + bottomSheetBehavior.setBottomSheetCallback(bottomSheetCallback); + if (delayedSkipCollapsed != null) { bottomSheetBehavior.setSkipCollapsed(delayedSkipCollapsed); delayedSkipCollapsed = null; @@ -97,6 +121,18 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { bottomSheetBehavior.setHideable(delayedHideable); delayedHideable = null; } + if (delayedPeekHeight != null) { + bottomSheetBehavior.setPeekHeight(delayedPeekHeight); + delayedPeekHeight = null; + } + if (delayedFitToContents != null) { + bottomSheetBehavior.setFitToContents(delayedFitToContents); + delayedFitToContents = null; + } + if (delayedExpandedOffset != null) { + bottomSheetBehavior.setExpandedOffset(delayedExpandedOffset); + delayedExpandedOffset = null; + } if (delayedState != null) { // This must be the last. bottomSheetBehavior.setState(delayedState); delayedState = null; @@ -114,13 +150,46 @@ public boolean canDrag(@NonNull AppBarLayout appBarLayout) { } }); hasAppBar = true; + + + // todo fix me D: + // somehow during expanding from half to expanded state, the appbar has the wrong parallax offset, + // we hammer the expanded state atm, but it's not particular good for performance + appBarBottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + + switch (newState) { + case BottomSheetCoordinatorBehavior.STATE_EXPANDED: + case BottomSheetCoordinatorBehavior.STATE_HALF_EXPANDED: + appBarLayout.setExpanded(true, true); + break; + case BottomSheetCoordinatorBehavior.STATE_COLLAPSED: + case BottomSheetCoordinatorBehavior.STATE_DRAGGING: + case BottomSheetCoordinatorBehavior.STATE_SETTLING: + appBarLayout.setExpanded(true, false); + break; + case BottomSheetBehavior.STATE_HIDDEN: + break; + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + DebugExtensions.log(this, "onSlide slideOffset=" + slideOffset); + + } + }; + + setBottomSheetCallback(delayedBottomSheetCallback); + } else { hasAppBar = false; } } /** - * Set a {@link android.support.design.widget.BottomSheetBehavior.BottomSheetCallback} callback + * Set a {@link com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback} callback * to our behavior, as soon as it is available. * * @param bottomSheetCallback desired callback. @@ -128,8 +197,6 @@ public boolean canDrag(@NonNull AppBarLayout appBarLayout) { public void setBottomSheetCallback(final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback) { if (bottomSheetBehavior == null) { delayedBottomSheetCallback = bottomSheetCallback; - } else { - bottomSheetBehavior.setBottomSheetCallback(bottomSheetCallback); } } @@ -172,17 +239,60 @@ public void setState(int state) { } } + /** + * Set's peek height. + * + * @param peekHeight Peek Height + */ + public void setPeekHeight(int peekHeight) { + if (delayedPeekHeight == null) { + delayedPeekHeight = peekHeight; + } else { + bottomSheetBehavior.setState(peekHeight); + } + } + + /** + * Sets whether the height of the expanded sheet is determined by the height of its contents, + * or if it is expanded in two stages (half the height of the parent container, + * full height of parent container). Default value is true + * + * @param fitToContents – whether or not to fit the expanded sheet to its contents. + */ + public void setFitContents(boolean fitToContents) { + if (delayedFitToContents == null) { + delayedFitToContents = fitToContents; + } else { + bottomSheetBehavior.setFitToContents(fitToContents); + } + } + + /** + * Determines the top offset of the BottomSheet in the STATE_EXPANDED state when fitsToContent is false. The default value is 0, which results in the sheet matching the parent's top. + * + * @param offset – an integer value greater than equal to 0, representing the STATE_EXPANDED offset. Value must not exceed the offset in the half expanded state. + */ + public void setExpandedOffset(int offset) { + if (delayedExpandedOffset == null) { + delayedExpandedOffset = offset; + } else { + bottomSheetBehavior.setExpandedOffset(offset); + } + } + /** * Returns our behavior, if available. + * * @return our behavior, or null if not available yet. */ @Nullable public BottomSheetBehavior getBehavior() { - return bottomSheetBehavior; + return (BottomSheetCoordinatorBehavior) ((LayoutParams) getLayoutParams()).getBehavior(); } /** * Returns an {@link AppBarLayout} if present. + * * @return the first available AppBarLayout, or null. */ @Nullable @@ -208,15 +318,24 @@ public int getState() { @Override public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { + + DebugExtensions.log(this, "onOffsetChanged=" + verticalOffset); + if (verticalOffset == appBarOffset) { // Do nothing + + DebugExtensions.log(this, "onOffsetChanged=" + verticalOffset + " -> do nothing"); + } else if (bottomSheetBehavior.getState() != BottomSheetCoordinatorBehavior.STATE_EXPANDED) { // We are trying to set a new offset, but it shouldn't change because the sheet // is not expanded. Let's get back to old offset. appBarBehavior.setTopAndBottomOffset(appBarOffset); + + DebugExtensions.log(this, "onOffsetChanged=" + verticalOffset + " -> appBarOffset=" + appBarOffset); } else { // we are trying to set a new offset, and sheet is expanded. Keep track of it. appBarOffset = verticalOffset; + DebugExtensions.log(this, "onOffsetChanged=" + verticalOffset + " = appBarOffset=" + appBarOffset); } } @@ -241,12 +360,12 @@ boolean hasAppBar() { * Through this behavior the dummy view can listen to touch/scroll events. * Our goal is to propagate them to the sheet behavior (the BottomSheetBehavior * that controls the bottom sheet position), - * + *

* It has to be done manually because nested scrolls/fling events are coordinated by coordinator * layouts. If the bottom sheet view does not support nested scrolling (and CoordinatorLayout doesn't) * any event that takes place inside the sheet is confined to sheet itself. * This way inner events never reach the BottomSheetBehavior. - * + *

* Another option would be to make a CoordinatorLayout class that actually implements * NestedScrollingChild and thus propagates events to the outer CoordinatorLayout: this, then, * could propagate the events to BottomSheetBehavior. I was not able to make that work. @@ -255,7 +374,8 @@ boolean hasAppBar() { */ private static class DummyBehavior extends CoordinatorLayout.Behavior { - public DummyBehavior() {} + public DummyBehavior() { + } public DummyBehavior(Context context, AttributeSet attrs) { super(context, attrs); @@ -268,6 +388,7 @@ private boolean shouldForwardEvent(BottomSheetCoordinatorLayout sheet, boolean f // If sheet is expanded, we only want to forward if the appBar is expanded. // AND the touch is going down... return !sheet.canScrollUp() && !fingerGoingUp; + case BottomSheetCoordinatorBehavior.STATE_HALF_EXPANDED: case BottomSheetCoordinatorBehavior.STATE_COLLAPSED: case BottomSheetCoordinatorBehavior.STATE_DRAGGING: case BottomSheetCoordinatorBehavior.STATE_SETTLING: @@ -278,40 +399,43 @@ private boolean shouldForwardEvent(BottomSheetCoordinatorLayout sheet, boolean f } @Override - public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, DummyView child, View directTargetChild, View target, int nestedScrollAxes) { + public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull DummyView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { BottomSheetCoordinatorLayout sheet = (BottomSheetCoordinatorLayout) coordinatorLayout; if (shouldForwardEvent(sheet, false)) { - return sheet.getBehavior().onStartNestedScroll(coordinatorLayout, sheet, directTargetChild, target, nestedScrollAxes); + return sheet.getBehavior().onStartNestedScroll(coordinatorLayout, sheet, directTargetChild, target, axes, type); } else { - return false; + return false; // super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type); } } @Override - public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, DummyView child, View target, int dx, int dy, int[] consumed) { + public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull DummyView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { + // super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); + // When moving the finger up, dy is > 0. BottomSheetCoordinatorLayout sheet = (BottomSheetCoordinatorLayout) coordinatorLayout; if (shouldForwardEvent(sheet, dy > 0)) { - sheet.getBehavior().onNestedPreScroll(coordinatorLayout, - sheet, target, dx, dy, consumed); + sheet.getBehavior().onNestedPreScroll(coordinatorLayout, sheet, target, dx, dy, consumed, type); } } @Override - public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, DummyView child, View target) { + public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull DummyView child, @NonNull View target, int type) { + // super.onStopNestedScroll(coordinatorLayout, child, target, type); + BottomSheetCoordinatorLayout sheet = (BottomSheetCoordinatorLayout) coordinatorLayout; if (shouldForwardEvent(sheet, false)) { - sheet.getBehavior().onStopNestedScroll(coordinatorLayout, sheet, target); + sheet.getBehavior().onStopNestedScroll(coordinatorLayout, sheet, target, type); } } @Override - public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, DummyView child, View target, float velocityX, float velocityY) { + public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull DummyView child, @NonNull View target, float velocityX, float velocityY) { BottomSheetCoordinatorLayout sheet = (BottomSheetCoordinatorLayout) coordinatorLayout; if (shouldForwardEvent(sheet, velocityY > 0)) { return sheet.getBehavior().onNestedPreFling(coordinatorLayout, sheet, target, velocityX, velocityY); } else { - return false; + return false; // super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); } } } diff --git a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetInsetsBehavior.java b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetInsetsBehavior.java index b5ce376..4a0b1a3 100644 --- a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetInsetsBehavior.java +++ b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/BottomSheetInsetsBehavior.java @@ -3,17 +3,19 @@ import android.content.Context; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.design.widget.BottomSheetBehavior; -import android.support.design.widget.CoordinatorLayout; -import android.support.v4.os.ParcelableCompat; -import android.support.v4.os.ParcelableCompatCreatorCallbacks; -import android.support.v4.view.AbsSavedState; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.WindowInsetsCompat; import android.util.AttributeSet; import android.view.View; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.os.ParcelableCompat; +import androidx.core.os.ParcelableCompatCreatorCallbacks; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.customview.view.AbsSavedState; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; + import java.lang.reflect.Field; /** @@ -25,9 +27,9 @@ */ // link to bug https://code.google.com/p/android/issues/detail?id=207191&thanks=207191&ts=1460894786 public class BottomSheetInsetsBehavior extends BottomSheetBehavior { - private final static String TAG = BottomSheetInsetsBehavior.class.getSimpleName(); - public BottomSheetInsetsBehavior() {} + public BottomSheetInsetsBehavior() { + } public BottomSheetInsetsBehavior(Context context, AttributeSet attrs) { super(context, attrs); @@ -35,9 +37,10 @@ public BottomSheetInsetsBehavior(Context context, AttributeSet attrs) { @NonNull @Override - public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets) { + public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull WindowInsetsCompat insets) { // Steal the inset and dispatch to view. ViewCompat.dispatchApplyWindowInsets(child, insets); + DebugExtensions.log(this, "onApplyWindowInsets " + insets); // Pass unconsumed insets. return super.onApplyWindowInsets(coordinatorLayout, child, insets); } diff --git a/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/DebugExtensions.kt b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/DebugExtensions.kt new file mode 100644 index 0000000..f78e08d --- /dev/null +++ b/library/src/main/java/com/otaliastudios/bottomsheetcoordinatorlayout/DebugExtensions.kt @@ -0,0 +1,26 @@ +/** + * Created by [Jan Rabe](https://about.me/janrabe). + */ + +@file:JvmName("DebugExtensions") + +package com.otaliastudios.bottomsheetcoordinatorlayout + +import android.util.Log + +internal val debug = BuildConfig.DEBUG + +internal fun Any.log(message: String?) { + if (debug) + Log.d(this::class.java.simpleName, "$message") +} + +internal fun Any.loge(message: String?) { + if (debug) + Log.e(this::class.java.simpleName, "$message") +} + +internal fun Throwable.log() { + if (debug) + Log.d(this::class.java.simpleName, "$message") +} diff --git a/settings.gradle b/settings.gradle index d8f14a1..6a834e4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':library' +include ':library', ':app'