Repository: thefuntasty/hauler Branch: master Commit: 2c7cf8eaeb34 Files: 68 Total size: 69.6 KB Directory structure: gitextract_1jcvn5xr/ ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── publish_release.yml │ ├── publish_snapshot.yml │ └── pull_request.yml ├── .gitignore ├── Dangerfile ├── Gemfile ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── Deps.kt │ ├── ProjectSettings.kt │ ├── Versions.kt │ └── app/ │ └── futured/ │ └── hauler/ │ └── DependencyUpdates.kt ├── core/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── app/ │ │ └── futured/ │ │ └── hauler/ │ │ ├── ColorUtils.kt │ │ ├── DragDirection.kt │ │ ├── HaulerView.kt │ │ ├── HaulerViewExtensions.kt │ │ ├── LockableNestedScrollView.kt │ │ ├── OnDragActivityListener.kt │ │ ├── OnDragDismissedListener.kt │ │ └── SystemBarsFader.kt │ └── res/ │ └── values/ │ ├── attrs_hauler_view.xml │ └── dimens.xml ├── databinding/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── kotlin/ │ └── app/ │ └── futured/ │ └── hauler/ │ └── databinding/ │ ├── IsDragEnabledAdapter.kt │ ├── IsDragUpEnabledAdapter.kt │ ├── IsScrollableAdapter.kt │ └── OnDragDismissedListenerAdapter.kt ├── detekt.yml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── sample/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── app/ │ │ └── futured/ │ │ └── haulersample/ │ │ ├── MainActivity.kt │ │ └── draggable/ │ │ ├── SimpleActivity.kt │ │ ├── SimpleJavaActivity.java │ │ ├── advanced/ │ │ │ ├── AdvancedActivity.kt │ │ │ └── IgnoredAreaView.kt │ │ └── databinding/ │ │ ├── DatabindingActivity.kt │ │ ├── DatabindingActivityState.kt │ │ └── DatabindingActivityView.kt │ └── res/ │ ├── anim/ │ │ ├── anim_slide_down.xml │ │ └── anim_slide_up.xml │ ├── drawable/ │ │ └── ic_launcher_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_advanced.xml │ │ ├── activity_databinding.xml │ │ ├── activity_main.xml │ │ └── activity_simple.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*.{kt,kts}] max_line_length=130 ================================================ FILE: .github/workflows/publish_release.yml ================================================ name: Publish Release on: release: types: [published] jobs: release: name: Release Publish runs-on: [ubuntu-latest] steps: - uses: actions/checkout@v1 - name: Set up JDK 11 uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: '11' - name: Build & run unit tests shell: bash run: ./gradlew --continue build testRelease - name: Publish release run: ./gradlew publish --no-daemon --no-parallel --stacktrace -PVERSION_NAME=${{github.event.release.name}} env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_REPOSITORY_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_REPOSITORY_PASSWORD }} ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.GPG_SIGNING_PRIVATE_KEY }} ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} ================================================ FILE: .github/workflows/publish_snapshot.yml ================================================ name: Publish Snapshot on: push: branches: master jobs: master: name: Snapshot Publish runs-on: [ ubuntu-latest ] env: SLACK_CHANNEL: android steps: - uses: actions/checkout@v1 - name: Set up JDK 11 uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: '11' - name: Run unit tests shell: bash run: ./gradlew --continue build testRelease - name: Build & publish snapshot run: ./gradlew publish --no-daemon --no-parallel --stacktrace env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_REPOSITORY_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_REPOSITORY_PASSWORD }} ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.GPG_SIGNING_PRIVATE_KEY }} ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} - name: Slack Notification if: failure() uses: homoluctus/slatify@master with: type: "failure" job_name: '*Snapshot Publish*' username: GitHub channel: ${{env.SLACK_CHANNEL}} url: ${{ secrets.SLACK_WEB_HOOK }} commit: true token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/pull_request.yml ================================================ name: Check PR on: [pull_request] jobs: pr: name: PR check runs-on: [ubuntu-latest] env: DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} steps: - uses: actions/checkout@v1 - uses: actions/setup-ruby@v1 with: ruby-version: '2.6' - name: Set up JDK 11 uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: '11' - name: Run LintCheck shell: bash run: ./gradlew detekt ktlintCheck lint assembleRelease - name: Run unit tests shell: bash run: ./gradlew --continue testRelease - name: Danger action uses: MeilCli/danger-action@v2 continue-on-error: true with: plugins_file: 'Gemfile' danger_file: 'Dangerfile' danger_id: 'danger-pr' ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/* /buildSrc/build/* .DS_Store /build /captures .externalNativeBuild ================================================ FILE: Dangerfile ================================================ is_pr_big = git.insertions > 500 has_correct_prefix = github.branch_for_head.match(/^(feature|hotfix|fix|release|housekeep)\//) warn("Branch name should have `release/`, `hotfix/`, `fix/`, `housekeep/` or `feature/` prefix.") if !has_correct_prefix warn("This pull request is too big.") if is_pr_big commit_lint.check warn: :all, disable: [:subject_length] # Utils def report_checkstyle_for_directory(directory_name) if Dir.exists?(directory_name) Dir.glob("#{directory_name}/*.xml").each {|f| report_checkstyle(f) } end end def report_checkstyle(file_name) if File.file?(file_name) checkstyle_format.report file_name end end # Setup checkstyle checkstyle_format.base_path = Dir.pwd # Detekt checkstyle report_checkstyle 'build/reports/detekt/detekt.xml' # Ktlint checkstyle report_checkstyle_for_directory 'library/build/reports/ktlint' report_checkstyle_for_directory 'sample/build/reports/ktlint' ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gem 'danger-commit_lint' gem 'danger-checkstyle_format' ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Futured apps s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Hauler [![Maven Central](https://img.shields.io/maven-central/v/app.futured.hauler/hauler)](https://search.maven.org/artifact/app.futured.hauler/hauler/) ![minSdk:21](https://img.shields.io/badge/minSDK-21-brightgreen.svg) [![Android Arsenal]( https://img.shields.io/badge/Android%20Arsenal-Hauler-brightgreen.svg?style=flat )]( https://android-arsenal.com/details/1/7359 ) ![Publish Snapshot](https://github.com/futuredapp/hauler/actions/workflows/publish_snapshot.yml/badge.svg) ![License](https://img.shields.io/github/license/futuredapp/hauler?color=black) Hauler is an Android library containing custom layout which enables to easily create swipe to dismiss `Activity`. Implementation is based on code from project [Plaid](https://github.com/nickbutcher/plaid). ![Alt text](https://github.com/thefuntasty/hauler/blob/master/images/example.gif) # Installation ```groovy dependencies { implementation("app.futured.hauler:hauler:latestVersion") // optional dependency with set of Data Binding adapters implementation("app.futured.hauler:databinding:latestVersion") } ``` ### Snapshot installation Add new maven repo to your top level gradle file. ```groovy maven { url "https://oss.sonatype.org/content/repositories/snapshots" } ``` Snapshots are grouped based on major version, so for version 5.x.x use: ```groovy implementation "app.futured.hauler:hauler:5.X.X-SNAPSHOT" ``` # Features Hauler library comes with highly customizable `HaulerView` which provides swipe to dismiss functionality. It also ships with `databinding` module which contains Binding Adapters for smoother experience with Android Data Binding implementation. # Usage Activity which is meant to be dismissed must contain `HaulerView` as a root view and `NestedScrollView` (or other `View` what supports nested scroll) as its child. Make sure your `NestedScrollview`'s attribute `android:fillViewport` is set to `true` otherwise it might not behave as expected: ```xml ``` Secondly, define translucent floating Theme and assign it to the Activity you want to give dismiss ability: ```xml ``` ```xml ``` Set `onDragDismissListener` to react properly to user dismiss request. Example implementation might look like this: ```kotlin override fun onCreate(savedInstanceState: Bundle?) { // ... haulerView.setOnDragDismissedListener { finish() // finish activity when dismissed } } ``` ## Customization There are few styleable attributes you might want to use to customize your `HaulerView`: ```xml ``` | Attribute name | Type | Default value | Description| | -------------- | ---- | ------------- | ---------- | | `app:dragDismissDistance` | dimen | 100dp | Distance which should be `View` swiped to consider Activity as dismissed | | `app:dragDismissFraction` | float | unspecified | `<0;1>` - Fraction of `View`'s height we should reach swiping to consider Activity as dismissed | | `app:dragElasticity` | float | 0.8 | `<0;1>` - Toughness of swipe. Higher value indicates more rigid feeling | | `app:dragDismissScale` | float | 0.95 | `<0;1>` - Scale factor of `View` while performing swipe action | | `app:dragUpEnabled` | boolean | false | Flag indicating if drag up dismiss gesture is enabled | | `app:fadeSystemBars` | boolean | true | Flag indicating if system bars (status & navigation) fades while dismiss is in progress | Attributes `dragDismissDistance` and `dragDismissFraction` are exclusive. Do not use them together. # License Hauler is available under the MIT license. See the [LICENSE file](LICENCE) for more information. ================================================ FILE: build.gradle.kts ================================================ import org.jlleitschuh.gradle.ktlint.reporter.ReporterType buildscript { repositories { google() jcenter() } dependencies { classpath(Deps.gradlePlugin) classpath(kotlin(Deps.Kotlin.gradlePlugin, Versions.kotlin)) classpath(Deps.Plugins.mavenPublish) classpath(Deps.Plugins.dokka) } } plugins { idea id(Deps.Plugins.detekt) version Versions.detekt id(Deps.Plugins.ktlint) version Versions.ktlint } allprojects { repositories { google() jcenter() mavenCentral() } } tasks { register("dependencyUpdates") } subprojects { apply(plugin = Deps.Plugins.ktlint) ktlint { version.set(Versions.ktlintExtension) ignoreFailures.set(true) android.set(true) outputToConsole.set(true) reporters { reporter(ReporterType.PLAIN) reporter(ReporterType.CHECKSTYLE) } } plugins.whenPluginAdded { if (this is SigningPlugin) { extensions.findByType()?.apply { val hasKey = project.hasProperty("SIGNING_PRIVATE_KEY") val hasPassword = project.hasProperty("SIGNING_PASSWORD") if (hasKey && hasPassword) { useInMemoryPgpKeys( project.properties["SIGNING_PRIVATE_KEY"].toString(), project.properties["SIGNING_PASSWORD"].toString() ) } } } } } detekt { autoCorrect = false version = Versions.detekt input = files("sample/src/main/kotlin", "library/src/main/kotlin") config = files("detekt.yml") } ================================================ FILE: buildSrc/build.gradle.kts ================================================ plugins { `kotlin-dsl` } repositories { jcenter() } dependencies { implementation("com.github.ben-manes:gradle-versions-plugin:0.36.0") } dependencies { implementation("com.github.ben-manes:gradle-versions-plugin:0.33.0") } ================================================ FILE: buildSrc/src/main/kotlin/Deps.kt ================================================ object Deps { const val gradlePlugin = "com.android.tools.build:gradle:${Versions.gradle}" object Plugins { const val detekt = "io.gitlab.arturbosch.detekt" const val ktlint = "org.jlleitschuh.gradle.ktlint" const val mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:${Versions.mavenPublish}" const val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokka}" } object Kotlin { const val gradlePlugin = "gradle-plugin" const val stdlib = "stdlib-jdk7" } object AndroidX { const val appcompat = "androidx.appcompat:appcompat:${Versions.androidx}" const val palette = "androidx.palette:palette:${Versions.palette}" const val ktx = "androidx.core:core-ktx:${Versions.androidxKtx}" } } ================================================ FILE: buildSrc/src/main/kotlin/ProjectSettings.kt ================================================ object ProjectSettings { const val applicationId = "app.futured.hauler" const val targetSdk = 32 const val minSdk = 21 } ================================================ FILE: buildSrc/src/main/kotlin/Versions.kt ================================================ object Versions { // gradle const val gradle = "7.2.1" // plugins const val detekt = "1.20.0" const val ktlint = "10.3.0" const val ktlintExtension = "0.41.0" const val mavenPublish = "0.21.0" const val dokka = "1.7.0" // kotlin const val kotlin = "1.7.0" const val androidx = "1.4.2" const val androidxKtx = "1.8.0" const val palette = "1.0.0" } ================================================ FILE: buildSrc/src/main/kotlin/app/futured/hauler/DependencyUpdates.kt ================================================ package app.futured.hauler import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask abstract class DependencyUpdates : DependencyUpdatesTask() { init { group = "futured" this.resolutionStrategy { componentSelection { all { val rejected = listOf("alpha", "beta", "rc", "cr", "m", "preview", "testing") .map { qualifier -> Regex("(?i).*[.-]$qualifier[.\\d-]*") } .any { it.matches(candidate.version) } if (rejected) { reject("Release candidate") } } } } } } ================================================ FILE: core/.gitignore ================================================ /build ================================================ FILE: core/build.gradle.kts ================================================ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.library") id("kotlin-android") id("com.vanniktech.maven.publish") } android { compileSdkVersion(ProjectSettings.targetSdk) defaultConfig { minSdkVersion(ProjectSettings.minSdk) targetSdkVersion(ProjectSettings.targetSdk) } sourceSets { getByName("main").java.srcDir("src/main/kotlin") } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } } dependencies { implementation(kotlin(Deps.Kotlin.stdlib, KotlinCompilerVersion.VERSION)) implementation(Deps.AndroidX.appcompat) implementation(Deps.AndroidX.palette) implementation(Deps.AndroidX.ktx) } ================================================ FILE: core/gradle.properties ================================================ POM_NAME=Library containing custom layout which enables to easily create swipe to dismiss Activity POM_ARTIFACT_ID=hauler POM_PACKAGING=aar ================================================ FILE: core/src/main/AndroidManifest.xml ================================================ ================================================ FILE: core/src/main/kotlin/app/futured/hauler/ColorUtils.kt ================================================ package app.futured.hauler import androidx.annotation.CheckResult import androidx.annotation.ColorInt import androidx.annotation.IntRange internal object ColorUtils { @CheckResult @ColorInt fun modifyAlpha(@ColorInt color: Int, @IntRange(from = 0, to = 255) alpha: Int): Int = color and 0x00ffffff or (alpha shl 24) } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/DragDirection.kt ================================================ package app.futured.hauler enum class DragDirection { UP, DOWN } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/HaulerView.kt ================================================ package app.futured.hauler import android.app.Activity import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.widget.FrameLayout import androidx.core.content.withStyledAttributes import androidx.core.view.animation.PathInterpolatorCompat class HaulerView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { // configurable attributes private var dragDismissDistance = context.resources.getDimensionPixelSize(R.dimen.default_drag_dismiss_distance).toFloat() private var dragDismissFraction = -1f private var dragDismissScale = 0.95f private var shouldScale = true private var dragElasticity = 0.8f // state private var totalDrag: Float = 0.toFloat() private var draggingDown = false private var draggingUp = false private var mLastActionEvent: Int = 0 private var onDragDismissedListener: OnDragDismissedListener? = null private var onDragActivityListener: OnDragActivityListener? = null private var systemBarsFader: SystemBarsFader? = null private var isDragEnabled = true private var dragUpEnabled = false private var fadeSystemBars = true init { getContext().withStyledAttributes(set = attrs, attrs = R.styleable.HaulerView) { val distanceAvailable = hasValue(R.styleable.HaulerView_dragDismissDistance) val dismissFractionAvailable = hasValue(R.styleable.HaulerView_dragDismissFraction) if (distanceAvailable && dismissFractionAvailable) { throw IllegalStateException("Do not specify both dragDismissDistance and dragDismissFraction. Choose one.") } else if (distanceAvailable) { dragDismissDistance = getDimensionPixelSize(R.styleable.HaulerView_dragDismissDistance, 0).toFloat() } else if (dismissFractionAvailable) { dragDismissFraction = getFloat(R.styleable.HaulerView_dragDismissFraction, dragDismissFraction) } dragDismissScale = getFloat(R.styleable.HaulerView_dragDismissScale, dragDismissScale) dragUpEnabled = getBoolean(R.styleable.HaulerView_dragUpEnabled, dragUpEnabled) dragElasticity = getFloat(R.styleable.HaulerView_dragElasticity, dragElasticity) fadeSystemBars = getBoolean(R.styleable.HaulerView_fadeSystemBars, fadeSystemBars) } setFadeSystemBars(fadeSystemBars) shouldScale = dragDismissScale != 1f } override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean = nestedScrollAxes and View.SCROLL_AXIS_VERTICAL != 0 override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) { if (isDragEnabled.not()) { return super.onNestedPreScroll(target, dx, dy, consumed) } // if we're in a drag gesture and the user reverses up the we should take those events val draggingDownInProgress = draggingDown && dy > 0 val draggingUpInProgress = draggingUp && dy < 0 if (draggingDownInProgress || draggingUpInProgress) { dragScale(dy) consumed[1] = dy } } override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) { if (isDragEnabled.not() || (dragUpEnabled.not() && dyUnconsumed > 0)) { return super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) } dragScale(dyUnconsumed) } override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { mLastActionEvent = ev.action return super.onInterceptTouchEvent(ev) } override fun onStopNestedScroll(child: View) { if (isDragEnabled.not()) { return super.onStopNestedScroll(child) } val totalDragNormalized = if (dragUpEnabled) Math.abs(totalDrag) else -totalDrag val dragDirection = if (totalDrag > 0) DragDirection.UP else DragDirection.DOWN if (totalDragNormalized >= dragDismissDistance) { dispatchDismissCallback(dragDirection) } else { // settle back to natural position if (mLastActionEvent == MotionEvent.ACTION_DOWN) { // this is a 'defensive cleanup for new gestures', // don't animate here // see also https://github.com/nickbutcher/plaid/issues/185 translationY = 0f scaleX = 1f scaleY = 1f } else { animate() .translationY(0f) .scaleX(1f) .scaleY(1f) .setDuration(200L) .setInterpolator(PathInterpolatorCompat.create(0.4f, 0f, 0.2f, 1f)) .setListener(null) .start() } totalDrag = 0f draggingUp = false draggingDown = draggingUp dispatchDragCallback(0f, 0f) } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) if (dragDismissFraction > 0f) { dragDismissDistance = h * dragDismissFraction } } /** * Set if drag/swipe up dismiss is enabled */ fun setDragUpEnabled(dragUpEnabled: Boolean) { this.dragUpEnabled = dragUpEnabled } /** * Set lambda reference which is called when dismiss gesture has * been performed */ fun setOnDragDismissedListener(onDragDismissedListener: OnDragDismissedListener) { this.onDragDismissedListener = onDragDismissedListener } /** * Set lambda reference to listener which is called when drag is in * progress */ fun setOnDragActivityListener(onDragActivityListener: OnDragActivityListener) { this.onDragActivityListener = onDragActivityListener } /** * Set if drag gesture is enabled */ fun setDragEnabled(isDragEnabled: Boolean) { this.isDragEnabled = isDragEnabled } /** * Set if system bars should fade when dismiss is in progress */ fun setFadeSystemBars(fadeSystemBars: Boolean) { this.fadeSystemBars = fadeSystemBars if (fadeSystemBars) { (context as? Activity)?.also { systemBarsFader = SystemBarsFader(it) } } else { systemBarsFader = null } } private fun dragScale(scroll: Int) { if (scroll == 0) return totalDrag += scroll.toFloat() // track the direction & set the pivot point for scaling // don't double track i.e. if start dragging down and then reverse, keep tracking as // dragging down until they reach the 'natural' position if (scroll < 0 && !draggingUp && !draggingDown) { draggingDown = true if (shouldScale) pivotY = height.toFloat() } else if (scroll > 0 && !draggingDown && !draggingUp) { draggingUp = true if (shouldScale) { pivotY = 0f } } // how far have we dragged relative to the distance to perform a dismiss // (0–1 where 1 = dismiss distance). Decreasing logarithmically as we approach the limit var dragFraction = Math.log10((1 + Math.abs(totalDrag) / dragDismissDistance).toDouble()).toFloat() // calculate the desired translation given the drag fraction var dragTo = dragFraction * dragDismissDistance * dragElasticity if (draggingUp) { // as we use the absolute magnitude when calculating the drag fraction, need to // re-apply the drag direction dragTo *= -1f } translationY = dragTo if (shouldScale) { val scale = 1 - (1 - dragDismissScale) * dragFraction scaleX = scale scaleY = scale } // if we've reversed direction and gone past the settle point then clear the flags to // allow the list to get the scroll events & reset any transforms val downSettlePointReached = draggingDown && totalDrag >= 0 val upSettlePointReached = draggingUp && totalDrag <= 0 if (downSettlePointReached || upSettlePointReached) { dragFraction = 0f dragTo = dragFraction totalDrag = dragTo draggingUp = false draggingDown = draggingUp translationY = 0f scaleX = 1f scaleY = 1f } dispatchDragCallback(dragTo, Math.min(1f, Math.abs(totalDrag) / dragDismissDistance)) } private fun dispatchDragCallback(elasticOffsetPixels: Float, rawOffset: Float) { systemBarsFader?.onDrag(elasticOffsetPixels, rawOffset) onDragActivityListener?.onDrag(elasticOffsetPixels, rawOffset) } private fun dispatchDismissCallback(dragDirection: DragDirection) { systemBarsFader?.onDismiss() onDragDismissedListener?.onDismissed(dragDirection) } } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/HaulerViewExtensions.kt ================================================ package app.futured.hauler fun HaulerView.setOnDragDismissedListener(onDragDismissedListener: (DragDirection) -> Unit) { this.setOnDragDismissedListener(object : OnDragDismissedListener { override fun onDismissed(dragDirection: DragDirection) { onDragDismissedListener.invoke(dragDirection) } }) } fun HaulerView.setOnDragActivityListener(onDragActivityListener: (elasticOffsetPixels: Float, rawOffset: Float) -> Unit) { this.setOnDragActivityListener(object : OnDragActivityListener { override fun onDrag(elasticOffsetPixels: Float, rawOffset: Float) { onDragActivityListener.invoke(elasticOffsetPixels, rawOffset) } }) } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/LockableNestedScrollView.kt ================================================ package app.futured.hauler import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import androidx.core.widget.NestedScrollView class LockableNestedScrollView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : NestedScrollView(context, attrs, defStyleAttr) { private var isScrollable = true fun setScrollEnabled(isScrollEnabled: Boolean) { isScrollable = isScrollEnabled } @Suppress("ClickableViewAccessibility") override fun onTouchEvent(ev: MotionEvent): Boolean = when (ev.action) { MotionEvent.ACTION_DOWN -> isScrollable && super.onTouchEvent(ev) else -> super.onTouchEvent(ev) } override fun onInterceptTouchEvent(ev: MotionEvent) = isScrollable && super.onInterceptTouchEvent(ev) } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/OnDragActivityListener.kt ================================================ package app.futured.hauler interface OnDragActivityListener { fun onDrag(elasticOffsetPixels: Float, rawOffset: Float) } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/OnDragDismissedListener.kt ================================================ package app.futured.hauler interface OnDragDismissedListener { fun onDismissed(dragDirection: DragDirection) } ================================================ FILE: core/src/main/kotlin/app/futured/hauler/SystemBarsFader.kt ================================================ package app.futured.hauler import android.app.Activity import android.graphics.Color internal class SystemBarsFader(private val activity: Activity) { private val statusBarAlpha: Int by lazy { Color.alpha(getStatusBarColor()) } fun onDrag(elasticOffsetPixels: Float, rawOffset: Float) { when { elasticOffsetPixels != 0f -> // dragging downward or upward, fade the status bar in proportion activity.window.statusBarColor = ColorUtils.modifyAlpha(getStatusBarColor(), getNewAlpha(rawOffset)) elasticOffsetPixels == 0f -> activity.window.statusBarColor = ColorUtils.modifyAlpha(getStatusBarColor(), statusBarAlpha) } } fun onDismiss() { // set transparent window bg and transparent navigation bar activity.window.decorView.setBackgroundColor(0) activity.window.navigationBarColor = ColorUtils.modifyAlpha(activity.window.navigationBarColor, 0) } private fun getStatusBarColor() = activity.window.statusBarColor private fun getNewAlpha(rawOffset: Float) = ((1f - rawOffset) * statusBarAlpha).toInt() } ================================================ FILE: core/src/main/res/values/attrs_hauler_view.xml ================================================ ================================================ FILE: core/src/main/res/values/dimens.xml ================================================ 100dp ================================================ FILE: databinding/.gitignore ================================================ /build ================================================ FILE: databinding/build.gradle.kts ================================================ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.library") id("kotlin-android") id("kotlin-kapt") id("com.vanniktech.maven.publish") } android { compileSdkVersion(ProjectSettings.targetSdk) defaultConfig { minSdkVersion(ProjectSettings.minSdk) targetSdkVersion(ProjectSettings.targetSdk) } sourceSets { getByName("main").java.srcDir("src/main/kotlin") } buildFeatures { dataBinding = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } } dependencies { implementation(project(":core")) implementation(kotlin(Deps.Kotlin.stdlib, KotlinCompilerVersion.VERSION)) implementation(Deps.AndroidX.appcompat) } ================================================ FILE: databinding/gradle.properties ================================================ POM_NAME=Databinding extensions for core library POM_ARTIFACT_ID=databinding POM_PACKAGING=aar ================================================ FILE: databinding/src/main/AndroidManifest.xml ================================================ ================================================ FILE: databinding/src/main/kotlin/app/futured/hauler/databinding/IsDragEnabledAdapter.kt ================================================ package app.futured.hauler.databinding import androidx.databinding.BindingAdapter import app.futured.hauler.HaulerView @BindingAdapter("app:isDragEnabled") fun HaulerView.isDragEnabled(isDragEnabled: Boolean) { this.setDragEnabled(isDragEnabled) } ================================================ FILE: databinding/src/main/kotlin/app/futured/hauler/databinding/IsDragUpEnabledAdapter.kt ================================================ package app.futured.hauler.databinding import androidx.databinding.BindingAdapter import app.futured.hauler.HaulerView @BindingAdapter("app:isDragUpEnabled") fun HaulerView.isDragUpEnabled(isDragUpEnabled: Boolean) { this.setDragUpEnabled(isDragUpEnabled) } ================================================ FILE: databinding/src/main/kotlin/app/futured/hauler/databinding/IsScrollableAdapter.kt ================================================ package app.futured.hauler.databinding import androidx.databinding.BindingAdapter import app.futured.hauler.LockableNestedScrollView @BindingAdapter("app:isScrollable") fun LockableNestedScrollView.isScrollable(isScrollable: Boolean) { setScrollEnabled(isScrollable) } ================================================ FILE: databinding/src/main/kotlin/app/futured/hauler/databinding/OnDragDismissedListenerAdapter.kt ================================================ package app.futured.hauler.databinding import androidx.databinding.BindingAdapter import app.futured.hauler.HaulerView import app.futured.hauler.OnDragDismissedListener import app.futured.hauler.setOnDragDismissedListener @BindingAdapter("app:onDragDismissedListener") fun HaulerView.setOnDragDismissedListener(listener: OnDragDismissedListener) { this.setOnDragDismissedListener { listener.onDismissed(it) } } ================================================ FILE: detekt.yml ================================================ build: maxIssues: 100 complexity: active: true ComplexCondition: active: true ComplexMethod: active: true threshold: 20 ignoreSimpleWhenEntries: true LargeClass: threshold: 250 active: true TooManyFunctions: active: true thresholdInFiles: 30 thresholdInClasses: 30 thresholdInInterfaces: 30 thresholdInObjects: 25 LongParameterList: active: true NestedBlockDepth: active: true threshold: 4 StringLiteralDuplication: active: true empty-blocks: active: true EmptyCatchBlock: active: true EmptyClassBlock: active: true EmptyDefaultConstructor: active: true EmptyDoWhileBlock: active: true EmptyElseBlock: active: true EmptyFinallyBlock: active: true EmptyForBlock: active: true EmptyFunctionBlock: active: true EmptyIfBlock: active: true EmptyInitBlock: active: true EmptyKtFile: active: true EmptySecondaryConstructor: active: true EmptyWhenBlock: active: true EmptyWhileBlock: active: true exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true InstanceOfCheckForException: active: true ReturnFromFinally: active: true TooGenericExceptionCaught: active: false SwallowedException: active: true ThrowingExceptionFromFinally: active: true ThrowingExceptionsWithoutMessageOrCause: active: true ThrowingNewInstanceOfSameException: active: true TooGenericExceptionThrown: active: true naming: active: true ClassNaming: active: true ConstructorParameterNaming: active: true EnumNaming: active: true ForbiddenClassName: active: true FunctionMaxLength: active: true maximumFunctionNameLength: 35 FunctionMinLength: active: true FunctionNaming: active: true FunctionParameterNaming: active: true MatchingDeclarationName: active: true MemberNameEqualsClassName: active: true ObjectPropertyNaming: active: true PackageNaming: active: true TopLevelPropertyNaming: active: true VariableMaxLength: active: true maximumVariableNameLength: 50 VariableMinLength: active: true VariableNaming: active: true performance: ArrayPrimitive: active: true ForEachOnRange: active: true SpreadOperator: active: true UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true DuplicateCaseInWhenExpression: active: true EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: active: true ExplicitGarbageCollectionCall: active: true InvalidRange: active: true IteratorHasNextCallsNextMethod: active: true IteratorNotThrowingNoSuchElementException: active: true UnconditionalJumpStatementInLoop: active: true UnreachableCode: active: true UnsafeCallOnNullableType: active: true UnsafeCast: active: true UselessPostfixExpression: active: true WrongEqualsTypeParameter: active: true style: active: true MaxLineLength: active: false CollapsibleIfStatements: active: true DataClassContainsFunctions: active: false EqualsNullCall: active: true ExplicitItLambdaParameter: active: true ExpressionBodySyntax: active: true ForbiddenComment: active: true ForbiddenImport: active: true ForbiddenVoid: active: true FunctionOnlyReturningConstant: active: true LoopWithTooManyJumpStatements: active: true MagicNumber: active: false MandatoryBracesIfStatements: active: true MayBeConst: active: true ModifierOrder: active: true NestedClassesVisibility: active: false NewLineAtEndOfFile: active: true NoTabs: active: true OptionalAbstractKeyword: active: true OptionalUnit: active: true OptionalWhenBraces: active: true PreferToOverPairSyntax: active: false ProtectedMemberInFinalClass: active: true RedundantVisibilityModifierRule: active: true ReturnCount: active: true max: 4 SafeCast: active: true SerialVersionUIDInSerializableClass: active: true SpacingBetweenPackageAndImports: active: true ThrowsCount: active: true TrailingWhitespace: active: true UnnecessaryAbstractClass: active: true excludeAnnotatedClasses: "dagger.Module,android.arch.persistence.room.Dao" UnnecessaryApply: active: false # wait for fix UnnecessaryInheritance: active: true UnnecessaryLet: active: true UnnecessaryParentheses: active: true UntilInsteadOfRangeTo: active: true UnusedImports: active: false UnusedPrivateMember: active: true UseDataClass: active: true UtilityClassWithPublicConstructor: active: true VarCouldBeVal: active: true WildcardImport: active: true excludeImports: 'kotlinx.android.synthetic.*' ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # 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 # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.useAndroidX=true android.enableJetifier=true ### Maven publish configuration GROUP=app.futured.hauler VERSION_NAME=5.X.X-SNAPSHOT POM_DESCRIPTION=Library with swipe to dismiss Activity gesture implementation. POM_INCEPTION_YEAR=2018 POM_URL=https://github.com/futuredapp/hauler POM_SCM_URL=https://github.com/futuredapp/hauler POM_SCM_CONNECTION=scm:git:git://github.com/futuredapp/hauler.git POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/futuredapp/hauler.git POM_LICENCE_NAME=MIT POM_LICENCE_URL=https://github.com/futuredapp/hauler/blob/master/LICENSE POM_LICENCE_DIST=repo POM_DEVELOPER_ID=futured POM_DEVELOPER_NAME=Futured POM_DEVELOPER_URL=https://futured.app SONATYPE_HOST=DEFAULT RELEASE_SIGNING_ENABLED=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # 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="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. 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 MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock 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 APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } 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" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal 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= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: sample/.gitignore ================================================ /build ================================================ FILE: sample/build.gradle.kts ================================================ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.application") id("kotlin-android") id("kotlin-android-extensions") id("kotlin-kapt") } android { compileSdkVersion(ProjectSettings.targetSdk) defaultConfig { applicationId = ProjectSettings.applicationId minSdkVersion(ProjectSettings.minSdk) targetSdkVersion(ProjectSettings.targetSdk) } sourceSets { getByName("main").java.srcDir("src/main/kotlin") } dataBinding { isEnabled = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } } dependencies { implementation(project(":core")) implementation(project(":databinding")) // Kotlin implementation(kotlin(Deps.Kotlin.stdlib, KotlinCompilerVersion.VERSION)) implementation(Deps.AndroidX.appcompat) } ================================================ FILE: sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/MainActivity.kt ================================================ package app.futured.haulersample import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import app.futured.haulersample.draggable.SimpleActivity import app.futured.haulersample.draggable.SimpleJavaActivity import app.futured.haulersample.draggable.advanced.AdvancedActivity import app.futured.haulersample.draggable.databinding.DatabindingActivity import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startCommonButton.setOnClickListener { startActivity(SimpleActivity.getStartIntent(this)) } startAdvancedButton.setOnClickListener { startActivity(AdvancedActivity.getStartIntent(this)) } startBindingButton.setOnClickListener { startActivity(DatabindingActivity.getStartIntent(this)) } startJavaCommonButton.setOnClickListener { startActivity(SimpleJavaActivity.getStartIntent(this)) } } } ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/SimpleActivity.kt ================================================ package app.futured.haulersample.draggable import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import app.futured.hauler.setOnDragActivityListener import app.futured.hauler.setOnDragDismissedListener import app.futured.haulersample.R import kotlinx.android.synthetic.main.activity_simple.* class SimpleActivity : AppCompatActivity() { companion object { fun getStartIntent(context: Context): Intent = Intent(context, SimpleActivity::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_simple) commonHaulerView.setOnDragDismissedListener { finish() } commonHaulerView.setOnDragActivityListener { elasticOffset, rawOffset -> Log.d("SimpleActivity", "elasticOffset: $elasticOffset, rawOffset: $rawOffset") } } } ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/SimpleJavaActivity.java ================================================ package app.futured.haulersample.draggable; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import org.jetbrains.annotations.NotNull; import androidx.annotation.Nullable; import app.futured.hauler.DragDirection; import app.futured.hauler.HaulerView; import app.futured.hauler.OnDragDismissedListener; import app.futured.haulersample.R; public class SimpleJavaActivity extends Activity { public static Intent getStartIntent(Context context) { return new Intent(context, SimpleJavaActivity.class); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple); HaulerView hv = findViewById(R.id.commonHaulerView); hv.setOnDragDismissedListener(new OnDragDismissedListener() { @Override public void onDismissed(@NotNull DragDirection dragDirection) { finish(); } }); } } ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/advanced/AdvancedActivity.kt ================================================ package app.futured.haulersample.draggable.advanced import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import app.futured.hauler.DragDirection import app.futured.hauler.setOnDragDismissedListener import app.futured.haulersample.R import kotlinx.android.synthetic.main.activity_advanced.* class AdvancedActivity : AppCompatActivity() { companion object { fun getStartIntent(context: Context): Intent = Intent(context, AdvancedActivity::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_advanced) advancedHaulerView.setOnDragDismissedListener { dragDirection -> Toast.makeText(this, "Dismissed in direction: $dragDirection", Toast.LENGTH_SHORT).show() when (dragDirection) { DragDirection.DOWN -> { finish() overridePendingTransition(0, R.anim.anim_slide_down) } DragDirection.UP -> { finish() overridePendingTransition(0, R.anim.anim_slide_up) } } } ignoredAreaView.setScrollViewParent(scrollViewParent) } } ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/advanced/IgnoredAreaView.kt ================================================ package app.futured.haulersample.draggable.advanced import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import androidx.appcompat.widget.AppCompatTextView import app.futured.hauler.LockableNestedScrollView class IgnoredAreaView @kotlin.jvm.JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr) { private var parentScrollView: LockableNestedScrollView? = null fun setScrollViewParent(parentScrollView: LockableNestedScrollView) { this.parentScrollView = parentScrollView } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { return when (event.action) { MotionEvent.ACTION_DOWN -> { parentScrollView?.setScrollEnabled(false) true } MotionEvent.ACTION_UP -> { parentScrollView?.setScrollEnabled(true) true } else -> false } } } ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/databinding/DatabindingActivity.kt ================================================ package app.futured.haulersample.draggable.databinding import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import app.futured.hauler.DragDirection import app.futured.haulersample.R import app.futured.haulersample.databinding.ActivityDatabindingBinding import kotlinx.android.synthetic.main.activity_databinding.* class DatabindingActivity : AppCompatActivity(), DatabindingActivityView { companion object { fun getStartIntent(context: Context): Intent = Intent(context, DatabindingActivity::class.java) } lateinit var binding: ActivityDatabindingBinding private var state = DatabindingActivityState(true) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_databinding) refreshBinding() dragEnabledCheck.setOnCheckedChangeListener { _, isChecked -> state = state.copy(isDragEnabled = isChecked) refreshBinding() } } private fun refreshBinding() { binding.viewState = state binding.view = this binding.executePendingBindings() } override fun onDragDismissed(dragDirection: DragDirection) { finish() } } ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/databinding/DatabindingActivityState.kt ================================================ package app.futured.haulersample.draggable.databinding data class DatabindingActivityState( val isDragEnabled: Boolean ) ================================================ FILE: sample/src/main/kotlin/app/futured/haulersample/draggable/databinding/DatabindingActivityView.kt ================================================ package app.futured.haulersample.draggable.databinding import app.futured.hauler.DragDirection interface DatabindingActivityView { fun onDragDismissed(dragDirection: DragDirection) } ================================================ FILE: sample/src/main/res/anim/anim_slide_down.xml ================================================ ================================================ FILE: sample/src/main/res/anim/anim_slide_up.xml ================================================ ================================================ FILE: sample/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: sample/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: sample/src/main/res/layout/activity_advanced.xml ================================================ ================================================ FILE: sample/src/main/res/layout/activity_databinding.xml ================================================ ================================================ FILE: sample/src/main/res/layout/activity_main.xml ================================================