Repository: wayfair-incubator/panel-layout Branch: master Commit: f9fbb291a784 Files: 76 Total size: 113.6 KB Directory structure: gitextract_i0r_bc1v/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .idea/ │ ├── .name │ ├── codeStyles │ ├── gradle.xml │ ├── jarRepositories.xml │ ├── misc.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MAINTAINERS.md ├── README.md ├── RELEASING.md ├── SECURITY.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── wayfair/ │ │ └── panellayout/ │ │ └── ExampleInstrumentedTest.kt │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── wayfair/ │ │ └── panellayout/ │ │ ├── MainActivity.kt │ │ └── MaterialCardViewExt.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_ballpoint_pen.xml │ │ ├── ic_baseline_visibility_off.xml │ │ ├── ic_baseline_visibility_on.xml │ │ ├── ic_circle.xml │ │ ├── ic_circle_outline.xml │ │ ├── ic_color_fill.xml │ │ ├── ic_eraser.xml │ │ ├── ic_fountain_pen.xml │ │ ├── ic_grease_pencil.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_pencil.xml │ │ ├── ic_resize.xml │ │ ├── ic_select.xml │ │ ├── ic_show_hide_button.xml │ │ ├── ic_square.xml │ │ ├── ic_square_outline.xml │ │ ├── ic_text.xml │ │ ├── ic_triangle.xml │ │ └── ic_triangle_outline.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── colors.xml │ │ ├── shapes.xml │ │ └── tools.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── panellayout/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── wayfair/ │ │ └── panellayout/ │ │ ├── PanelLayout.kt │ │ ├── PanelLayoutCommands.kt │ │ ├── PanelPosition.kt │ │ └── PanelState.kt │ └── res/ │ ├── layout/ │ │ ├── panel_view_default_snap_bottom.xml │ │ ├── panel_view_default_snap_left.xml │ │ ├── panel_view_default_snap_right.xml │ │ └── panel_view_default_snap_top.xml │ └── values/ │ ├── panel_attrs.xml │ ├── panel_colors.xml │ ├── panel_dimens.xml │ └── panel_integers.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ master ] pull_request: paths-ignore: - 'docs/**' - '*.md' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: gradle/wrapper-validation-action@v1 - name: Build run: ./gradlew build -s ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish a release on: push: tags: - '*' jobs: plugin-deploy: runs-on: ubuntu-latest steps: - name: Checkout the repo uses: actions/checkout@v2 - name: Publish run: ./gradlew publish env: BINTRAY_API_KEY: ${{ secrets.BINTRAY_API_KEY }} BINTRAY_USER: ${{ secrets.BINTRAY_USER }} ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx ================================================ FILE: .idea/.name ================================================ Panel Layout ================================================ FILE: .idea/codeStyles ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/jarRepositories.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: CHANGELOG.md ================================================ ### v1.0.0-alpha02 - Fixes a bug that prevents resizing of the panel. ### v1.0.0-alpha01 - The very first release! ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing to Panel Layout First off, thank you for your interest in contributing to Panel Layout! ### Issue first Found a bug 🐛, have an idea 💡, or want to improve something 🔧? Please open an issue first describing the problem, your idea or possible improvements. Getting early feedback on your idea will save a ton of time before writing code in advance and realizing the idea is not feasible or not aligned with the goals of the project. ### Pull Requests Please try to cover added / modified functions with tests as much as possible and make tests a part of your PR. Writing a good description with screenshots / screen captures when necessary will help your PR get accepted / merged quickly. ================================================ FILE: LICENSE.md ================================================ BSD 2-Clause License Copyright (c) 2020 Wayfair GmbH Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MAINTAINERS.md ================================================ mkojadinovic [at] wayfair.com ================================================ FILE: README.md ================================================ *ARCHIVED* -- Wayfair's technology team is now focused on other endeavors. Please consider this project archived / available as-is until further notice. ## Panel Layout ![CI](https://github.com/GradleUp/auto-manifest/workflows/CI/badge.svg) Panel Layout is a UI library for Android that allows you to display a floating and resizable panel that can also snap to the edges. Panel Layout makes use of [ConstraintLayout](https://developer.android.com/training/constraint-layout) to lay out panel with rest of the content. This library is inspired by a good iOS UI framework: [PanelKit](https://github.com/louisdh/panelkit) ![](https://user-images.githubusercontent.com/4990386/83229577-e6564f00-a190-11ea-8998-97322e3d818d.gif) ### Importing Panel Layout [ ![Bintray](https://img.shields.io/bintray/v/wayfair/PanelLayout/PanelLayout) ](https://bintray.com/wayfair/PanelLayout/PanelLayout/_latestVersion) ```kotlin dependencies { implementation("com.wayfair.panellayout:panellayout:") } ``` **Note:** Panel Layout is currently in alpha and the public API it offers is __subject to heavy changes__. ### Panel Layout API Define if the Panel Layout is visible ```kotlin var panelVisible: Boolean ``` The command that put Panel Layout in one of the predefine Panel Positions. Possible panel positions are: `LEFT_EDGE`, `RIGHT_EDGE`, `TOP_EDGE`, `BOTTOM_EDGE`, `NO_EDGE`. ```kotlin fun snapPanelTo(panelPosition: PanelPosition) ``` The command that put the Panel Layout in absolute position with coordinates `x` and `y`. ```kotlin fun popPanelTo(x: Int, y: Int) ``` Define a listener to define actions on different kinds of events. ```kotlin var panelLayoutCallbacks: PanelLayout.Callbacks? interface Callbacks { fun beforeSnap(position: PanelPosition) fun afterSnap(position: PanelPosition) fun beforePop(popToX: Int, popToY: Int) fun afterPop(popToX: Int, popToY: Int) fun afterClose() } ``` ### Panel Layout attributes `panel_content` - Resource id of view where is placed the Panel Layout `panel_view` - Resource id of view inside the Panel Layout `panel_move_handle` - Resource id of view used for moving the Panel Layout inside of content view `panel_resize_enabled` - Flag that defines if the Panel Layout is resizable `panel_snap_to_edges` - Define edges where the Panel Layout could be snapped. Possible values: `all`, `none`, `left`, `top`, `right` and `bottom` `panel_start_height` - Start height `panel_start_width` - Start width ### How to Use Panel Layout Add Panel Layout in your layout: ```xml ``` Controls and listeners in the code: ```kotlin panelLayout.panelVisible = !panelLayout.panelVisible ``` ```kotlin panelLayout.popPanelTo(100, 100) ``` ```kotlin panelLayout.snapPanelTo(PanelPosition.RIGHT_EDGE) ``` ```kotlin panelLayout.panelLayoutCallbacks = object : PanelLayout.Callbacks { override fun beforeSnap(position: PanelPosition) { TODO("Not yet implemented") } override fun afterSnap(position: PanelPosition) { TODO("Not yet implemented") } override fun beforePop(popToX: Int, popToY: Int) { TODO("Not yet implemented") } override fun afterPop(popToX: Int, popToY: Int) { TODO("Not yet implemented") } override fun afterClose() { TODO("Not yet implemented") } } ``` ### LICENSE Panel Layout is licensed under 2-clause BSD License See [LICENSE.md](LICENSE.md) for details. ### CONTRIBUTION Panel Layout is open to contribution. See [CONTRIBUTING.md](CONTRIBUTING.md) for details ================================================ FILE: RELEASING.md ================================================ Releasing ======== 1. Change the version in `gradle.properties` to a new version. 1. Update the `CHANGELOG.md` for the impending release. 1. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 1. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version) 1. `git push && git push --tags` 1. Wait for CI to publish and verify the version in `README.md` ================================================ FILE: SECURITY.md ================================================ If you have discovered a security vulnerability, please email opensource@wayfair.com ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.wayfair.panellayout.sample" minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildFeatures { buildConfig = false resValues = false } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.1.0' implementation project(':panellayout') testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } ================================================ FILE: app/src/androidTest/java/com/wayfair/panellayout/ExampleInstrumentedTest.kt ================================================ package com.wayfair.panellayout 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.wayfair.panellayout", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/wayfair/panellayout/MainActivity.kt ================================================ package com.wayfair.panellayout import android.content.res.ColorStateList import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.children import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.colors.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) addShowHideClickListener() addPanelElevationAndRadiusAnimations() addColorSelectorClickListener() } private fun addShowHideClickListener() { showHideButton.setOnClickListener { panelLayout.panelVisible = !panelLayout.panelVisible showHideButton.isSelected = !showHideButton.isSelected } } private fun addColorSelectorClickListener() { colorSelector.children.forEach { child -> child.setOnClickListener { it.backgroundTintList?.defaultColor?.let { color -> art.imageTintList = ColorStateList.valueOf(color) } } } } private fun addPanelElevationAndRadiusAnimations() { panelLayout.panelLayoutCallbacks = object : PanelLayout.Callbacks { override fun beforeSnap(position: PanelPosition) { panel.animateRadius( from = resources.getDimension(R.dimen.panel_corner_radius), to = resources.getDimension(R.dimen.zero) ) panel.animateElevation( from = resources.getDimension(R.dimen.panel_elevation), to = resources.getDimension(R.dimen.zero) ) } override fun afterSnap(position: PanelPosition) {} override fun beforePop(popToX: Int, popToY: Int) { panel.animateRadius( from = resources.getDimension(R.dimen.zero), to = resources.getDimension(R.dimen.panel_corner_radius) ) panel.animateElevation( from = resources.getDimension(R.dimen.zero), to = resources.getDimension(R.dimen.panel_elevation) ) } override fun afterPop(popToX: Int, popToY: Int) {} override fun afterClose() {} } } } ================================================ FILE: app/src/main/java/com/wayfair/panellayout/MaterialCardViewExt.kt ================================================ package com.wayfair.panellayout import android.animation.ValueAnimator import com.google.android.material.card.MaterialCardView fun MaterialCardView.animateRadius(from: Float, to: Float) = ValueAnimator.ofFloat(from, to).apply { addUpdateListener { radius = it.animatedValue as Float } }.start() fun MaterialCardView.animateElevation(from: Float, to: Float) = ValueAnimator.ofFloat(from, to).apply { addUpdateListener { elevation = it.animatedValue as Float } }.start() ================================================ FILE: app/src/main/res/drawable/ic_ballpoint_pen.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_visibility_off.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_visibility_on.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_circle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_circle_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_color_fill.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_eraser.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_fountain_pen.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_grease_pencil.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_pencil.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_resize.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_select.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_show_hide_button.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_square.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_square_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_text.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_triangle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_triangle_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/colors.xml ================================================ ================================================ FILE: app/src/main/res/layout/shapes.xml ================================================ ================================================ FILE: app/src/main/res/layout/tools.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #9C27B0 #6a0080 #9C27B0 #FFFFFF #20000000 #424242 #F44336 #E91E63 #9C27B0 #673AB7 #3F51B5 #2196F3 #03A9F4 #00BCD4 #009688 #4CAF50 #8BC34A #CDDC39 #FFEB3B #FFC107 #FF9800 #FF5722 #795548 #9E9E9E #757575 #616161 #424242 #212121 #FFFFFF ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 0dp 4dp 4dp 32dp 32dp ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Panel Layout Shapes Tools Colors ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:4.0.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue May 05 17:46:57 EET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official GROUP=com.wayfair.panellayout VERSION_NAME=1.0.0-alpha02 POM_URL=https://github.com/wayfair-incubator/panel-layout/ POM_SCM_URL=https://github.com/wayfair-incubator/panel-layout.git POM_SCM_CONNECTION=scm:git:git://github.com/wayfair-incubator/panel-layout.git POM_LICENCE_NAME=BSD 2-Clause "Simplified" License POM_LICENCE_URL=https://raw.githubusercontent.com/wayfair-incubator/panel-layout/master/LICENSE POM_LICENCE_DIST=repo # https://github.com/gradle/gradle/issues/11412 systemProp.org.gradle.internal.publish.checksums.insecure=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: panellayout/.gitignore ================================================ /build ================================================ FILE: panellayout/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'org.gradle.maven-publish' android { compileSdkVersion 28 defaultConfig { minSdkVersion 21 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 } buildFeatures { buildConfig = false resValues = false } } androidExtensions { features = ['parcelize'] } dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation 'androidx.core:core-ktx:1.2.0' implementation "androidx.transition:transition:1.3.1" } publishing { publications.create("default", MavenPublication) { afterEvaluate { from(components.findByName("release")) } def sourcesJarTask = tasks.create("sourcesJar", Jar) { archiveClassifier.set("sources") from(android.sourceSets["main"].java.srcDirs) } artifact(sourcesJarTask) groupId = findProperty("GROUP") version = findProperty("VERSION_NAME") artifactId = findProperty("POM_ARTIFACT_ID") pom { name = findProperty("POM_NAME") description = findProperty("POM_DESCRIPTION") packaging = findProperty("POM_PACKAGING") url = findProperty("POM_URL") scm { url = findProperty("POM_SCM_URL") connection = findProperty("POM_SCM_CONNECTION") } licenses { license { name = findProperty("POM_LICENCE_NAME") url = findProperty("POM_LICENCE_URL") } } } } repositories { maven { name = "bintray" url = uri("https://api.bintray.com/maven/wayfair/PanelLayout/PanelLayout/;publish=1;override=1") credentials { username = System.getenv("BINTRAY_USER") password = System.getenv("BINTRAY_API_KEY") } } } } ================================================ FILE: panellayout/gradle.properties ================================================ POM_ARTIFACT_ID=panellayout POM_NAME=Panel Layout POM_DESCRIPTION=UI library for Android that allows you to display a floating and resizable panel that can also snap to the edges. POM_PACKAGING=aar ================================================ FILE: panellayout/src/main/AndroidManifest.xml ================================================ ================================================ FILE: panellayout/src/main/java/com/wayfair/panellayout/PanelLayout.kt ================================================ /* These imports produce a detekt false-positive: import androidx.core.graphics.component1 import androidx.core.graphics.component2 import androidx.core.graphics.component3 import androidx.core.graphics.component4 So UnusedImports are suppressed. */ @file:Suppress("UnusedImports", "CommentOverPrivateFunction") package com.wayfair.panellayout import android.annotation.SuppressLint import android.content.Context import android.content.res.TypedArray import android.graphics.Rect import android.os.Bundle import android.os.Parcelable import android.util.AttributeSet import android.view.* import android.view.MotionEvent.* import android.view.animation.AccelerateDecelerateInterpolator import androidx.annotation.ColorRes import androidx.annotation.IdRes import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.res.getResourceIdOrThrow import androidx.core.graphics.component1 import androidx.core.graphics.component2 import androidx.core.graphics.component3 import androidx.core.graphics.component4 import androidx.core.graphics.drawable.toDrawable import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.transition.AutoTransition import androidx.transition.ChangeBounds import androidx.transition.Transition import androidx.transition.TransitionManager import com.wayfair.panellayout.PanelPosition.* import com.wayfair.panellayout.PanelPosition.values import com.wayfair.panellayout.PanelState.HorizontalEdge.LEFT import com.wayfair.panellayout.PanelState.HorizontalEdge.RIGHT import com.wayfair.panellayout.PanelState.Snap.ANIMATING import com.wayfair.panellayout.PanelState.Snap.FLOATING import com.wayfair.panellayout.PanelState.Snap.SNAPPED import com.wayfair.panellayout.PanelState.VerticalEdge.BOTTOM import com.wayfair.panellayout.PanelState.VerticalEdge.TOP import kotlin.math.roundToInt import kotlin.math.sqrt @Suppress("LargeClass", "LongMethod") // It's hard to split view classes @SuppressLint("ClickableViewAccessibility") // ClickableViewAccessibility: We do not want to handle clicks on move and resize handles class PanelLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), PanelLayoutCommands { // Public field override var panelLayoutCallbacks: Callbacks? = null override var panelVisible: Boolean get() = panelState.isVisible set(value) { if (value) showPanel() else hidePanel() } // Attrs (initial values are not used) @IdRes private var panelResId = 0 @IdRes private var contentResId = 0 @IdRes private var moveHandleResId = 0 @IdRes private var resizeHandleResId = 0 private var resizeEnabled = false private var panelMinWidth = 0 private var panelMaxWidth = 0 private var panelMinHeight = 0 private var panelMaxHeight = 0 private var panelStartWidth = 0 private var panelStartHeight = 0 private var panelSnapWidth = 0f private var panelSnapHeight = 0f private var panelSnapWidthPercent = 0f private var panelSnapHeightPercent = 0f @LayoutRes private var snapLeftLayout = 0 @LayoutRes private var snapTopLayout = 0 @LayoutRes private var snapRightLayout = 0 @LayoutRes private var snapBottomLayout = 0 private var snapAnimationDuration = 0L private var snapOverlayAnimationDuration = 0L @ColorRes private var snapOverlayColor = 0 private var snapToEdges = 0 // View references private lateinit var content: View private lateinit var panelView: View private lateinit var moveHandle: View private var resizeHandle: View? = null private val leftOverlay = View(context) private val topOverlay = View(context) private val rightOverlay = View(context) private val bottomOverlay = View(context) private var isPanelStateRestored = false lateinit var panelState: PanelState val initialPanelState: PanelState get() = PanelState( snap = FLOATING, position = NO_EDGE, size = panelStartWidth to panelStartHeight ) private val preferredSnapWidth: Float get() = when (panelSnapWidthPercent) { NOT_SET -> panelSnapWidth else -> width * panelSnapWidthPercent } private val preferredSnapHeight: Float get() = when (panelSnapHeightPercent) { NOT_SET -> panelSnapHeight else -> height * panelSnapHeightPercent } // Touch / motion related private val moveSnapListener = PanelMoveSnapListener() private val popListener = PanelPopListener() private val resizeListener = PanelResizeListener() private var lastDownEvent: MotionEvent? = null private var relativeTouchPositionX = 0f private var relativeTouchPositionY = 0f private var touchSubject: View? = null init { isMotionEventSplittingEnabled = false isSaveEnabled = true readAttrs(attrs) setUpOverlays() } override fun onAttachedToWindow() { super.onAttachedToWindow() if (!isPanelStateRestored) { panelState = initialPanelState } ensureChildren() setTouchListeners() restorePanelFromState() } private fun ensureChildren() { fun Int.toResourceName() = resources.getResourceName(this) require(::content.isInitialized) { "Could not find child (panel_content) with id: ${contentResId.toResourceName()}" } require(::panelView.isInitialized) { "Could not find child (panel_view) with id: ${panelResId.toResourceName()}" } require(::moveHandle.isInitialized) { "Could not find child (panel_move_handle) with id: ${moveHandleResId.toResourceName()}" } if (resizeEnabled) { require(resizeHandle != null) { "Could not find child (panel_resize_handle) with id: ${resizeHandleResId.toResourceName()}" } } } override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { super.addView(child, index, params) child.findViewById(contentResId)?.let { content = it } child.findViewById(panelResId)?.let { panelView = it moveHandle = it.findViewById(moveHandleResId) } if (resizeEnabled) { child.findViewById(resizeHandleResId)?.let { resizeHandle = it } } } private fun restorePanelFromState() = post { if (!panelState.isVisible) { panelView.isVisible = false } else if (panelState.snap == FLOATING) { // Restore size val (withWidth, withHeight) = panelState.size // Restore position val toX = panelState.horizontalNearestEdgeDistance.toX(withWidth) val toY = panelState.verticalNearestEdgeDistance.toY(withHeight) applyFloatingPanelConstraints(toX, toY, withWidth, withHeight) } else { // Restore snap snapPanelTo(panelState.position) } } override fun onSaveInstanceState(): Parcelable? { val superState = super.onSaveInstanceState() return bundleOf( PARCELABLE_KEY_SUPER_STATE to superState, PARCELABLE_KEY_PANEL_STATE to panelState ) } override fun onRestoreInstanceState(state: Parcelable?) { val bundle = state as Bundle panelState = bundle.getParcelable(PARCELABLE_KEY_PANEL_STATE)!! val superState: AbsSavedState? = bundle.getParcelable(PARCELABLE_KEY_SUPER_STATE) isPanelStateRestored = true super.onRestoreInstanceState(superState) } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { if (panelState.snap == FLOATING) { coercePanelSize() } } private fun readAttrs(attrs: AttributeSet?) = attrs?.let { val a = context.obtainStyledAttributes(it, R.styleable.PanelLayout, 0, 0) try { readResizeEnabledAttr(a) readViewReferenceAttrs(a) readMinMaxSizeAttrs(a) readStartSizeAttrs(a) readSnapSizeAttrs(a) readSnapToEdgesAttr(a) readSnapAttrs(a) readOverlayAndAnimationAttrs(a) } finally { a.recycle() } } private fun readResizeEnabledAttr(a: TypedArray) { resizeEnabled = a.getBoolean(R.styleable.PanelLayout_panel_resize_enabled, true) } private fun readViewReferenceAttrs(a: TypedArray) { panelResId = a.getResourceIdOrThrow(R.styleable.PanelLayout_panel_view) contentResId = a.getResourceIdOrThrow(R.styleable.PanelLayout_panel_content) moveHandleResId = a.getResourceIdOrThrow(R.styleable.PanelLayout_panel_move_handle) resizeHandleResId = a.getResourceId(R.styleable.PanelLayout_panel_resize_handle, -1) } private fun readMinMaxSizeAttrs(a: TypedArray) { panelMinWidth = a.getDimensionPixelSize( R.styleable.PanelLayout_panel_min_width, resources.getDimensionPixelSize(R.dimen.panel_default_min_width) ) panelMaxWidth = a.getDimensionPixelSize( R.styleable.PanelLayout_panel_max_width, resources.getDimensionPixelSize(R.dimen.panel_default_max_width) ) panelMinHeight = a.getDimensionPixelSize( R.styleable.PanelLayout_panel_min_height, resources.getDimensionPixelSize(R.dimen.panel_default_min_height) ) panelMaxHeight = a.getDimensionPixelSize( R.styleable.PanelLayout_panel_max_height, resources.getDimensionPixelSize(R.dimen.panel_default_max_height) ) } private fun readStartSizeAttrs(a: TypedArray) { panelStartWidth = a.getDimensionPixelSize( R.styleable.PanelLayout_panel_start_width, resources.getDimensionPixelSize(R.dimen.panel_default_start_width) ) panelStartHeight = a.getDimensionPixelSize( R.styleable.PanelLayout_panel_start_height, resources.getDimensionPixelSize(R.dimen.panel_default_start_height) ) } private fun readSnapSizeAttrs(a: TypedArray) { panelSnapWidthPercent = a.getFloat(R.styleable.PanelLayout_panel_snap_width_percent, NOT_SET) panelSnapHeightPercent = a.getFloat(R.styleable.PanelLayout_panel_snap_height_percent, NOT_SET) if (panelSnapWidthPercent == NOT_SET) { panelSnapWidth = a.getDimension( R.styleable.PanelLayout_panel_snap_width, resources.getDimension(R.dimen.panel_default_snap_width) ) } if (panelSnapHeightPercent == NOT_SET) { panelSnapHeight = a.getDimension( R.styleable.PanelLayout_panel_snap_height, resources.getDimension(R.dimen.panel_default_snap_height) ) } } private fun readSnapToEdgesAttr(a: TypedArray) { snapToEdges = a.getInt( R.styleable.PanelLayout_panel_snap_to_edges, R.integer.panel_default_snap_edges ) } private fun readSnapAttrs(a: TypedArray) { snapLeftLayout = a.getResourceId( R.styleable.PanelLayout_panel_snap_left_layout, R.layout.panel_view_default_snap_left ) snapTopLayout = a.getResourceId( R.styleable.PanelLayout_panel_snap_top_layout, R.layout.panel_view_default_snap_top ) snapRightLayout = a.getResourceId( R.styleable.PanelLayout_panel_snap_right_layout, R.layout.panel_view_default_snap_right ) snapBottomLayout = a.getResourceId( R.styleable.PanelLayout_panel_snap_bottom_layout, R.layout.panel_view_default_snap_bottom ) } private fun readOverlayAndAnimationAttrs(a: TypedArray) { snapAnimationDuration = a.getInt( R.styleable.PanelLayout_panel_snap_animation_duration, resources.getInteger(R.integer.panel_default_snap_animation_duration) ).toLong() snapOverlayAnimationDuration = a.getInt( R.styleable.PanelLayout_panel_snap_overlay_animation_duration, resources.getInteger(R.integer.panel_default_snap_overlay_animation_duration) ).toLong() snapOverlayColor = a.getResourceId( R.styleable.PanelLayout_panel_snap_overlay_color, R.color.panel_default_snap_overlay_color ) } private fun setUpOverlays() { for (overlay in arrayOf(leftOverlay, topOverlay, rightOverlay, bottomOverlay)) { overlay.background = ContextCompat.getColor(context, snapOverlayColor).toDrawable() overlay.alpha = 0f overlay.layoutParams = LayoutParams(1, 1) // create square 1px x 1px to scale later. overlay.elevation = 2f addView(overlay) } } private fun setTouchListeners() { setOnTouchListener { v: View, event: MotionEvent -> when (touchSubject) { moveHandle -> when (panelState.snap) { FLOATING -> moveSnapListener.onTouch(v, event) SNAPPED -> popListener.onTouch(v, event) else -> false } resizeHandle -> when (panelState.snap) { FLOATING -> resizeListener.onTouch(v, event) else -> false } else -> false } } } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { when (event.actionMasked) { ACTION_DOWN -> { relativeTouchPositionX = event.rawX - panelView.x relativeTouchPositionY = event.rawY - panelView.y lastDownEvent = obtain(event) if (moveHandle.containsInHitRect(event)) { touchSubject = moveHandle } else if (resizeEnabled && resizeHandle != null && resizeHandle!!.containsInHitRect(event)) { touchSubject = resizeHandle!! } else { touchSubject = null } return false } ACTION_MOVE -> { return touchSubject != null && event.isSignificantlyDistantTo(lastDownEvent!!) } ACTION_UP -> { return touchSubject != null && event.isSignificantlyDistantTo(lastDownEvent!!) } ACTION_CANCEL -> { touchSubject = null return false } else -> return false } } /** * Imitates a panel resize with zero difference in size. *

* This will make sure the panel size is recalculated and limited by the new panelLayout size. * Whenever the keyboard is shown, we resize the panelView to fit into the limited space available. */ private fun coercePanelSize() = post { val (updatedWidth, updatedHeight) = calculateNewSize(diffX = 0f, diffY = 0f) resizePanel(updatedWidth, updatedHeight) } private fun calculateNewSize(diffX: Float, diffY: Float): Pair { var updatedWidth = (panelView.width + diffX).roundToInt().coerceIn(panelMinWidth, panelMaxWidth) var updatedHeight = (panelView.height + diffY).roundToInt().coerceIn(panelMinHeight, panelMaxHeight) // These negative adjustment values are to prevent resizing out of the panel layout val negativeAdjustmentX = (width - (panelView.x + updatedWidth)).coerceAtMost(0f).roundToInt() val negativeAdjustmentY = (height - (panelView.y + updatedHeight)).coerceAtMost(0f).roundToInt() updatedWidth += negativeAdjustmentX updatedHeight += negativeAdjustmentY return updatedWidth to updatedHeight } private fun resizePanel(updatedWidth: Int, updatedHeight: Int) { val params = panelView.layoutParams as LayoutParams params.width = updatedWidth params.height = updatedHeight panelView.layoutParams = params } private fun calculatePositionFor(x: Int, y: Int): PanelPosition { val (left, top, right, bottom) = panelView.moveBounds() if (x == left) return LEFT_EDGE if (x == right) return RIGHT_EDGE if (y == top) return TOP_EDGE if (y == bottom) return BOTTOM_EDGE return NO_EDGE } private fun showSnapOverlay(snapPosition: PanelPosition) { val overlay = snapPosition.overlay() val (touchPointX, touchPointY) = snapPosition.touchPoint() val (scaleX, scaleY) = snapPosition.overlayScale() overlay.translationX = touchPointX overlay.translationY = touchPointY overlay.pivotX = touchPointX / width overlay.pivotY = touchPointY / height overlay.clearAnimation() overlay.animate() .alpha(1f) .scaleX(scaleX) .scaleY(scaleY) .setDuration(snapOverlayAnimationDuration) .setInterpolator(AccelerateDecelerateInterpolator()) .start() } private fun hideSnapOverlay(snapPosition: PanelPosition) { val overlay = snapPosition.overlay() overlay.clearAnimation() overlay.animate() .alpha(0f) .scaleX(0f) .scaleY(0f) .setDuration(snapOverlayAnimationDuration) .setInterpolator(AccelerateDecelerateInterpolator()) .start() } private fun hidePanel() { if (panelState.snap == SNAPPED) { val transition = AutoTransition().apply { interpolator = AccelerateDecelerateInterpolator() duration = snapAnimationDuration addListener(object : Transition.TransitionListener { override fun onTransitionEnd(transition: Transition) { panelLayoutCallbacks?.afterClose() } override fun onTransitionStart(transition: Transition) {} // No-op override fun onTransitionResume(transition: Transition) {} // No-op override fun onTransitionPause(transition: Transition) {} // No-op override fun onTransitionCancel(transition: Transition) {} // No-op }) } TransitionManager.beginDelayedTransition(this, transition) } panelView.isVisible = false panelState.isVisible = false } private fun showPanel() { panelState.isVisible = true restorePanelFromState() } override fun popPanelTo(x: Int, y: Int) { panelState.snap = ANIMATING val (popWidth, popHeight) = panelState.sanitizedSize() val transition = ChangeBounds().apply { interpolator = AccelerateDecelerateInterpolator() duration = snapAnimationDuration addListener(object : Transition.TransitionListener { override fun onTransitionStart(transition: Transition) { panelLayoutCallbacks?.beforePop(x, y) resizeHandle?.isVisible = true } override fun onTransitionEnd(transition: Transition) { panelLayoutCallbacks?.afterPop(x, y) } override fun onTransitionResume(transition: Transition) {} // No-op override fun onTransitionPause(transition: Transition) {} // No-op override fun onTransitionCancel(transition: Transition) {} // No-op }) } TransitionManager.beginDelayedTransition(this, transition) applyFloatingPanelConstraints(x, y, popWidth, popHeight) panelState.position = NO_EDGE panelState.snap = FLOATING } private fun applyFloatingPanelConstraints(x: Int, y: Int, width: Int, height: Int) = ConstraintSet().apply { clone(this) clear(panelResId) clear(contentResId) connect(contentResId, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.LEFT) connect(contentResId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) connect(contentResId, ConstraintSet.RIGHT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT) connect(contentResId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) setTranslationX(panelResId, x.toFloat()) setTranslationY(panelResId, y.toFloat()) constrainWidth(panelResId, width) constrainHeight(panelResId, height) applyTo(this@PanelLayout) } override fun snapPanelTo(panelPosition: PanelPosition) { if (panelState.snap == FLOATING) { switchToMarginPositioning() } panelState.snap = ANIMATING when (panelPosition) { LEFT_EDGE -> applyConstraintsWithAnimation(snapLeftLayout) RIGHT_EDGE -> applyConstraintsWithAnimation(snapRightLayout) TOP_EDGE -> applyConstraintsWithAnimation(snapTopLayout) BOTTOM_EDGE -> applyConstraintsWithAnimation(snapBottomLayout) NO_EDGE -> throw IllegalArgumentException("Cannot snap panel with position NO_EDGE") } } private fun switchToMarginPositioning() { val marginLeft = panelView.translationX.roundToInt() val marginTop = panelView.translationY.roundToInt() panelView.translationX = 0f panelView.translationY = 0f ConstraintSet().apply { clone(this) connect(panelResId, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.LEFT, marginLeft) connect(panelResId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, marginTop) applyTo(this@PanelLayout) } } private fun applyConstraintsWithAnimation(layoutResId: Int) = post { val constraintSet = ConstraintSet() constraintSet.load(context, layoutResId) val transition = AutoTransition().apply { interpolator = AccelerateDecelerateInterpolator() duration = snapAnimationDuration addListener(object : Transition.TransitionListener { override fun onTransitionStart(transition: Transition) { panelLayoutCallbacks?.beforeSnap(panelState.position) resizeHandle?.isVisible = false } override fun onTransitionEnd(transition: Transition) { panelLayoutCallbacks?.afterSnap(panelState.position) } override fun onTransitionResume(transition: Transition) {} // No-op override fun onTransitionPause(transition: Transition) {} // No-op override fun onTransitionCancel(transition: Transition) {} // No-op }) } TransitionManager.beginDelayedTransition(this, transition) constraintSet.applyTo(this) panelState.snap = SNAPPED } private fun PanelPosition.isSnapEnabled() = snapToEdges.hasFlag(this.snapFlag()) private fun PanelPosition.snapFlag() = when (this) { LEFT_EDGE -> SNAP_TO_LEFT RIGHT_EDGE -> SNAP_TO_RIGHT TOP_EDGE -> SNAP_TO_TOP BOTTOM_EDGE -> SNAP_TO_BOTTOM NO_EDGE -> throw IllegalArgumentException("You cannot get a snapFlag for PanelState.Position.NO_EDGE") } private fun PanelPosition.overlay() = when (this) { LEFT_EDGE -> leftOverlay RIGHT_EDGE -> rightOverlay TOP_EDGE -> topOverlay BOTTOM_EDGE -> bottomOverlay NO_EDGE -> throw IllegalArgumentException("You cannot get an overlay for PanelState.Position.NO_EDGE") } private fun PanelPosition.overlayScale() = when (this) { LEFT_EDGE, RIGHT_EDGE -> preferredSnapWidth to height.toFloat() + 1 TOP_EDGE, BOTTOM_EDGE -> width.toFloat() + 1 to preferredSnapHeight NO_EDGE -> throw IllegalArgumentException("You cannot get an overlayScale for PanelState.Position.NO_EDGE") } private fun PanelPosition.touchPoint() = when (this) { LEFT_EDGE -> 0f to panelView.centerY() RIGHT_EDGE -> width.toFloat() to panelView.centerY() TOP_EDGE -> panelView.centerX() to 0f BOTTOM_EDGE -> panelView.centerX() to height.toFloat() NO_EDGE -> throw IllegalArgumentException("You cannot get an touchPoint for PanelState.Position.NO_EDGE") } private fun View.centerX() = x + width / 2f private fun View.centerY() = y + height / 2f private fun PanelState.sanitizedSize(): Pair { val (savedWidth, savedHeight) = size val w = when (savedWidth) { -1 -> panelStartWidth else -> savedWidth.coerceAtMost(width) } val h = when (savedHeight) { -1 -> panelStartHeight else -> savedHeight.coerceAtMost(height) } return w to h } private fun PanelState.HorizontalEdgeDistance.toX(panelWidth: Int) = when (this.edge) { LEFT -> distance RIGHT -> width - panelWidth - distance } private fun PanelState.VerticalEdgeDistance.toY(panelHeight: Int) = when (this.edge) { TOP -> distance BOTTOM -> height - panelHeight - distance } private fun View.calculateHorizontalNearestEdgeDistance(): PanelState.HorizontalEdgeDistance { val leftDistance = this.x.roundToInt() val rightDistance = this@PanelLayout.width - this.x.roundToInt() - this.width return if (leftDistance <= rightDistance) { PanelState.HorizontalEdgeDistance(LEFT, leftDistance) } else { PanelState.HorizontalEdgeDistance(RIGHT, rightDistance) } } private fun View.calculateVerticalNearestEdgeDistance(): PanelState.VerticalEdgeDistance { val topDistance = this.y.roundToInt() val bottomDistance = this@PanelLayout.height - this.y.roundToInt() - this.height return if (topDistance <= bottomDistance) { PanelState.VerticalEdgeDistance(TOP, topDistance) } else { PanelState.VerticalEdgeDistance(BOTTOM, bottomDistance) } } // Calculates move bounds given (width to height) pair in PanelLayout private fun Pair.moveBounds(offset: Int = 0): Rect { val (childWidth, childHeight) = this val minX = 0 + offset val minY = 0 + offset val maxX = (width - childWidth - offset).coerceAtLeast(minX) val maxY = (height - childHeight - offset).coerceAtLeast(minY) return Rect(minX, minY, maxX, maxY) } private fun View.moveBounds(): Rect = (width to height).moveBounds() private fun MotionEvent.isSignificantlyDistantTo(other: MotionEvent): Boolean { val deltaX = other.rawX - this.rawX val deltaY = other.rawY - this.rawY return sqrt(deltaX * deltaX + deltaY * deltaY) > ViewConfiguration.get(context).scaledTouchSlop } private fun View.containsInHitRect(event: MotionEvent) = offsetHitRectToAscendant(this@PanelLayout) .contains(event.x.toInt(), event.y.toInt()) private fun View.offsetHitRectToAscendant(ascendant: View): Rect { val hitRect = Rect() this.getHitRect(hitRect) var iterator = parent as View while (iterator != ascendant) { hitRect.offset(iterator.x.toInt(), iterator.y.toInt()) iterator = iterator.parent as View } return hitRect } private inner class PanelMoveSnapListener : OnTouchListener { override fun onTouch(v: View, event: MotionEvent): Boolean { return when (event.action) { ACTION_DOWN -> true ACTION_MOVE -> handleActionMove(event) ACTION_UP -> handleActionUp() else -> false } } private fun handleActionMove(event: MotionEvent): Boolean { val (left, top, right, bottom) = panelView.moveBounds() val nextX = (event.rawX - relativeTouchPositionX).roundToInt().coerceIn(left, right) val nextY = (event.rawY - relativeTouchPositionY).roundToInt().coerceIn(top, bottom) val currentPosition = panelState.position val nextPosition = calculatePositionFor(nextX, nextY) panelState.position = nextPosition handleSnapOverlayAnimation(currentPosition, nextPosition) movePanelTo(nextX, nextY) return true } private fun handleActionUp(): Boolean { if (panelState.position != NO_EDGE && panelState.position.isSnapEnabled()) { hideSnapOverlay(panelState.position) snapPanelTo(panelState.position) } touchSubject = null return true } // Checking current and next positions shows or hides snap overlays with animation private fun handleSnapOverlayAnimation( currentPosition: PanelPosition, nextPosition: PanelPosition ) { for (position in values()) { if (position == NO_EDGE) continue if (position.isSnapEnabled().not()) continue if (currentPosition != position && nextPosition == position) { showSnapOverlay(position) } if (currentPosition == position && nextPosition != position) { hideSnapOverlay(position) } } } private fun movePanelTo(destinationX: Int, destinationY: Int) { panelView.animate() .translationX(destinationX.toFloat()) .translationY(destinationY.toFloat()) .setDuration(0) .withEndAction { panelState.horizontalNearestEdgeDistance = panelView.calculateHorizontalNearestEdgeDistance() panelState.verticalNearestEdgeDistance = panelView.calculateVerticalNearestEdgeDistance() } .start() } } private inner class PanelPopListener : OnTouchListener { override fun onTouch(v: View, event: MotionEvent): Boolean { return when (event.action) { ACTION_DOWN -> true ACTION_MOVE -> handleActionMove(event) ACTION_UP -> handleActionUp() else -> false } } private fun handleActionMove(event: MotionEvent): Boolean { relativeTouchPositionX = event.rawX - panelView.x relativeTouchPositionY = event.rawY - panelView.y val (popWidth, popHeight) = panelState.sanitizedSize() val nextRelativeTouchPositionX = relativeTouchPositionX / panelView.width * popWidth val nextRelativeTouchPositionY = relativeTouchPositionY / panelView.height * popHeight val (left, top, right, bottom) = (popWidth to popHeight).moveBounds(offset = PANEL_POP_OFFSET) val popToX = (event.rawX - nextRelativeTouchPositionX).roundToInt().coerceIn(left, right) val popToY = (event.rawY - nextRelativeTouchPositionY).roundToInt().coerceIn(top, bottom) popPanelTo(popToX, popToY) relativeTouchPositionX = event.rawX - popToX relativeTouchPositionY = event.rawY - popToY return true } private fun handleActionUp(): Boolean { touchSubject = null return true } } private inner class PanelResizeListener : OnTouchListener { private var previousX = NOT_SET private var previousY = NOT_SET override fun onTouch(v: View, event: MotionEvent): Boolean { return when (event.action) { ACTION_DOWN -> true ACTION_MOVE -> handleActionMove(event) ACTION_UP -> handleActionUp() else -> false } } private fun handleActionMove(event: MotionEvent): Boolean { if (previousX == NOT_SET || previousY == NOT_SET) { previousX = lastDownEvent!!.rawX previousY = lastDownEvent!!.rawY } val diffX = event.rawX - previousX val diffY = event.rawY - previousY val (updatedWidth, updatedHeight) = calculateNewSize(diffX, diffY) resizePanel(updatedWidth, updatedHeight) panelState.size = updatedWidth to updatedHeight previousX = event.rawX previousY = event.rawY return true } private fun handleActionUp(): Boolean { previousX = NOT_SET previousY = NOT_SET touchSubject = null return true } } interface Callbacks { fun beforeSnap(position: PanelPosition) fun afterSnap(position: PanelPosition) fun beforePop(popToX: Int, popToY: Int) fun afterPop(popToX: Int, popToY: Int) fun afterClose() } companion object { private const val NOT_SET = -1f private const val PANEL_POP_OFFSET = 4 // distance from edges when panelState is popped. in pixels private const val PARCELABLE_KEY_SUPER_STATE = "superState" private const val PARCELABLE_KEY_PANEL_STATE = "panelState" // Possible flags for snapToEdges private const val SNAP_TO_LEFT = 1 private const val SNAP_TO_TOP = 2 private const val SNAP_TO_RIGHT = 4 private const val SNAP_TO_BOTTOM = 8 private fun Int.hasFlag(flag: Int) = (this and flag) == flag } } ================================================ FILE: panellayout/src/main/java/com/wayfair/panellayout/PanelLayoutCommands.kt ================================================ package com.wayfair.panellayout internal interface PanelLayoutCommands { var panelLayoutCallbacks: PanelLayout.Callbacks? var panelVisible: Boolean fun snapPanelTo(panelPosition: PanelPosition) fun popPanelTo(x: Int, y: Int) } ================================================ FILE: panellayout/src/main/java/com/wayfair/panellayout/PanelPosition.kt ================================================ package com.wayfair.panellayout enum class PanelPosition { LEFT_EDGE, RIGHT_EDGE, TOP_EDGE, BOTTOM_EDGE, NO_EDGE } ================================================ FILE: panellayout/src/main/java/com/wayfair/panellayout/PanelState.kt ================================================ package com.wayfair.panellayout import android.os.Parcelable import com.wayfair.panellayout.PanelState.HorizontalEdge.LEFT import com.wayfair.panellayout.PanelState.VerticalEdge.TOP import kotlinx.android.parcel.Parcelize @Parcelize data class PanelState( var isVisible: Boolean = true, var snap: Snap = Snap.FLOATING, var size: Pair = -1 to -1, var position: PanelPosition = PanelPosition.NO_EDGE, var horizontalNearestEdgeDistance: HorizontalEdgeDistance = HorizontalEdgeDistance(edge = LEFT, distance = 0), var verticalNearestEdgeDistance: VerticalEdgeDistance = VerticalEdgeDistance(edge = TOP, distance = 0) ) : Parcelable { @Parcelize data class HorizontalEdgeDistance(val edge: HorizontalEdge, val distance: Int) : Parcelable @Parcelize data class VerticalEdgeDistance(val edge: VerticalEdge, val distance: Int) : Parcelable enum class HorizontalEdge { LEFT, RIGHT } enum class VerticalEdge { TOP, BOTTOM } enum class Snap { FLOATING, ANIMATING, SNAPPED } } ================================================ FILE: panellayout/src/main/res/layout/panel_view_default_snap_bottom.xml ================================================ ================================================ FILE: panellayout/src/main/res/layout/panel_view_default_snap_left.xml ================================================ ================================================ FILE: panellayout/src/main/res/layout/panel_view_default_snap_right.xml ================================================ ================================================ FILE: panellayout/src/main/res/layout/panel_view_default_snap_top.xml ================================================ ================================================ FILE: panellayout/src/main/res/values/panel_attrs.xml ================================================ ================================================ FILE: panellayout/src/main/res/values/panel_colors.xml ================================================ #802196F3 ================================================ FILE: panellayout/src/main/res/values/panel_dimens.xml ================================================ 100dp 600dp 100dp 600dp 320dp 480dp 300dp 300dp ================================================ FILE: panellayout/src/main/res/values/panel_integers.xml ================================================ 15 300 300 ================================================ FILE: settings.gradle ================================================ include ':app', ':panellayout' rootProject.name = "Panel Layout"