From cbd4ece8c1b175728b0c9314acbee8b248174ec6 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Thu, 22 Feb 2018 22:16:55 +0100
Subject: [PATCH] Prepare for new editor

---
 app/build.gradle.kts                          |    1 +
 .../quasseldroid_ng/ui/chat/ChatActivity.kt   |   67 +-
 .../quasseldroid_ng/ui/chat/EditorFragment.kt |   78 -
 .../res/layout-sw720dp-land/activity_main.xml |   63 +-
 app/src/main/res/layout/activity_main.xml     |   63 +-
 app/src/main/res/layout/fragment_editor.xml   |   39 -
 app/src/main/res/layout/layout_editor.xml     |   70 +
 app/src/main/res/layout/layout_main.xml       |   34 +
 app/src/main/res/layout/layout_slider.xml     |   56 +
 app/src/main/res/layout/layout_toolbar.xml    |   41 +
 app/src/main/res/values/strings.xml           |    1 +
 app/src/main/res/values/styles_widgets.xml    |   15 +-
 app/src/main/res/values/themes_amoled.xml     |    3 +-
 app/src/main/res/values/themes_base.xml       |   28 +-
 app/src/main/res/values/themes_gruvbox.xml    |    6 +-
 app/src/main/res/values/themes_quassel.xml    |    7 -
 app/src/main/res/values/themes_solarized.xml  |    6 +-
 settings.gradle                               |    2 +-
 slidingpanel/build.gradle.kts                 |   22 +
 slidingpanel/src/main/AndroidManifest.xml     |    5 +
 .../slidinguppanel/ScrollableViewHelper.java  |   63 +
 .../slidinguppanel/SlidingUpPanelLayout.java  | 1492 +++++++++++++++++
 .../slidinguppanel/ViewDragHelper.java        | 1481 ++++++++++++++++
 .../src/main/res/drawable/above_shadow.xml    |    7 +
 .../src/main/res/drawable/below_shadow.xml    |    7 +
 slidingpanel/src/main/res/values/attrs.xml    |   25 +
 26 files changed, 3413 insertions(+), 269 deletions(-)
 delete mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/EditorFragment.kt
 delete mode 100644 app/src/main/res/layout/fragment_editor.xml
 create mode 100644 app/src/main/res/layout/layout_editor.xml
 create mode 100644 app/src/main/res/layout/layout_main.xml
 create mode 100644 app/src/main/res/layout/layout_slider.xml
 create mode 100644 app/src/main/res/layout/layout_toolbar.xml
 create mode 100644 slidingpanel/build.gradle.kts
 create mode 100644 slidingpanel/src/main/AndroidManifest.xml
 create mode 100644 slidingpanel/src/main/java/com/sothree/slidinguppanel/ScrollableViewHelper.java
 create mode 100644 slidingpanel/src/main/java/com/sothree/slidinguppanel/SlidingUpPanelLayout.java
 create mode 100644 slidingpanel/src/main/java/com/sothree/slidinguppanel/ViewDragHelper.java
 create mode 100644 slidingpanel/src/main/res/drawable/above_shadow.xml
 create mode 100644 slidingpanel/src/main/res/drawable/below_shadow.xml
 create mode 100644 slidingpanel/src/main/res/values/attrs.xml

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 46b14cc6d..037188746 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -140,6 +140,7 @@ dependencies {
   // UI
   implementation("me.zhanghai.android.materialprogressbar", "library", "1.4.2")
   implementation("com.afollestad.material-dialogs", "core", "0.9.6.0")
+  implementation(project(":slidingpanel"))
 
   // Quality Assurance
   implementation(project(":malheur"))
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
index 476a772b9..9f75fec59 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
@@ -10,12 +10,14 @@ import android.support.design.widget.Snackbar
 import android.support.v4.widget.DrawerLayout
 import android.support.v7.app.ActionBarDrawerToggle
 import android.support.v7.widget.Toolbar
-import android.view.Gravity
-import android.view.Menu
-import android.view.MenuItem
+import android.text.InputType
+import android.view.*
+import android.widget.EditText
+import android.widget.ImageButton
 import butterknife.BindView
 import butterknife.ButterKnife
 import com.afollestad.materialdialogs.MaterialDialog
+import com.sothree.slidinguppanel.SlidingUpPanelLayout
 import de.kuschku.libquassel.protocol.Message
 import de.kuschku.libquassel.protocol.Message_Type
 import de.kuschku.libquassel.session.ConnectionState
@@ -45,6 +47,15 @@ class ChatActivity : ServiceBoundActivity() {
   @BindView(R.id.progressBar)
   lateinit var progressBar: MaterialContentLoadingProgressBar
 
+  @BindView(R.id.editor_panel)
+  lateinit var editorPanel: SlidingUpPanelLayout
+
+  @BindView(R.id.send)
+  lateinit var send: ImageButton
+
+  @BindView(R.id.chatline)
+  lateinit var chatline: EditText
+
   private lateinit var drawerToggle: ActionBarDrawerToggle
 
   private val handler = AndroidHandlerThread("Chat")
@@ -57,6 +68,27 @@ class ChatActivity : ServiceBoundActivity() {
 
   private lateinit var backlogSettings: BacklogSettings
 
+  private val panelSlideListener: SlidingUpPanelLayout.PanelSlideListener = object :
+    SlidingUpPanelLayout.PanelSlideListener {
+    override fun onPanelSlide(panel: View?, slideOffset: Float) = Unit
+
+    override fun onPanelStateChanged(panel: View?,
+                                     previousState: SlidingUpPanelLayout.PanelState?,
+                                     newState: SlidingUpPanelLayout.PanelState?) {
+      val selectionStart = chatline.selectionStart
+      val selectionEnd = chatline.selectionEnd
+
+      when (newState) {
+        SlidingUpPanelLayout.PanelState.COLLAPSED ->
+          chatline.inputType = chatline.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
+        else                                      ->
+          chatline.inputType = chatline.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
+      }
+
+      chatline.setSelection(selectionStart, selectionEnd)
+    }
+  }
+
   override fun onCreate(savedInstanceState: Bundle?) {
     handler.onCreate()
     super.onCreate(savedInstanceState)
@@ -71,6 +103,17 @@ class ChatActivity : ServiceBoundActivity() {
 
     setSupportActionBar(toolbar)
 
+    send.setOnClickListener {
+      send()
+    }
+
+    chatline.setOnKeyListener { _, keyCode, event ->
+      if (event.hasNoModifiers() && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) {
+        send()
+      }
+      false
+    }
+
     viewModel.getBuffer().observe(
       this, Observer {
       if (it != null && drawerLayout.isDrawerOpen(Gravity.START)) {
@@ -139,6 +182,20 @@ class ChatActivity : ServiceBoundActivity() {
       progressBar.progress = progress
     }
     )
+
+    editorPanel.addPanelSlideListener(panelSlideListener)
+    editorPanel.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED
+  }
+
+  fun send() {
+    viewModel.session { session ->
+      viewModel.getBuffer().let { bufferId ->
+        session.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
+          session.rpcHandler?.sendInput(bufferInfo, chatline.text.toString())
+        }
+      }
+    }
+    chatline.text.clear()
   }
 
   override fun onSaveInstanceState(outState: Bundle?) {
@@ -230,7 +287,7 @@ class ChatActivity : ServiceBoundActivity() {
       startActivity(Intent(applicationContext, SettingsActivity::class.java))
       true
     }
-    R.id.disconnect   -> {
+    R.id.disconnect      -> {
       handler.post {
         getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE).editApply {
           putBoolean(Keys.Status.reconnect, false)
@@ -241,7 +298,7 @@ class ChatActivity : ServiceBoundActivity() {
       }
       true
     }
-    else              -> super.onOptionsItemSelected(item)
+    else                 -> super.onOptionsItemSelected(item)
   }
 
   override fun onDestroy() {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/EditorFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/EditorFragment.kt
deleted file mode 100644
index bbabc6a96..000000000
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/EditorFragment.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package de.kuschku.quasseldroid_ng.ui.chat
-
-import android.arch.lifecycle.ViewModelProviders
-import android.os.Bundle
-import android.view.KeyEvent
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.EditText
-import android.widget.ImageButton
-import butterknife.BindView
-import butterknife.ButterKnife
-import de.kuschku.quasseldroid_ng.R
-import de.kuschku.quasseldroid_ng.ui.settings.data.AppearanceSettings
-import de.kuschku.quasseldroid_ng.ui.settings.data.Settings
-import de.kuschku.quasseldroid_ng.ui.viewmodel.QuasselViewModel
-import de.kuschku.quasseldroid_ng.util.helper.invoke
-import de.kuschku.quasseldroid_ng.util.helper.let
-import de.kuschku.quasseldroid_ng.util.irc.format.IrcFormatSerializer
-import de.kuschku.quasseldroid_ng.util.service.ServiceBoundFragment
-
-class EditorFragment : ServiceBoundFragment() {
-
-  @BindView(R.id.send)
-  lateinit var send: ImageButton
-
-  @BindView(R.id.chatline)
-  lateinit var chatline: EditText
-
-  private lateinit var viewModel: QuasselViewModel
-
-  private var ircFormatSerializer: IrcFormatSerializer? = null
-  private lateinit var appearanceSettings: AppearanceSettings
-
-  override fun onCreate(savedInstanceState: Bundle?) {
-    super.onCreate(savedInstanceState)
-
-    viewModel = ViewModelProviders.of(activity!!)[QuasselViewModel::class.java]
-    appearanceSettings = Settings.appearance(activity!!)
-
-    if (ircFormatSerializer == null) {
-      ircFormatSerializer = IrcFormatSerializer(context!!)
-    }
-  }
-
-  override fun onCreateView(inflater: LayoutInflater,
-                            container: ViewGroup?,
-                            savedInstanceState: Bundle?): View? {
-    val view = inflater.inflate(R.layout.fragment_editor, container, false)
-    ButterKnife.bind(this, view)
-
-
-
-    send.setOnClickListener {
-      send()
-    }
-
-    chatline.setOnKeyListener { _, keyCode, event ->
-      if (event.hasNoModifiers() && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) {
-        send()
-      }
-      false
-    }
-
-    return view
-  }
-
-  fun send() {
-    viewModel.session { session ->
-      viewModel.getBuffer().let { bufferId ->
-        session.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
-          session.rpcHandler?.sendInput(bufferInfo, chatline.text.toString())
-        }
-      }
-    }
-    chatline.text.clear()
-  }
-}
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw720dp-land/activity_main.xml b/app/src/main/res/layout-sw720dp-land/activity_main.xml
index c28cef95b..cb1aeeba4 100644
--- a/app/src/main/res/layout-sw720dp-land/activity_main.xml
+++ b/app/src/main/res/layout-sw720dp-land/activity_main.xml
@@ -42,68 +42,7 @@
         android:background="?colorDivider" />
     </LinearLayout>
 
-    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-      xmlns:app="http://schemas.android.com/apk/res-auto"
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:fitsSystemWindows="true"
-      android:orientation="vertical">
-
-      <android.support.design.widget.AppBarLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:theme="?attr/actionBarTheme">
-
-        <FrameLayout
-          android:layout_width="match_parent"
-          android:layout_height="?attr/actionBarSize">
-
-          <android.support.v7.widget.Toolbar
-            android:id="@+id/toolbar"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            app:contentInsetStartWithNavigation="0dp"
-            app:popupTheme="@style/Widget.PopupOverlay">
-
-            <fragment
-              android:id="@+id/fragment_toolbar"
-              android:name="de.kuschku.quasseldroid_ng.ui.chat.ToolbarFragment"
-              android:layout_width="fill_parent"
-              android:layout_height="fill_parent"
-              tools:layout="@layout/fragment_toolbar" />
-
-          </android.support.v7.widget.Toolbar>
-
-          <de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
-            android:id="@+id/progressBar"
-            style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="bottom"
-            app:mpb_progressStyle="horizontal"
-            app:mpb_setBothDrawables="true"
-            app:mpb_useIntrinsicPadding="false"
-            tools:indeterminate="true" />
-        </FrameLayout>
-
-      </android.support.design.widget.AppBarLayout>
-
-      <fragment
-        android:id="@+id/fragment_messages"
-        android:name="de.kuschku.quasseldroid_ng.ui.chat.messages.MessageListFragment"
-        android:layout_width="match_parent"
-        android:layout_height="0dip"
-        android:layout_weight="1"
-        tools:layout="@layout/fragment_messages" />
-
-      <fragment
-        android:id="@+id/fragment_editor"
-        android:name="de.kuschku.quasseldroid_ng.ui.chat.EditorFragment"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        tools:layout="@layout/fragment_editor" />
-
-    </LinearLayout>
+    <include layout="@layout/layout_main" />
   </LinearLayout>
 
   <fragment
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index a1b588ad5..b7d6dc16c 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -6,68 +6,7 @@
   android:layout_height="match_parent"
   android:fitsSystemWindows="true">
 
-  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true"
-    android:orientation="vertical">
-
-    <android.support.design.widget.AppBarLayout
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:theme="?attr/actionBarTheme">
-
-      <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="?attr/actionBarSize">
-
-        <android.support.v7.widget.Toolbar
-          android:id="@+id/toolbar"
-          android:layout_width="match_parent"
-          android:layout_height="match_parent"
-          app:contentInsetStartWithNavigation="0dp"
-          app:popupTheme="?attr/actionBarPopupTheme">
-
-          <fragment
-            android:id="@+id/fragment_toolbar"
-            android:name="de.kuschku.quasseldroid_ng.ui.chat.ToolbarFragment"
-            android:layout_width="fill_parent"
-            android:layout_height="fill_parent"
-            tools:layout="@layout/fragment_toolbar" />
-
-        </android.support.v7.widget.Toolbar>
-
-        <de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
-          android:id="@+id/progressBar"
-          style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
-          android:layout_width="match_parent"
-          android:layout_height="wrap_content"
-          android:layout_gravity="bottom"
-          app:mpb_progressStyle="horizontal"
-          app:mpb_setBothDrawables="true"
-          app:mpb_useIntrinsicPadding="false"
-          tools:indeterminate="true" />
-      </FrameLayout>
-
-    </android.support.design.widget.AppBarLayout>
-
-    <fragment
-      android:id="@+id/fragment_messages"
-      android:name="de.kuschku.quasseldroid_ng.ui.chat.messages.MessageListFragment"
-      android:layout_width="match_parent"
-      android:layout_height="0dip"
-      android:layout_weight="1"
-      tools:layout="@layout/fragment_messages" />
-
-    <fragment
-      android:id="@+id/fragment_editor"
-      android:name="de.kuschku.quasseldroid_ng.ui.chat.EditorFragment"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      tools:layout="@layout/fragment_editor" />
-
-  </LinearLayout>
+  <include layout="@layout/layout_main" />
 
   <fragment
     android:id="@+id/fragment_nick_list"
diff --git a/app/src/main/res/layout/fragment_editor.xml b/app/src/main/res/layout/fragment_editor.xml
deleted file mode 100644
index 4c8573c69..000000000
--- a/app/src/main/res/layout/fragment_editor.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:app="http://schemas.android.com/apk/res-auto"
-  xmlns:tools="http://schemas.android.com/tools"
-  android:layout_width="match_parent"
-  android:layout_height="wrap_content"
-  android:background="?colorBackgroundCard"
-  android:elevation="4dp"
-  tools:showIn="@layout/activity_main">
-
-  <android.support.v7.widget.AppCompatEditText
-    android:id="@+id/chatline"
-    android:layout_width="0dip"
-    android:layout_height="?attr/actionBarSize"
-    android:layout_weight="1"
-    android:background="@android:color/transparent"
-    android:gravity="top"
-    android:hint="@string/label_placeholder"
-    android:imeOptions="actionSend|flagNoEnterAction"
-    android:inputType="textCapSentences|textShortMessage|textAutoCorrect"
-    android:paddingBottom="17dp"
-    android:paddingLeft="20dp"
-    android:paddingRight="20dp"
-    android:paddingTop="17dp"
-    android:textColor="?attr/colorForeground"
-    android:textSize="16sp" />
-
-  <android.support.v7.widget.AppCompatImageButton
-    android:id="@+id/send"
-    style="?attr/buttonStyleSmall"
-    android:layout_width="?attr/actionBarSize"
-    android:layout_height="?attr/actionBarSize"
-    android:layout_gravity="top"
-    android:background="?attr/selectableItemBackgroundBorderless"
-    android:padding="12dp"
-    android:scaleType="fitXY"
-    android:tint="?attr/colorAccent"
-    app:srcCompat="@drawable/ic_send" />
-</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_editor.xml b/app/src/main/res/layout/layout_editor.xml
new file mode 100644
index 000000000..f2c4e903a
--- /dev/null
+++ b/app/src/main/res/layout/layout_editor.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <ScrollView
+    android:id="@+id/chatline_scroller"
+    android:layout_width="0dp"
+    android:layout_height="0dp"
+    android:layout_marginBottom="8dp"
+    app:layout_constraintBottom_toTopOf="@+id/formatting_toolbar_container"
+    app:layout_constraintEnd_toStartOf="@+id/send"
+    app:layout_constraintHorizontal_bias="1.0"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintTop_toTopOf="parent"
+    app:layout_constraintVertical_bias="0.0">
+
+    <android.support.v7.widget.AppCompatEditText
+      android:id="@+id/chatline"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:background="@android:color/transparent"
+      android:gravity="top"
+      android:hint="@string/label_placeholder"
+      android:imeOptions="actionSend|flagNoEnterAction"
+      android:inputType="textCapSentences|textAutoCorrect|textShortMessage"
+      android:paddingBottom="17dp"
+      android:paddingLeft="20dp"
+      android:paddingRight="20dp"
+      android:paddingTop="17dp"
+      android:textColor="?attr/colorForeground"
+      android:textSize="16sp" />
+
+  </ScrollView>
+
+  <android.support.v7.widget.AppCompatImageButton
+    android:id="@+id/send"
+    style="?attr/buttonStyleSmall"
+    android:layout_width="?attr/actionBarSize"
+    android:layout_height="?attr/actionBarSize"
+    android:layout_gravity="top"
+    android:background="?attr/selectableItemBackgroundBorderless"
+    android:padding="12dp"
+    android:scaleType="fitXY"
+    android:tint="?attr/colorAccent"
+    app:layout_constraintEnd_toEndOf="parent"
+    app:srcCompat="@drawable/ic_send" />
+
+  <android.support.design.widget.AppBarLayout
+    android:id="@+id/formatting_toolbar_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?attr/colorBackgroundCard"
+    app:layout_constraintBottom_toBottomOf="parent">
+
+    <android.support.v7.widget.Toolbar
+      android:id="@+id/formatting_toolbar"
+      android:layout_width="match_parent"
+      android:layout_height="?attr/actionBarSize">
+
+      <android.support.v7.widget.ActionMenuView
+        android:id="@+id/formatting_menu"
+        android:layout_width="wrap_content"
+        android:layout_height="?attr/actionBarSize" />
+
+    </android.support.v7.widget.Toolbar>
+
+  </android.support.design.widget.AppBarLayout>
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_main.xml b/app/src/main/res/layout/layout_main.xml
new file mode 100644
index 000000000..ef45eb6e9
--- /dev/null
+++ b/app/src/main/res/layout/layout_main.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:fitsSystemWindows="true"
+  android:orientation="vertical">
+
+  <include layout="@layout/layout_toolbar" />
+
+  <com.sothree.slidinguppanel.SlidingUpPanelLayout
+    android:id="@+id/editor_panel"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="bottom"
+    app:umanoAntiDragView="@id/card_panel"
+    app:umanoPanelHeight="?actionBarSize"
+    app:umanoScrollableView="@id/chatline_scroller"
+    app:umanoShadowHeight="4dp">
+
+    <fragment
+      android:id="@+id/fragment_messages"
+      android:name="de.kuschku.quasseldroid_ng.ui.chat.messages.MessageListFragment"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      tools:layout="@layout/fragment_messages" />
+
+    <include layout="@layout/layout_slider" />
+
+  </com.sothree.slidinguppanel.SlidingUpPanelLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_slider.xml b/app/src/main/res/layout/layout_slider.xml
new file mode 100644
index 000000000..6d4a3b7a4
--- /dev/null
+++ b/app/src/main/res/layout/layout_slider.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.sothree.slidinguppanel.SlidingUpPanelLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:gravity="bottom"
+  app:umanoFadeColor="?colorBackground"
+  app:umanoOverlay="true"
+  app:umanoPanelHeight="0dip"
+  app:umanoScrollableView="@id/msg_history"
+  app:umanoShadowHeight="0dip">
+
+  <include layout="@layout/layout_editor" />
+
+  <FrameLayout
+    android:id="@+id/card_panel"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+
+    <android.support.v7.widget.CardView
+      style="?attr/cardStyle"
+      android:layout_width="fill_parent"
+      android:layout_height="fill_parent"
+      android:layout_marginBottom="16.0dip"
+      android:layout_marginLeft="16.0dip"
+      android:layout_marginRight="16.0dip"
+      android:layout_marginTop="16.0dip"
+      app:cardBackgroundColor="?attr/colorBackgroundCard">
+
+      <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="vertical">
+
+        <TextView
+          android:layout_width="fill_parent"
+          android:layout_height="48.0dip"
+          android:gravity="center_vertical"
+          android:paddingLeft="16.0dip"
+          android:paddingRight="16.0dip"
+          android:text="@string/label_input_history"
+          android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+          android:textColor="?attr/colorForegroundSecondary" />
+
+        <android.support.v7.widget.RecyclerView
+          android:id="@+id/msg_history"
+          android:layout_width="fill_parent"
+          android:layout_height="fill_parent" />
+
+      </LinearLayout>
+
+    </android.support.v7.widget.CardView>
+
+  </FrameLayout>
+
+</com.sothree.slidinguppanel.SlidingUpPanelLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_toolbar.xml b/app/src/main/res/layout/layout_toolbar.xml
new file mode 100644
index 000000000..ae3f32270
--- /dev/null
+++ b/app/src/main/res/layout/layout_toolbar.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:theme="?attr/actionBarTheme">
+
+  <FrameLayout
+    android:layout_width="match_parent"
+    android:layout_height="?attr/actionBarSize">
+
+    <android.support.v7.widget.Toolbar
+      android:id="@+id/toolbar"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      app:contentInsetStartWithNavigation="0dp"
+      app:popupTheme="?attr/actionBarPopupTheme">
+
+      <fragment
+        android:id="@+id/fragment_toolbar"
+        android:name="de.kuschku.quasseldroid_ng.ui.chat.ToolbarFragment"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        tools:layout="@layout/fragment_toolbar" />
+
+    </android.support.v7.widget.Toolbar>
+
+    <de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
+      android:id="@+id/progressBar"
+      style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="bottom"
+      app:mpb_progressStyle="horizontal"
+      app:mpb_setBothDrawables="true"
+      app:mpb_useIntrinsicPadding="false"
+      tools:indeterminate="true" />
+  </FrameLayout>
+
+</android.support.design.widget.AppBarLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9e5efd640..65e50732a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,6 +11,7 @@
   <string name="label_drawer_close">Close</string>
   <string name="label_drawer_open">Open</string>
   <string name="label_filter_messages">Filter Messages</string>
+  <string name="label_input_history">Input History</string>
   <string name="label_placeholder">Write a message…</string>
   <string name="label_save">Save</string>
   <string name="label_select_multiple">Select</string>
diff --git a/app/src/main/res/values/styles_widgets.xml b/app/src/main/res/values/styles_widgets.xml
index 106ffe012..e9c9428f5 100644
--- a/app/src/main/res/values/styles_widgets.xml
+++ b/app/src/main/res/values/styles_widgets.xml
@@ -10,6 +10,10 @@
 
   <style name="Widget.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
     <item name="drawerArrowStyle">@style/Widget.DrawerArrowToggle</item>
+  </style>
+
+  <style name="Widget.AppBarOverlay.Auto" parent="Widget.AppBarOverlay">
+    <item name="drawerArrowStyle">@style/Widget.DrawerArrowToggle</item>
     <item name="colorControlNormal">?colorTextPrimary</item>
     <item name="android:textColorPrimary">?colorTextPrimary</item>
     <item name="android:textColorSecondary">?colorTextSecondary</item>
@@ -17,6 +21,9 @@
 
   <style name="Widget.AppBarOverlay.Light" parent="ThemeOverlay.AppCompat.ActionBar">
     <item name="drawerArrowStyle">@style/Widget.DrawerArrowToggle.Light</item>
+  </style>
+
+  <style name="Widget.AppBarOverlay.Light.Auto" parent="Widget.AppBarOverlay.Light">
     <item name="colorControlNormal">?colorTextPrimary</item>
     <item name="android:textColorPrimary">?colorTextPrimary</item>
     <item name="android:textColorSecondary">?colorTextSecondary</item>
@@ -30,11 +37,15 @@
     <item name="color">?attr/colorControlNormal</item>
   </style>
 
-  <style name="Widget.PopupOverlay" parent="ThemeOverlay.AppCompat">
+  <style name="Widget.PopupOverlay" parent="ThemeOverlay.AppCompat" />
+
+  <style name="Widget.PopupOverlay.Auto" parent="Widget.PopupOverlay">
     <item name="android:colorBackground">?colorBackgroundCard</item>
   </style>
 
-  <style name="Widget.PopupOverlay.Light" parent="ThemeOverlay.AppCompat.Light">
+  <style name="Widget.PopupOverlay.Light" parent="ThemeOverlay.AppCompat.Light" />
+
+  <style name="Widget.PopupOverlay.Light.Auto" parent="Widget.PopupOverlay.Light">
     <item name="android:colorBackground">?colorBackgroundCard</item>
   </style>
 
diff --git a/app/src/main/res/values/themes_amoled.xml b/app/src/main/res/values/themes_amoled.xml
index d07b367e0..d6ba7db0f 100644
--- a/app/src/main/res/values/themes_amoled.xml
+++ b/app/src/main/res/values/themes_amoled.xml
@@ -2,7 +2,8 @@
 
 <resources>
   <color name="amoled_background">#000000</color>
-  <style name="Theme.ChatTheme.Amoled" parent="Theme.ChatTheme">
+
+  <style name="Theme.ChatTheme.Amoled" parent="Theme.ChatTheme.Auto">
     <item name="colorPrimary">#000</item>
     <item name="colorPrimaryDark">#000</item>
     <item name="colorAccent">@color/colorAccent</item>
diff --git a/app/src/main/res/values/themes_base.xml b/app/src/main/res/values/themes_base.xml
index 25c06f659..352c1f815 100644
--- a/app/src/main/res/values/themes_base.xml
+++ b/app/src/main/res/values/themes_base.xml
@@ -48,10 +48,6 @@
     <item name="colorOffline">@color/colorOfflineDark</item>
     <item name="colorAway">@color/colorAwayDark</item>
 
-    <item name="android:textColor">?colorTextPrimary</item>
-    <item name="android:textColorSecondary">?colorTextSecondary</item>
-    <item name="colorControlNormal">?colorTextPrimary</item>
-
     <item name="cardStyle">@style/CardView.Dark</item>
 
     <item name="mircColor0">#ffffff</item>
@@ -72,6 +68,16 @@
     <item name="mircColorF">#c0c0c0</item>
   </style>
 
+  <style name="Theme.ChatTheme.Auto" parent="Theme.ChatTheme">
+    <item name="actionBarTheme">@style/Widget.AppBarOverlay.Auto</item>
+    <item name="formatBarTheme">@style/Widget.AppBarOverlay.Auto</item>
+    <item name="actionBarPopupTheme">@style/Widget.PopupOverlay.Auto</item>
+
+    <item name="android:textColor">?colorTextPrimary</item>
+    <item name="android:textColorSecondary">?colorTextSecondary</item>
+    <item name="colorControlNormal">?colorTextPrimary</item>
+  </style>
+
   <style name="Theme.ChatTheme.Light" parent="Base.ChatTheme.Light">
     <item name="actionBarTheme">@style/Widget.AppBarOverlay</item>
     <item name="formatBarTheme">@style/Widget.AppBarOverlay.Light</item>
@@ -88,10 +94,6 @@
     <item name="colorOffline">@color/colorOfflineLight</item>
     <item name="colorAway">@color/colorAwayLight</item>
 
-    <item name="android:textColor">?colorTextPrimary</item>
-    <item name="android:textColorSecondary">?colorTextSecondary</item>
-    <item name="colorControlNormal">?colorTextPrimary</item>
-
     <item name="cardStyle">@style/CardView.Light</item>
 
     <item name="mircColor0">#ffffff</item>
@@ -111,4 +113,14 @@
     <item name="mircColorE">#808080</item>
     <item name="mircColorF">#c0c0c0</item>
   </style>
+
+  <style name="Theme.ChatTheme.Light.Auto" parent="Theme.ChatTheme.Light">
+    <item name="actionBarTheme">@style/Widget.AppBarOverlay.Light.Auto</item>
+    <item name="formatBarTheme">@style/Widget.AppBarOverlay.Light.Auto</item>
+    <item name="actionBarPopupTheme">@style/Widget.PopupOverlay.Light.Auto</item>
+
+    <item name="android:textColor">?colorTextPrimary</item>
+    <item name="android:textColorSecondary">?colorTextSecondary</item>
+    <item name="colorControlNormal">?colorTextPrimary</item>
+  </style>
 </resources>
diff --git a/app/src/main/res/values/themes_gruvbox.xml b/app/src/main/res/values/themes_gruvbox.xml
index 7076cc5c7..f42712844 100644
--- a/app/src/main/res/values/themes_gruvbox.xml
+++ b/app/src/main/res/values/themes_gruvbox.xml
@@ -2,7 +2,8 @@
 
 <resources>
   <color name="gruvbox_light_background">#fbf1c7</color>
-  <style name="Theme.ChatTheme.Gruvbox_Light" parent="Theme.ChatTheme.Light">
+
+  <style name="Theme.ChatTheme.Gruvbox_Light" parent="Theme.ChatTheme.Light.Auto">
     <item name="colorPrimary">?attr/colorBackgroundCard</item>
     <item name="colorPrimaryDark">#b6ae91</item>
     <item name="colorAccent">#d65d0e</item>
@@ -55,7 +56,8 @@
   </style>
 
   <color name="gruvbox_dark_background">#282828</color>
-  <style name="Theme.ChatTheme.Gruvbox_Dark" parent="Theme.ChatTheme">
+
+  <style name="Theme.ChatTheme.Gruvbox_Dark" parent="Theme.ChatTheme.Auto">
     <item name="colorPrimary">?attr/colorBackgroundCard</item>
     <item name="colorPrimaryDark">#3c3734</item>
     <item name="colorAccent">#d65d0e</item>
diff --git a/app/src/main/res/values/themes_quassel.xml b/app/src/main/res/values/themes_quassel.xml
index 09d64fbac..f21e5ab13 100644
--- a/app/src/main/res/values/themes_quassel.xml
+++ b/app/src/main/res/values/themes_quassel.xml
@@ -2,14 +2,7 @@
 
 <resources>
   <color name="quassel_light_background">#fafafa</color>
-
-  <style name="Widget.AppBarOverlay.Quassel_Light" parent="Widget.AppBarOverlay.Light">
-    <item name="colorControlNormal">@color/colorFillDark</item>
-    <item name="android:textColorPrimary">@color/colorFillDark</item>
-  </style>
   <style name="Theme.ChatTheme.Quassel_Light" parent="Theme.ChatTheme.Light">
-    <item name="actionBarTheme">@style/Widget.AppBarOverlay.Quassel_Light</item>
-
     <item name="senderColor0">#cc0000</item>
     <item name="senderColor1">#006cad</item>
     <item name="senderColor2">#4d9900</item>
diff --git a/app/src/main/res/values/themes_solarized.xml b/app/src/main/res/values/themes_solarized.xml
index 7bab332e7..69aed4a23 100644
--- a/app/src/main/res/values/themes_solarized.xml
+++ b/app/src/main/res/values/themes_solarized.xml
@@ -2,7 +2,8 @@
 
 <resources>
   <color name="solarized_light_background">#FDF6E3</color>
-  <style name="Theme.ChatTheme.Solarized_Light" parent="Theme.ChatTheme.Light">
+
+  <style name="Theme.ChatTheme.Solarized_Light" parent="Theme.ChatTheme.Light.Auto">
     <item name="colorPrimary">?attr/colorBackgroundCard</item>
     <item name="colorPrimaryDark">#b0ac9e</item>
     <item name="colorAccent">#B58900</item>
@@ -55,7 +56,8 @@
   </style>
 
   <color name="solarized_dark_background">#002B36</color>
-  <style name="Theme.ChatTheme.Solarized_Dark" parent="Theme.ChatTheme">
+
+  <style name="Theme.ChatTheme.Solarized_Dark" parent="Theme.ChatTheme.Auto">
     <item name="colorPrimary">?attr/colorBackgroundCard</item>
     <item name="colorPrimaryDark">?attr/colorBackground</item>
     <item name="colorAccent">#B58900</item>
diff --git a/settings.gradle b/settings.gradle
index 1215a3453..89d849405 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,3 @@
-include ':invokerannotations', ':invokergenerator', ':lib', ':malheur', ':app'
+include ':invokerannotations', ':invokergenerator', ':lib', ':malheur', ':app', ":slidingpanel"
 
 rootProject.buildFileName = 'build.gradle.kts'
diff --git a/slidingpanel/build.gradle.kts b/slidingpanel/build.gradle.kts
new file mode 100644
index 000000000..1caf1beaa
--- /dev/null
+++ b/slidingpanel/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+  id("com.android.library")
+}
+
+android {
+  compileSdkVersion(27)
+  buildToolsVersion("27.0.3")
+
+  defaultConfig {
+    minSdkVersion(14)
+    targetSdkVersion(27)
+  }
+}
+
+dependencies {
+  // App Compat
+  withVersion("27.0.2") {
+    implementation("com.android.support", "support-v4", version)
+    implementation("com.android.support", "support-annotations", version)
+    implementation("com.android.support", "recyclerview-v7", version)
+  }
+}
diff --git a/slidingpanel/src/main/AndroidManifest.xml b/slidingpanel/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..e8ac6fcc6
--- /dev/null
+++ b/slidingpanel/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.sothree.slidinguppanel.library"
+  android:versionCode="17"
+  android:versionName="3.4.0"></manifest>
diff --git a/slidingpanel/src/main/java/com/sothree/slidinguppanel/ScrollableViewHelper.java b/slidingpanel/src/main/java/com/sothree/slidinguppanel/ScrollableViewHelper.java
new file mode 100644
index 000000000..d5539a85f
--- /dev/null
+++ b/slidingpanel/src/main/java/com/sothree/slidinguppanel/ScrollableViewHelper.java
@@ -0,0 +1,63 @@
+package com.sothree.slidinguppanel;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.ScrollView;
+
+/**
+ * Helper class for determining the current scroll positions for scrollable views. Currently works
+ * for ListView, ScrollView and RecyclerView, but the library users can override it to add support
+ * for other views.
+ */
+public class ScrollableViewHelper {
+  /**
+   * Returns the current scroll position of the scrollable view. If this method returns zero or
+   * less, it means at the scrollable view is in a position such as the panel should handle
+   * scrolling. If the method returns anything above zero, then the panel will let the scrollable
+   * view handle the scrolling
+   *
+   * @param scrollableView the scrollable view
+   * @param isSlidingUp    whether or not the panel is sliding up or down
+   * @return the scroll position
+   */
+  public int getScrollableViewScrollPosition(View scrollableView, boolean isSlidingUp) {
+    if (scrollableView == null) return 0;
+    if (scrollableView instanceof ScrollView) {
+      if (isSlidingUp) {
+        return scrollableView.getScrollY();
+      } else {
+        ScrollView sv = ((ScrollView) scrollableView);
+        View child = sv.getChildAt(0);
+        return (child.getBottom() - (sv.getHeight() + sv.getScrollY()));
+      }
+    } else if (scrollableView instanceof ListView && ((ListView) scrollableView).getChildCount() > 0) {
+      ListView lv = ((ListView) scrollableView);
+      if (lv.getAdapter() == null) return 0;
+      if (isSlidingUp) {
+        View firstChild = lv.getChildAt(0);
+        // Approximate the scroll position based on the top child and the first visible item
+        return lv.getFirstVisiblePosition() * firstChild.getHeight() - firstChild.getTop();
+      } else {
+        View lastChild = lv.getChildAt(lv.getChildCount() - 1);
+        // Approximate the scroll position based on the bottom child and the last visible item
+        return (lv.getAdapter().getCount() - lv.getLastVisiblePosition() - 1) * lastChild.getHeight() + lastChild.getBottom() - lv.getBottom();
+      }
+    } else if (scrollableView instanceof RecyclerView && ((RecyclerView) scrollableView).getChildCount() > 0) {
+      RecyclerView rv = ((RecyclerView) scrollableView);
+      RecyclerView.LayoutManager lm = rv.getLayoutManager();
+      if (rv.getAdapter() == null) return 0;
+      if (isSlidingUp) {
+        View firstChild = rv.getChildAt(0);
+        // Approximate the scroll position based on the top child and the first visible item
+        return rv.getChildLayoutPosition(firstChild) * lm.getDecoratedMeasuredHeight(firstChild) - lm.getDecoratedTop(firstChild);
+      } else {
+        View lastChild = rv.getChildAt(rv.getChildCount() - 1);
+        // Approximate the scroll position based on the bottom child and the last visible item
+        return (rv.getAdapter().getItemCount() - 1) * lm.getDecoratedMeasuredHeight(lastChild) + lm.getDecoratedBottom(lastChild) - rv.getBottom();
+      }
+    } else {
+      return 0;
+    }
+  }
+}
diff --git a/slidingpanel/src/main/java/com/sothree/slidinguppanel/SlidingUpPanelLayout.java b/slidingpanel/src/main/java/com/sothree/slidinguppanel/SlidingUpPanelLayout.java
new file mode 100644
index 000000000..bc850c0f1
--- /dev/null
+++ b/slidingpanel/src/main/java/com/sothree/slidinguppanel/SlidingUpPanelLayout.java
@@ -0,0 +1,1492 @@
+package com.sothree.slidinguppanel;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.sothree.slidinguppanel.library.R;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class SlidingUpPanelLayout extends ViewGroup {
+
+  /**
+   * Tag for the sliding state stored inside the bundle
+   */
+  public static final String SLIDING_STATE = "sliding_state";
+  private static final String TAG = SlidingUpPanelLayout.class.getSimpleName();
+  /**
+   * Default peeking out panel height
+   */
+  private static final int DEFAULT_PANEL_HEIGHT = 68; // dp;
+  /**
+   * Default anchor point height
+   */
+  private static final float DEFAULT_ANCHOR_POINT = 1.0f; // In relative %
+  /**
+   * Default height of the shadow above the peeking out panel
+   */
+  private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp;
+
+  /**
+   * If no fade color is given by default it will fade to 80% gray.
+   */
+  private static final int DEFAULT_FADE_COLOR = 0x99000000;
+
+  /**
+   * Default Minimum velocity that will be detected as a fling
+   */
+  private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second
+  /**
+   * Default is set to false because that is how it was written
+   */
+  private static final boolean DEFAULT_OVERLAY_FLAG = false;
+  /**
+   * Default is set to true for clip panel for performance reasons
+   */
+  private static final boolean DEFAULT_CLIP_PANEL_FLAG = true;
+  /**
+   * Default attributes for layout
+   */
+  private static final int[] DEFAULT_ATTRS = new int[]{
+    android.R.attr.gravity
+  };
+  /**
+   * Default parallax length of the main view
+   */
+  private static final int DEFAULT_PARALLAX_OFFSET = 0;
+  /**
+   * Default initial state for the component
+   */
+  private static PanelState DEFAULT_SLIDE_STATE = PanelState.COLLAPSED;
+  /**
+   * The paint used to dim the main layout when sliding
+   */
+  private final Paint mCoveredFadePaint = new Paint();
+  /**
+   * Drawable used to draw the shadow between panes.
+   */
+  private final Drawable mShadowDrawable;
+  private final List<PanelSlideListener> mPanelSlideListeners = new CopyOnWriteArrayList<>();
+  private final ViewDragHelper mDragHelper;
+  private final Rect mTmpRect = new Rect();
+  /**
+   * Minimum velocity that will be detected as a fling
+   */
+  private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY;
+  /**
+   * The fade color used for the panel covered by the slider. 0 = no fading.
+   */
+  private int mCoveredFadeColor = DEFAULT_FADE_COLOR;
+  /**
+   * The size of the overhang in pixels.
+   */
+  private int mPanelHeight = -1;
+  /**
+   * The size of the shadow in pixels.
+   */
+  private int mShadowHeight = -1;
+  /**
+   * Parallax offset
+   */
+  private int mParallaxOffset = -1;
+  /**
+   * True if the collapsed panel should be dragged up.
+   */
+  private boolean mIsSlidingUp;
+  /**
+   * Panel overlays the windows instead of putting it underneath it.
+   */
+  private boolean mOverlayContent = DEFAULT_OVERLAY_FLAG;
+  /**
+   * The main view is clipped to the main top border
+   */
+  private boolean mClipPanel = DEFAULT_CLIP_PANEL_FLAG;
+  /**
+   * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
+   * used for dragging.
+   */
+  private View mDragView;
+  /**
+   * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
+   * used for dragging.
+   */
+  private int mDragViewResId = -1;
+  /**
+   * If provided, the panel can not be dragged by this view. Otherwise, the entire panel can be
+   * used for dragging.
+   */
+  private View mAntiDragView;
+  /**
+   * If provided, the panel can not be dragged by this view. Otherwise, the entire panel can be
+   * used for dragging.
+   */
+  private int mAntiDragViewResId = -1;
+  /**
+   * If provided, the panel will transfer the scroll from this view to itself when needed.
+   */
+  private View mScrollableView;
+  private int mScrollableViewResId;
+  private ScrollableViewHelper mScrollableViewHelper = new ScrollableViewHelper();
+  /**
+   * The child view that can slide, if any.
+   */
+  private View mSlideableView;
+  /**
+   * The main view
+   */
+  private View mMainView;
+  private PanelState mSlideState = DEFAULT_SLIDE_STATE;
+  /**
+   * If the current slide state is DRAGGING, this will store the last non dragging state
+   */
+  private PanelState mLastNotDraggingSlideState = DEFAULT_SLIDE_STATE;
+  /**
+   * How far the panel is offset from its expanded position.
+   * range [0, 1] where 0 = collapsed, 1 = expanded.
+   */
+  private float mSlideOffset;
+  /**
+   * How far in pixels the slideable panel may move.
+   */
+  private int mSlideRange;
+  /**
+   * An anchor point where the panel can stop during sliding
+   */
+  private float mAnchorPoint = 1.f;
+  /**
+   * A panel view is locked into internal scrolling or another condition that
+   * is preventing a drag.
+   */
+  private boolean mIsUnableToDrag;
+  /**
+   * Flag indicating that sliding feature is enabled\disabled
+   */
+  private boolean mIsTouchEnabled;
+  private float mPrevMotionX;
+  private float mPrevMotionY;
+  private float mInitialMotionX;
+  private float mInitialMotionY;
+  private boolean mIsScrollableViewHandlingTouch = false;
+  private View.OnClickListener mFadeOnClickListener;
+  /**
+   * Stores whether or not the pane was expanded the last time it was slideable.
+   * If expand/collapse operations are invoked this state is modified. Used by
+   * instance state save/restore.
+   */
+  private boolean mFirstLayout = true;
+
+  public SlidingUpPanelLayout(Context context) {
+    this(context, null);
+  }
+
+  public SlidingUpPanelLayout(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public SlidingUpPanelLayout(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+
+    if (isInEditMode()) {
+      mShadowDrawable = null;
+      mDragHelper = null;
+      return;
+    }
+
+    Interpolator scrollerInterpolator = null;
+    if (attrs != null) {
+      TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS);
+
+      if (defAttrs != null) {
+        int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY);
+        setGravity(gravity);
+        defAttrs.recycle();
+      }
+
+
+      TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingUpPanelLayout);
+
+      if (ta != null) {
+        mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoPanelHeight, -1);
+        mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoShadowHeight, -1);
+        mParallaxOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoParallaxOffset, -1);
+
+        mMinFlingVelocity = ta.getInt(R.styleable.SlidingUpPanelLayout_umanoFlingVelocity, DEFAULT_MIN_FLING_VELOCITY);
+        mCoveredFadeColor = ta.getColor(R.styleable.SlidingUpPanelLayout_umanoFadeColor, DEFAULT_FADE_COLOR);
+
+        mDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoDragView, -1);
+        mAntiDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoAntiDragView, -1);
+        mScrollableViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoScrollableView, -1);
+
+        mOverlayContent = ta.getBoolean(R.styleable.SlidingUpPanelLayout_umanoOverlay, DEFAULT_OVERLAY_FLAG);
+        mClipPanel = ta.getBoolean(R.styleable.SlidingUpPanelLayout_umanoClipPanel, DEFAULT_CLIP_PANEL_FLAG);
+
+        mAnchorPoint = ta.getFloat(R.styleable.SlidingUpPanelLayout_umanoAnchorPoint, DEFAULT_ANCHOR_POINT);
+
+        mSlideState = PanelState.values()[ta.getInt(R.styleable.SlidingUpPanelLayout_umanoInitialState, DEFAULT_SLIDE_STATE.ordinal())];
+
+        int interpolatorResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoScrollInterpolator, -1);
+        if (interpolatorResId != -1) {
+          scrollerInterpolator = AnimationUtils.loadInterpolator(context, interpolatorResId);
+        }
+        ta.recycle();
+      }
+    }
+
+    final float density = context.getResources().getDisplayMetrics().density;
+    if (mPanelHeight == -1) {
+      mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
+    }
+    if (mShadowHeight == -1) {
+      mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
+    }
+    if (mParallaxOffset == -1) {
+      mParallaxOffset = (int) (DEFAULT_PARALLAX_OFFSET * density);
+    }
+    // If the shadow height is zero, don't show the shadow
+    if (mShadowHeight > 0) {
+      if (mIsSlidingUp) {
+        mShadowDrawable = getResources().getDrawable(R.drawable.above_shadow);
+      } else {
+        mShadowDrawable = getResources().getDrawable(R.drawable.below_shadow);
+      }
+    } else {
+      mShadowDrawable = null;
+    }
+
+    setWillNotDraw(false);
+
+    mDragHelper = ViewDragHelper.create(this, 0.5f, scrollerInterpolator, new DragHelperCallback());
+    mDragHelper.setMinVelocity(mMinFlingVelocity * density);
+
+    mIsTouchEnabled = true;
+  }
+
+  private static boolean hasOpaqueBackground(View v) {
+    final Drawable bg = v.getBackground();
+    return bg != null && bg.getOpacity() == PixelFormat.OPAQUE;
+  }
+
+  /**
+   * Set the Drag View after the view is inflated
+   */
+  @Override
+  protected void onFinishInflate() {
+    super.onFinishInflate();
+    if (mDragViewResId != -1) {
+      setDragView(findViewById(mDragViewResId));
+    }
+    if (mAntiDragViewResId != -1) {
+      setAntiDragView(findViewById(mAntiDragViewResId));
+    }
+    if (mScrollableViewResId != -1) {
+      setScrollableView(findViewById(mScrollableViewResId));
+    }
+  }
+
+  public void setGravity(int gravity) {
+    if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) {
+      throw new IllegalArgumentException("gravity must be set to either top or bottom");
+    }
+    mIsSlidingUp = gravity == Gravity.BOTTOM;
+    if (!mFirstLayout) {
+      requestLayout();
+    }
+  }
+
+  /**
+   * @return The ARGB-packed color value used to fade the fixed pane
+   */
+  public int getCoveredFadeColor() {
+    return mCoveredFadeColor;
+  }
+
+  /**
+   * Set the color used to fade the pane covered by the sliding pane out when the pane
+   * will become fully covered in the expanded state.
+   *
+   * @param color An ARGB-packed color value
+   */
+  public void setCoveredFadeColor(int color) {
+    mCoveredFadeColor = color;
+    requestLayout();
+  }
+
+  public boolean isTouchEnabled() {
+    return mIsTouchEnabled && mSlideableView != null && mSlideState != PanelState.HIDDEN;
+  }
+
+  /**
+   * Set sliding enabled flag
+   *
+   * @param enabled flag value
+   */
+  public void setTouchEnabled(boolean enabled) {
+    mIsTouchEnabled = enabled;
+  }
+
+  protected void smoothToBottom() {
+    smoothSlideTo(0, 0);
+  }
+
+  /**
+   * @return The current shadow height
+   */
+  public int getShadowHeight() {
+    return mShadowHeight;
+  }
+
+  /**
+   * Set the shadow height
+   *
+   * @param val A height in pixels
+   */
+  public void setShadowHeight(int val) {
+    mShadowHeight = val;
+    if (!mFirstLayout) {
+      invalidate();
+    }
+  }
+
+  /**
+   * @return The current collapsed panel height
+   */
+  public int getPanelHeight() {
+    return mPanelHeight;
+  }
+
+  /**
+   * Set the collapsed panel height in pixels
+   *
+   * @param val A height in pixels
+   */
+  public void setPanelHeight(int val) {
+    if (getPanelHeight() == val) {
+      return;
+    }
+
+    mPanelHeight = val;
+    if (!mFirstLayout) {
+      requestLayout();
+    }
+
+    if (getPanelState() == PanelState.COLLAPSED) {
+      smoothToBottom();
+      invalidate();
+      return;
+    }
+  }
+
+  /**
+   * @return The current parallax offset
+   */
+  public int getCurrentParallaxOffset() {
+    // Clamp slide offset at zero for parallax computation;
+    int offset = (int) (mParallaxOffset * Math.max(mSlideOffset, 0));
+    return mIsSlidingUp ? -offset : offset;
+  }
+
+  /**
+   * Set parallax offset for the panel
+   *
+   * @param val A height in pixels
+   */
+  public void setParallaxOffset(int val) {
+    mParallaxOffset = val;
+    if (!mFirstLayout) {
+      requestLayout();
+    }
+  }
+
+  /**
+   * @return The current minimin fling velocity
+   */
+  public int getMinFlingVelocity() {
+    return mMinFlingVelocity;
+  }
+
+  /**
+   * Sets the minimum fling velocity for the panel
+   *
+   * @param val the new value
+   */
+  public void setMinFlingVelocity(int val) {
+    mMinFlingVelocity = val;
+  }
+
+  /**
+   * Adds a panel slide listener
+   *
+   * @param listener
+   */
+  public void addPanelSlideListener(PanelSlideListener listener) {
+    synchronized (mPanelSlideListeners) {
+      mPanelSlideListeners.add(listener);
+    }
+  }
+
+  /**
+   * Removes a panel slide listener
+   *
+   * @param listener
+   */
+  public void removePanelSlideListener(PanelSlideListener listener) {
+    synchronized (mPanelSlideListeners) {
+      mPanelSlideListeners.remove(listener);
+    }
+  }
+
+  /**
+   * Provides an on click for the portion of the main view that is dimmed. The listener is not
+   * triggered if the panel is in a collapsed or a hidden position. If the on click listener is
+   * not provided, the clicks on the dimmed area are passed through to the main layout.
+   *
+   * @param listener
+   */
+  public void setFadeOnClickListener(View.OnClickListener listener) {
+    mFadeOnClickListener = listener;
+  }
+
+  /**
+   * Set the draggable view portion. Use to null, to allow the whole panel to be draggable
+   *
+   * @param dragView A view that will be used to drag the panel.
+   */
+  public void setDragView(View dragView) {
+    if (mDragView != null) {
+      mDragView.setOnClickListener(null);
+    }
+    mDragView = dragView;
+    if (mDragView != null) {
+      mDragView.setClickable(true);
+      mDragView.setFocusable(false);
+      mDragView.setFocusableInTouchMode(false);
+      mDragView.setOnClickListener(new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+          if (!isEnabled() || !isTouchEnabled()) return;
+          if (mSlideState != PanelState.EXPANDED && mSlideState != PanelState.ANCHORED) {
+            if (mAnchorPoint < 1.0f) {
+              setPanelState(PanelState.ANCHORED);
+            } else {
+              setPanelState(PanelState.EXPANDED);
+            }
+          } else {
+            setPanelState(PanelState.COLLAPSED);
+          }
+        }
+      });
+      ;
+    }
+  }
+
+  /**
+   * Set the draggable view portion. Use to null, to allow the whole panel to be draggable
+   *
+   * @param dragViewResId The resource ID of the new drag view
+   */
+  public void setDragView(int dragViewResId) {
+    mDragViewResId = dragViewResId;
+    setDragView(findViewById(dragViewResId));
+  }
+
+  /**
+   * Set a portion of the view that shall not be draggable. Use null to make everything draggable
+   *
+   * @param antiDragViewResId The resource ID of the anti drag view
+   */
+  public void setAntiDragView(int antiDragViewResId) {
+    mAntiDragViewResId = antiDragViewResId;
+    setAntiDragView(findViewById(antiDragViewResId));
+  }
+
+  /**
+   * Set a portion of the view that shall not be draggable. Use null to make everything draggable
+   *
+   * @param antiDragView A view that will be used to drag the panel.
+   */
+  public void setAntiDragView(View antiDragView) {
+    this.mAntiDragView = antiDragView;
+  }
+
+  /**
+   * Set the scrollable child of the sliding layout. If set, scrolling will be transfered between
+   * the panel and the view when necessary
+   *
+   * @param scrollableView The scrollable view
+   */
+  public void setScrollableView(View scrollableView) {
+    mScrollableView = scrollableView;
+  }
+
+  /**
+   * Sets the current scrollable view helper. See ScrollableViewHelper description for details.
+   *
+   * @param helper
+   */
+  public void setScrollableViewHelper(ScrollableViewHelper helper) {
+    mScrollableViewHelper = helper;
+  }
+
+  /**
+   * Gets the currently set anchor point
+   *
+   * @return the currently set anchor point
+   */
+  public float getAnchorPoint() {
+    return mAnchorPoint;
+  }
+
+  /**
+   * Set an anchor point where the panel can stop during sliding
+   *
+   * @param anchorPoint A value between 0 and 1, determining the position of the anchor point
+   *                    starting from the top of the layout.
+   */
+  public void setAnchorPoint(float anchorPoint) {
+    if (anchorPoint > 0 && anchorPoint <= 1) {
+      mAnchorPoint = anchorPoint;
+      mFirstLayout = true;
+      requestLayout();
+    }
+  }
+
+  /**
+   * Check if the panel is set as an overlay.
+   */
+  public boolean isOverlayed() {
+    return mOverlayContent;
+  }
+
+  /**
+   * Sets whether or not the panel overlays the content
+   *
+   * @param overlayed
+   */
+  public void setOverlayed(boolean overlayed) {
+    mOverlayContent = overlayed;
+  }
+
+  /**
+   * Check whether or not the main content is clipped to the top of the panel
+   */
+  public boolean isClipPanel() {
+    return mClipPanel;
+  }
+
+  /**
+   * Sets whether or not the main content is clipped to the top of the panel
+   *
+   * @param clip
+   */
+  public void setClipPanel(boolean clip) {
+    mClipPanel = clip;
+  }
+
+  void dispatchOnPanelSlide(View panel) {
+    synchronized (mPanelSlideListeners) {
+      for (PanelSlideListener l : mPanelSlideListeners) {
+        l.onPanelSlide(panel, mSlideOffset);
+      }
+    }
+  }
+
+  void dispatchOnPanelStateChanged(View panel, PanelState previousState, PanelState newState) {
+    synchronized (mPanelSlideListeners) {
+      for (PanelSlideListener l : mPanelSlideListeners) {
+        l.onPanelStateChanged(panel, previousState, newState);
+      }
+    }
+    sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+  }
+
+  void updateObscuredViewVisibility() {
+    if (getChildCount() == 0) {
+      return;
+    }
+    final int leftBound = getPaddingLeft();
+    final int rightBound = getWidth() - getPaddingRight();
+    final int topBound = getPaddingTop();
+    final int bottomBound = getHeight() - getPaddingBottom();
+    final int left;
+    final int right;
+    final int top;
+    final int bottom;
+    if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) {
+      left = mSlideableView.getLeft();
+      right = mSlideableView.getRight();
+      top = mSlideableView.getTop();
+      bottom = mSlideableView.getBottom();
+    } else {
+      left = right = top = bottom = 0;
+    }
+    View child = getChildAt(0);
+    final int clampedChildLeft = Math.max(leftBound, child.getLeft());
+    final int clampedChildTop = Math.max(topBound, child.getTop());
+    final int clampedChildRight = Math.min(rightBound, child.getRight());
+    final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
+    final int vis;
+    if (clampedChildLeft >= left && clampedChildTop >= top &&
+      clampedChildRight <= right && clampedChildBottom <= bottom) {
+      vis = INVISIBLE;
+    } else {
+      vis = VISIBLE;
+    }
+    child.setVisibility(vis);
+  }
+
+  void setAllChildrenVisible() {
+    for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
+      final View child = getChildAt(i);
+      if (child.getVisibility() == INVISIBLE) {
+        child.setVisibility(VISIBLE);
+      }
+    }
+  }
+
+  @Override
+  protected void onAttachedToWindow() {
+    super.onAttachedToWindow();
+    mFirstLayout = true;
+  }
+
+  @Override
+  protected void onDetachedFromWindow() {
+    super.onDetachedFromWindow();
+    mFirstLayout = true;
+  }
+
+  @Override
+  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+    if (widthMode != MeasureSpec.EXACTLY && widthMode != MeasureSpec.AT_MOST) {
+      throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+    } else if (heightMode != MeasureSpec.EXACTLY && heightMode != MeasureSpec.AT_MOST) {
+      throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
+    }
+
+    final int childCount = getChildCount();
+
+    if (childCount != 2) {
+      throw new IllegalStateException("Sliding up panel layout must have exactly 2 children!");
+    }
+
+    mMainView = getChildAt(0);
+    mSlideableView = getChildAt(1);
+    if (mDragView == null) {
+      setDragView(mSlideableView);
+    }
+
+    // If the sliding panel is not visible, then put the whole view in the hidden state
+    if (mSlideableView.getVisibility() != VISIBLE) {
+      mSlideState = PanelState.HIDDEN;
+    }
+
+    int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
+    int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
+
+    // First pass. Measure based on child LayoutParams width/height.
+    for (int i = 0; i < childCount; i++) {
+      final View child = getChildAt(i);
+      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+      // We always measure the sliding panel in order to know it's height (needed for show panel)
+      if (child.getVisibility() == GONE && i == 0) {
+        continue;
+      }
+
+      int height = layoutHeight;
+      int width = layoutWidth;
+      if (child == mMainView) {
+        if (!mOverlayContent && mSlideState != PanelState.HIDDEN) {
+          height -= mPanelHeight;
+        }
+
+        width -= lp.leftMargin + lp.rightMargin;
+      } else if (child == mSlideableView) {
+        // The slideable view should be aware of its top margin.
+        // See https://github.com/umano/AndroidSlidingUpPanel/issues/412.
+        height -= lp.topMargin;
+      }
+
+      int childWidthSpec;
+      if (lp.width == LayoutParams.WRAP_CONTENT) {
+        childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST);
+      } else if (lp.width == LayoutParams.MATCH_PARENT) {
+        childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+      } else {
+        childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
+      }
+
+      int childHeightSpec;
+      if (lp.height == LayoutParams.WRAP_CONTENT) {
+        childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+      } else {
+        // Modify the height based on the weight.
+        if (lp.weight > 0 && lp.weight < 1) {
+          height = (int) (height * lp.weight);
+        } else if (lp.height != LayoutParams.MATCH_PARENT) {
+          height = lp.height;
+        }
+        childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+      }
+
+      child.measure(childWidthSpec, childHeightSpec);
+
+      if (child == mSlideableView) {
+        mSlideRange = mSlideableView.getMeasuredHeight() - mPanelHeight;
+      }
+    }
+
+    setMeasuredDimension(widthSize, heightSize);
+  }
+
+  @Override
+  protected void onLayout(boolean changed, int l, int t, int r, int b) {
+    final int paddingLeft = getPaddingLeft();
+    final int paddingTop = getPaddingTop();
+
+    final int childCount = getChildCount();
+
+    if (mFirstLayout) {
+      switch (mSlideState) {
+        case EXPANDED:
+          mSlideOffset = 1.0f;
+          break;
+        case ANCHORED:
+          mSlideOffset = mAnchorPoint;
+          break;
+        case HIDDEN:
+          int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight);
+          mSlideOffset = computeSlideOffset(newTop);
+          break;
+        default:
+          mSlideOffset = 0.f;
+          break;
+      }
+    }
+
+    for (int i = 0; i < childCount; i++) {
+      final View child = getChildAt(i);
+      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+      // Always layout the sliding view on the first layout
+      if (child.getVisibility() == GONE && (i == 0 || mFirstLayout)) {
+        continue;
+      }
+
+      final int childHeight = child.getMeasuredHeight();
+      int childTop = paddingTop;
+
+      if (child == mSlideableView) {
+        childTop = computePanelTopPosition(mSlideOffset);
+      }
+
+      if (!mIsSlidingUp) {
+        if (child == mMainView && !mOverlayContent) {
+          childTop = computePanelTopPosition(mSlideOffset) + mSlideableView.getMeasuredHeight();
+        }
+      }
+      final int childBottom = childTop + childHeight;
+      final int childLeft = paddingLeft + lp.leftMargin;
+      final int childRight = childLeft + child.getMeasuredWidth();
+
+      child.layout(childLeft, childTop, childRight, childBottom);
+    }
+
+    if (mFirstLayout) {
+      updateObscuredViewVisibility();
+    }
+    applyParallaxForCurrentSlideOffset();
+
+    mFirstLayout = false;
+  }
+
+  @Override
+  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+    super.onSizeChanged(w, h, oldw, oldh);
+    // Recalculate sliding panes and their details
+    if (h != oldh) {
+      mFirstLayout = true;
+    }
+  }
+
+  @Override
+  public boolean onInterceptTouchEvent(MotionEvent ev) {
+    // If the scrollable view is handling touch, never intercept
+    if (mIsScrollableViewHandlingTouch || !isTouchEnabled()) {
+      mDragHelper.abort();
+      return false;
+    }
+
+    final int action = MotionEventCompat.getActionMasked(ev);
+    final float x = ev.getX();
+    final float y = ev.getY();
+    final float adx = Math.abs(x - mInitialMotionX);
+    final float ady = Math.abs(y - mInitialMotionY);
+    final int dragSlop = mDragHelper.getTouchSlop();
+
+    switch (action) {
+      case MotionEvent.ACTION_DOWN: {
+        mIsUnableToDrag = false;
+        mInitialMotionX = x;
+        mInitialMotionY = y;
+        if (!checkTouchingDragView(mDragView, (int) x, (int) y)) {
+          mDragHelper.cancel();
+          mIsUnableToDrag = true;
+          return false;
+        }
+
+        break;
+      }
+
+      case MotionEvent.ACTION_MOVE: {
+        if (ady > dragSlop && adx > ady) {
+          mDragHelper.cancel();
+          mIsUnableToDrag = true;
+          return false;
+        }
+        break;
+      }
+
+      case MotionEvent.ACTION_CANCEL:
+      case MotionEvent.ACTION_UP:
+        // If the dragView is still dragging when we get here, we need to call processTouchEvent
+        // so that the view is settled
+        // Added to make scrollable views work (tokudu)
+        if (mDragHelper.isDragging()) {
+          mDragHelper.processTouchEvent(ev);
+          return true;
+        }
+        // Check if this was a click on the faded part of the screen, and fire off the listener if there is one.
+        if (ady <= dragSlop
+          && adx <= dragSlop
+          && mSlideOffset > 0 && !checkTouchingDragView(mSlideableView, (int) mInitialMotionX, (int) mInitialMotionY) && mFadeOnClickListener != null) {
+          playSoundEffect(android.view.SoundEffectConstants.CLICK);
+          mFadeOnClickListener.onClick(this);
+          return true;
+        }
+        break;
+    }
+    return mDragHelper.shouldInterceptTouchEvent(ev);
+  }
+
+  @Override
+  public boolean onTouchEvent(MotionEvent ev) {
+    if (!isEnabled() || !isTouchEnabled()) {
+      return super.onTouchEvent(ev);
+    }
+    try {
+      mDragHelper.processTouchEvent(ev);
+      return true;
+    } catch (Exception ex) {
+      // Ignore the pointer out of range exception
+      return false;
+    }
+  }
+
+  @Override
+  public boolean dispatchTouchEvent(MotionEvent ev) {
+    final int action = MotionEventCompat.getActionMasked(ev);
+
+    if (!isEnabled() || !isTouchEnabled() || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
+      mDragHelper.abort();
+      return super.dispatchTouchEvent(ev);
+    }
+
+    final float x = ev.getX();
+    final float y = ev.getY();
+
+    if (action == MotionEvent.ACTION_DOWN) {
+      mIsScrollableViewHandlingTouch = false;
+      mPrevMotionX = x;
+      mPrevMotionY = y;
+    } else if (action == MotionEvent.ACTION_MOVE) {
+      float dx = x - mPrevMotionX;
+      float dy = y - mPrevMotionY;
+      mPrevMotionX = x;
+      mPrevMotionY = y;
+
+      if (Math.abs(dx) > Math.abs(dy)) {
+        // Scrolling horizontally, so ignore
+        return super.dispatchTouchEvent(ev);
+      }
+
+      // If the scroll view isn't under the touch, pass the
+      // event along to the dragView.
+      if (!checkTouchingDragView(mScrollableView, (int) mInitialMotionX, (int) mInitialMotionY)) {
+        return super.dispatchTouchEvent(ev);
+      }
+
+      // Which direction (up or down) is the drag moving?
+      if (dy * (mIsSlidingUp ? 1 : -1) > 0) { // Collapsing
+        // Is the child less than fully scrolled?
+        // Then let the child handle it.
+        if (mScrollableViewHelper.getScrollableViewScrollPosition(mScrollableView, mIsSlidingUp) > 0) {
+          mIsScrollableViewHandlingTouch = true;
+          return super.dispatchTouchEvent(ev);
+        }
+
+        // Was the child handling the touch previously?
+        // Then we need to rejigger things so that the
+        // drag panel gets a proper down event.
+        if (mIsScrollableViewHandlingTouch) {
+          // Send an 'UP' event to the child.
+          MotionEvent up = MotionEvent.obtain(ev);
+          up.setAction(MotionEvent.ACTION_CANCEL);
+          super.dispatchTouchEvent(up);
+          up.recycle();
+
+          // Send a 'DOWN' event to the panel. (We'll cheat
+          // and hijack this one)
+          ev.setAction(MotionEvent.ACTION_DOWN);
+        }
+
+        mIsScrollableViewHandlingTouch = false;
+        return this.onTouchEvent(ev);
+      } else if (dy * (mIsSlidingUp ? 1 : -1) < 0) { // Expanding
+        // Is the panel less than fully expanded?
+        // Then we'll handle the drag here.
+        if (mSlideOffset < 1.0f) {
+          mIsScrollableViewHandlingTouch = false;
+          return this.onTouchEvent(ev);
+        }
+
+        // Was the panel handling the touch previously?
+        // Then we need to rejigger things so that the
+        // child gets a proper down event.
+        if (!mIsScrollableViewHandlingTouch && mDragHelper.isDragging()) {
+          mDragHelper.cancel();
+          ev.setAction(MotionEvent.ACTION_DOWN);
+        }
+
+        mIsScrollableViewHandlingTouch = true;
+        return super.dispatchTouchEvent(ev);
+      }
+    } else if (action == MotionEvent.ACTION_UP) {
+      // If the scrollable view was handling the touch and we receive an up
+      // we want to clear any previous dragging state so we don't intercept a touch stream accidentally
+      if (mIsScrollableViewHandlingTouch) {
+        mDragHelper.setDragState(ViewDragHelper.STATE_IDLE);
+      }
+    }
+
+    // In all other cases, just let the default behavior take over.
+    return super.dispatchTouchEvent(ev);
+  }
+
+  private boolean checkTouchingDragView(View view, int x, int y) {
+    return isViewUnder(view, x, y) && !isViewUnder(mAntiDragView, x, y);
+  }
+
+  private boolean isViewUnder(View view, int x, int y) {
+    if (view == null) return false;
+    int[] viewLocation = new int[2];
+    view.getLocationOnScreen(viewLocation);
+    int[] parentLocation = new int[2];
+    this.getLocationOnScreen(parentLocation);
+    int screenX = parentLocation[0] + x;
+    int screenY = parentLocation[1] + y;
+    return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
+      screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
+  }
+
+  /*
+   * Computes the top position of the panel based on the slide offset.
+   */
+  private int computePanelTopPosition(float slideOffset) {
+    int slidingViewHeight = mSlideableView != null ? mSlideableView.getMeasuredHeight() : 0;
+    int slidePixelOffset = (int) (slideOffset * mSlideRange);
+    // Compute the top of the panel if its collapsed
+    return mIsSlidingUp
+      ? getMeasuredHeight() - getPaddingBottom() - mPanelHeight - slidePixelOffset
+      : getPaddingTop() - slidingViewHeight + mPanelHeight + slidePixelOffset;
+  }
+
+  /*
+   * Computes the slide offset based on the top position of the panel
+   */
+  private float computeSlideOffset(int topPosition) {
+    // Compute the panel top position if the panel is collapsed (offset 0)
+    final int topBoundCollapsed = computePanelTopPosition(0);
+
+    // Determine the new slide offset based on the collapsed top position and the new required
+    // top position
+    return (mIsSlidingUp
+      ? (float) (topBoundCollapsed - topPosition) / mSlideRange
+      : (float) (topPosition - topBoundCollapsed) / mSlideRange);
+  }
+
+  /**
+   * Returns the current state of the panel as an enum.
+   *
+   * @return the current panel state
+   */
+  public PanelState getPanelState() {
+    return mSlideState;
+  }
+
+  /**
+   * Change panel state to the given state with
+   *
+   * @param state - new panel state
+   */
+  public void setPanelState(PanelState state) {
+
+    // Abort any running animation, to allow state change
+    if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_SETTLING) {
+      Log.d(TAG, "View is settling. Aborting animation.");
+      mDragHelper.abort();
+    }
+
+    if (state == null || state == PanelState.DRAGGING) {
+      throw new IllegalArgumentException("Panel state cannot be null or DRAGGING.");
+    }
+    if (!isEnabled()
+      || (!mFirstLayout && mSlideableView == null)
+      || state == mSlideState
+      || mSlideState == PanelState.DRAGGING) return;
+
+    if (mFirstLayout) {
+      setPanelStateInternal(state);
+    } else {
+      if (mSlideState == PanelState.HIDDEN) {
+        mSlideableView.setVisibility(View.VISIBLE);
+        requestLayout();
+      }
+      switch (state) {
+        case ANCHORED:
+          smoothSlideTo(mAnchorPoint, 0);
+          break;
+        case COLLAPSED:
+          smoothSlideTo(0, 0);
+          break;
+        case EXPANDED:
+          smoothSlideTo(1.0f, 0);
+          break;
+        case HIDDEN:
+          int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight);
+          smoothSlideTo(computeSlideOffset(newTop), 0);
+          break;
+      }
+    }
+  }
+
+  private void setPanelStateInternal(PanelState state) {
+    if (mSlideState == state) return;
+    PanelState oldState = mSlideState;
+    mSlideState = state;
+    dispatchOnPanelStateChanged(this, oldState, state);
+  }
+
+  /**
+   * Update the parallax based on the current slide offset.
+   */
+  @SuppressLint("NewApi")
+  private void applyParallaxForCurrentSlideOffset() {
+    if (mParallaxOffset > 0) {
+      int mainViewOffset = getCurrentParallaxOffset();
+      ViewCompat.setTranslationY(mMainView, mainViewOffset);
+    }
+  }
+
+  private void onPanelDragged(int newTop) {
+    if (mSlideState != PanelState.DRAGGING) {
+      mLastNotDraggingSlideState = mSlideState;
+    }
+    setPanelStateInternal(PanelState.DRAGGING);
+    // Recompute the slide offset based on the new top position
+    mSlideOffset = computeSlideOffset(newTop);
+    applyParallaxForCurrentSlideOffset();
+    // Dispatch the slide event
+    dispatchOnPanelSlide(mSlideableView);
+    // If the slide offset is negative, and overlay is not on, we need to increase the
+    // height of the main content
+    LayoutParams lp = (LayoutParams) mMainView.getLayoutParams();
+    int defaultHeight = getHeight() - getPaddingBottom() - getPaddingTop() - mPanelHeight;
+
+    if (mSlideOffset <= 0 && !mOverlayContent) {
+      // expand the main view
+      lp.height = mIsSlidingUp ? (newTop - getPaddingBottom()) : (getHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() - newTop);
+      if (lp.height == defaultHeight) {
+        lp.height = LayoutParams.MATCH_PARENT;
+      }
+      mMainView.requestLayout();
+    } else if (lp.height != LayoutParams.MATCH_PARENT && !mOverlayContent) {
+      lp.height = LayoutParams.MATCH_PARENT;
+      mMainView.requestLayout();
+    }
+  }
+
+  @Override
+  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+    boolean result;
+    final int save = canvas.save();
+
+    if (mSlideableView != null && mSlideableView != child) { // if main view
+      // Clip against the slider; no sense drawing what will immediately be covered,
+      // Unless the panel is set to overlay content
+      canvas.getClipBounds(mTmpRect);
+      if (!mOverlayContent) {
+        if (mIsSlidingUp) {
+          mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop());
+        } else {
+          mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom());
+        }
+      }
+      if (mClipPanel) {
+        canvas.clipRect(mTmpRect);
+      }
+
+      result = super.drawChild(canvas, child, drawingTime);
+
+      if (mCoveredFadeColor != 0 && mSlideOffset > 0) {
+        final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;
+        final int imag = (int) (baseAlpha * mSlideOffset);
+        final int color = imag << 24 | (mCoveredFadeColor & 0xffffff);
+        mCoveredFadePaint.setColor(color);
+        canvas.drawRect(mTmpRect, mCoveredFadePaint);
+      }
+    } else {
+      result = super.drawChild(canvas, child, drawingTime);
+    }
+
+    canvas.restoreToCount(save);
+
+    return result;
+  }
+
+  /**
+   * Smoothly animate mDraggingPane to the target X position within its range.
+   *
+   * @param slideOffset position to animate to
+   * @param velocity    initial velocity in case of fling, or 0.
+   */
+  boolean smoothSlideTo(float slideOffset, int velocity) {
+    if (!isEnabled() || mSlideableView == null) {
+      // Nothing to do.
+      return false;
+    }
+
+    int panelTop = computePanelTopPosition(slideOffset);
+
+    if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), panelTop)) {
+      setAllChildrenVisible();
+      ViewCompat.postInvalidateOnAnimation(this);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void computeScroll() {
+    if (mDragHelper != null && mDragHelper.continueSettling(true)) {
+      if (!isEnabled()) {
+        mDragHelper.abort();
+        return;
+      }
+
+      ViewCompat.postInvalidateOnAnimation(this);
+    }
+  }
+
+  @Override
+  public void draw(Canvas c) {
+    super.draw(c);
+
+    // draw the shadow
+    if (mShadowDrawable != null && mSlideableView != null) {
+      final int right = mSlideableView.getRight();
+      final int top;
+      final int bottom;
+      if (mIsSlidingUp) {
+        top = mSlideableView.getTop() - mShadowHeight;
+        bottom = mSlideableView.getTop();
+      } else {
+        top = mSlideableView.getBottom();
+        bottom = mSlideableView.getBottom() + mShadowHeight;
+      }
+      final int left = mSlideableView.getLeft();
+      mShadowDrawable.setBounds(left, top, right, bottom);
+      mShadowDrawable.draw(c);
+    }
+  }
+
+  /**
+   * Tests scrollability within child views of v given a delta of dx.
+   *
+   * @param v      View to test for horizontal scrollability
+   * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+   *               or just its children (false).
+   * @param dx     Delta scrolled in pixels
+   * @param x      X coordinate of the active touch point
+   * @param y      Y coordinate of the active touch point
+   * @return true if child views of v can be scrolled by delta of dx.
+   */
+  protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+    if (v instanceof ViewGroup) {
+      final ViewGroup group = (ViewGroup) v;
+      final int scrollX = v.getScrollX();
+      final int scrollY = v.getScrollY();
+      final int count = group.getChildCount();
+      // Count backwards - let topmost views consume scroll distance first.
+      for (int i = count - 1; i >= 0; i--) {
+        final View child = group.getChildAt(i);
+        if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+          y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+          canScroll(child, true, dx, x + scrollX - child.getLeft(),
+            y + scrollY - child.getTop())) {
+          return true;
+        }
+      }
+    }
+    return checkV && ViewCompat.canScrollHorizontally(v, -dx);
+  }
+
+  @Override
+  protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+    return new LayoutParams();
+  }
+
+  @Override
+  protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+    return p instanceof MarginLayoutParams
+      ? new LayoutParams((MarginLayoutParams) p)
+      : new LayoutParams(p);
+  }
+
+  @Override
+  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+    return p instanceof LayoutParams && super.checkLayoutParams(p);
+  }
+
+  @Override
+  public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+    return new LayoutParams(getContext(), attrs);
+  }
+
+  @Override
+  public Parcelable onSaveInstanceState() {
+    Bundle bundle = new Bundle();
+    bundle.putParcelable("superState", super.onSaveInstanceState());
+    bundle.putSerializable(SLIDING_STATE, mSlideState != PanelState.DRAGGING ? mSlideState : mLastNotDraggingSlideState);
+    return bundle;
+  }
+
+  @Override
+  public void onRestoreInstanceState(Parcelable state) {
+    if (state instanceof Bundle) {
+      Bundle bundle = (Bundle) state;
+      mSlideState = (PanelState) bundle.getSerializable(SLIDING_STATE);
+      mSlideState = mSlideState == null ? DEFAULT_SLIDE_STATE : mSlideState;
+      state = bundle.getParcelable("superState");
+    }
+    super.onRestoreInstanceState(state);
+  }
+
+  /**
+   * Current state of the slideable view.
+   */
+  public enum PanelState {
+    EXPANDED,
+    COLLAPSED,
+    ANCHORED,
+    HIDDEN,
+    DRAGGING
+  }
+
+  /**
+   * Listener for monitoring events about sliding panes.
+   */
+  public interface PanelSlideListener {
+    /**
+     * Called when a sliding pane's position changes.
+     *
+     * @param panel       The child view that was moved
+     * @param slideOffset The new offset of this sliding pane within its range, from 0-1
+     */
+    public void onPanelSlide(View panel, float slideOffset);
+
+    /**
+     * Called when a sliding panel state changes
+     *
+     * @param panel The child view that was slid to an collapsed position
+     */
+    public void onPanelStateChanged(View panel, PanelState previousState, PanelState newState);
+  }
+
+  /**
+   * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset
+   * of the listener methods you can extend this instead of implement the full interface.
+   */
+  public static class SimplePanelSlideListener implements PanelSlideListener {
+    @Override
+    public void onPanelSlide(View panel, float slideOffset) {
+    }
+
+    @Override
+    public void onPanelStateChanged(View panel, PanelState previousState, PanelState newState) {
+    }
+  }
+
+  public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+    private static final int[] ATTRS = new int[]{
+      android.R.attr.layout_weight
+    };
+
+    public float weight = 0;
+
+    public LayoutParams() {
+      super(MATCH_PARENT, MATCH_PARENT);
+    }
+
+    public LayoutParams(int width, int height) {
+      super(width, height);
+    }
+
+    public LayoutParams(int width, int height, float weight) {
+      super(width, height);
+      this.weight = weight;
+    }
+
+    public LayoutParams(android.view.ViewGroup.LayoutParams source) {
+      super(source);
+    }
+
+    public LayoutParams(MarginLayoutParams source) {
+      super(source);
+    }
+
+    public LayoutParams(LayoutParams source) {
+      super(source);
+    }
+
+    public LayoutParams(Context c, AttributeSet attrs) {
+      super(c, attrs);
+
+      final TypedArray ta = c.obtainStyledAttributes(attrs, ATTRS);
+      if (ta != null) {
+        this.weight = ta.getFloat(0, 0);
+        ta.recycle();
+      }
+
+
+    }
+  }
+
+  private class DragHelperCallback extends ViewDragHelper.Callback {
+
+    @Override
+    public boolean tryCaptureView(View child, int pointerId) {
+      return !mIsUnableToDrag && child == mSlideableView;
+
+    }
+
+    @Override
+    public void onViewDragStateChanged(int state) {
+      if (mDragHelper != null && mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
+        mSlideOffset = computeSlideOffset(mSlideableView.getTop());
+        applyParallaxForCurrentSlideOffset();
+
+        if (mSlideOffset == 1) {
+          updateObscuredViewVisibility();
+          setPanelStateInternal(PanelState.EXPANDED);
+        } else if (mSlideOffset == 0) {
+          setPanelStateInternal(PanelState.COLLAPSED);
+        } else if (mSlideOffset < 0) {
+          setPanelStateInternal(PanelState.HIDDEN);
+          mSlideableView.setVisibility(View.INVISIBLE);
+        } else {
+          updateObscuredViewVisibility();
+          setPanelStateInternal(PanelState.ANCHORED);
+        }
+      }
+    }
+
+    @Override
+    public void onViewCaptured(View capturedChild, int activePointerId) {
+      setAllChildrenVisible();
+    }
+
+    @Override
+    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+      onPanelDragged(top);
+      invalidate();
+    }
+
+    @Override
+    public void onViewReleased(View releasedChild, float xvel, float yvel) {
+      int target = 0;
+
+      // direction is always positive if we are sliding in the expanded direction
+      float direction = mIsSlidingUp ? -yvel : yvel;
+
+      if (direction > 0 && mSlideOffset <= mAnchorPoint) {
+        // swipe up -> expand and stop at anchor point
+        target = computePanelTopPosition(mAnchorPoint);
+      } else if (direction > 0 && mSlideOffset > mAnchorPoint) {
+        // swipe up past anchor -> expand
+        target = computePanelTopPosition(1.0f);
+      } else if (direction < 0 && mSlideOffset >= mAnchorPoint) {
+        // swipe down -> collapse and stop at anchor point
+        target = computePanelTopPosition(mAnchorPoint);
+      } else if (direction < 0 && mSlideOffset < mAnchorPoint) {
+        // swipe down past anchor -> collapse
+        target = computePanelTopPosition(0.0f);
+      } else if (mSlideOffset >= (1.f + mAnchorPoint) / 2) {
+        // zero velocity, and far enough from anchor point => expand to the top
+        target = computePanelTopPosition(1.0f);
+      } else if (mSlideOffset >= mAnchorPoint / 2) {
+        // zero velocity, and close enough to anchor point => go to anchor
+        target = computePanelTopPosition(mAnchorPoint);
+      } else {
+        // settle at the bottom
+        target = computePanelTopPosition(0.0f);
+      }
+
+      if (mDragHelper != null) {
+        mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), target);
+      }
+      invalidate();
+    }
+
+    @Override
+    public int getViewVerticalDragRange(View child) {
+      return mSlideRange;
+    }
+
+    @Override
+    public int clampViewPositionVertical(View child, int top, int dy) {
+      final int collapsedTop = computePanelTopPosition(0.f);
+      final int expandedTop = computePanelTopPosition(1.0f);
+      if (mIsSlidingUp) {
+        return Math.min(Math.max(top, expandedTop), collapsedTop);
+      } else {
+        return Math.min(Math.max(top, collapsedTop), expandedTop);
+      }
+    }
+  }
+}
diff --git a/slidingpanel/src/main/java/com/sothree/slidinguppanel/ViewDragHelper.java b/slidingpanel/src/main/java/com/sothree/slidinguppanel/ViewDragHelper.java
new file mode 100644
index 000000000..cea894fbe
--- /dev/null
+++ b/slidingpanel/src/main/java/com/sothree/slidinguppanel/ViewDragHelper.java
@@ -0,0 +1,1481 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.sothree.slidinguppanel;
+
+import android.content.Context;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ScrollerCompat;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import java.util.Arrays;
+
+/**
+ * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
+ * of useful operations and state tracking for allowing a user to drag and reposition
+ * views within their parent ViewGroup.
+ */
+public class ViewDragHelper {
+  /**
+   * A null/invalid pointer ID.
+   */
+  public static final int INVALID_POINTER = -1;
+  /**
+   * A view is not currently being dragged or animating as a result of a fling/snap.
+   */
+  public static final int STATE_IDLE = 0;
+  /**
+   * A view is currently being dragged. The position is currently changing as a result
+   * of user input or simulated user input.
+   */
+  public static final int STATE_DRAGGING = 1;
+  /**
+   * A view is currently settling into place as a result of a fling or
+   * predefined non-interactive motion.
+   */
+  public static final int STATE_SETTLING = 2;
+  /**
+   * Edge flag indicating that the left edge should be affected.
+   */
+  public static final int EDGE_LEFT = 1 << 0;
+  /**
+   * Edge flag indicating that the right edge should be affected.
+   */
+  public static final int EDGE_RIGHT = 1 << 1;
+  /**
+   * Edge flag indicating that the top edge should be affected.
+   */
+  public static final int EDGE_TOP = 1 << 2;
+  /**
+   * Edge flag indicating that the bottom edge should be affected.
+   */
+  public static final int EDGE_BOTTOM = 1 << 3;
+  /**
+   * Edge flag set indicating all edges should be affected.
+   */
+  public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
+  /**
+   * Indicates that a check should occur along the horizontal axis
+   */
+  public static final int DIRECTION_HORIZONTAL = 1 << 0;
+  /**
+   * Indicates that a check should occur along the vertical axis
+   */
+  public static final int DIRECTION_VERTICAL = 1 << 1;
+  /**
+   * Indicates that a check should occur along all axes
+   */
+  public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
+  private static final String TAG = "ViewDragHelper";
+  private static final int EDGE_SIZE = 20; // dp
+
+  private static final int BASE_SETTLE_DURATION = 256; // ms
+  private static final int MAX_SETTLE_DURATION = 600; // ms
+  /**
+   * Interpolator defining the animation curve for mScroller
+   */
+  private static final Interpolator sInterpolator = new Interpolator() {
+    public float getInterpolation(float t) {
+      t -= 1.0f;
+      return t * t * t * t * t + 1.0f;
+    }
+  };
+  private final Callback mCallback;
+  private final ViewGroup mParentView;
+  // Current drag state; idle, dragging or settling
+  private int mDragState;
+  // Distance to travel before a drag may begin
+  private int mTouchSlop;
+  // Last known position/pointer tracking
+  private int mActivePointerId = INVALID_POINTER;
+  private float[] mInitialMotionX;
+  private float[] mInitialMotionY;
+  private float[] mLastMotionX;
+  private float[] mLastMotionY;
+  private int[] mInitialEdgesTouched;
+  private int[] mEdgeDragsInProgress;
+  private int[] mEdgeDragsLocked;
+  private int mPointersDown;
+  private VelocityTracker mVelocityTracker;
+  private float mMaxVelocity;
+  private float mMinVelocity;
+  private int mEdgeSize;
+  private int mTrackingEdges;
+  private ScrollerCompat mScroller;
+  private View mCapturedView;
+  private final Runnable mSetIdleRunnable = new Runnable() {
+    public void run() {
+      setDragState(STATE_IDLE);
+    }
+  };
+  private boolean mReleaseInProgress;
+
+  /**
+   * Apps should use ViewDragHelper.create() to get a new instance.
+   * This will allow VDH to use internal compatibility implementations for different
+   * platform versions.
+   * If the interpolator is null, the default interpolator will be used.
+   *
+   * @param context      Context to initialize config-dependent params from
+   * @param forParent    Parent view to monitor
+   * @param interpolator interpolator for scroller
+   */
+  private ViewDragHelper(Context context, ViewGroup forParent, Interpolator interpolator, Callback cb) {
+    if (forParent == null) {
+      throw new IllegalArgumentException("Parent view may not be null");
+    }
+    if (cb == null) {
+      throw new IllegalArgumentException("Callback may not be null");
+    }
+
+    mParentView = forParent;
+    mCallback = cb;
+
+    final ViewConfiguration vc = ViewConfiguration.get(context);
+    final float density = context.getResources().getDisplayMetrics().density;
+    mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
+
+    mTouchSlop = vc.getScaledTouchSlop();
+    mMaxVelocity = vc.getScaledMaximumFlingVelocity();
+    mMinVelocity = vc.getScaledMinimumFlingVelocity();
+    mScroller = ScrollerCompat.create(context, interpolator != null ? interpolator : sInterpolator);
+  }
+
+  /**
+   * Factory method to create a new ViewDragHelper.
+   *
+   * @param forParent Parent view to monitor
+   * @param cb        Callback to provide information and receive events
+   * @return a new ViewDragHelper instance
+   */
+  public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
+    return new ViewDragHelper(forParent.getContext(), forParent, null, cb);
+  }
+
+  /**
+   * Factory method to create a new ViewDragHelper with the specified interpolator.
+   *
+   * @param forParent    Parent view to monitor
+   * @param interpolator interpolator for scroller
+   * @param cb           Callback to provide information and receive events
+   * @return a new ViewDragHelper instance
+   */
+  public static ViewDragHelper create(ViewGroup forParent, Interpolator interpolator, Callback cb) {
+    return new ViewDragHelper(forParent.getContext(), forParent, interpolator, cb);
+  }
+
+  /**
+   * Factory method to create a new ViewDragHelper.
+   *
+   * @param forParent   Parent view to monitor
+   * @param sensitivity Multiplier for how sensitive the helper should be about detecting
+   *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
+   * @param cb          Callback to provide information and receive events
+   * @return a new ViewDragHelper instance
+   */
+  public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
+    final ViewDragHelper helper = create(forParent, cb);
+    helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
+    return helper;
+  }
+
+  /**
+   * Factory method to create a new ViewDragHelper with the specified interpolator.
+   *
+   * @param forParent    Parent view to monitor
+   * @param sensitivity  Multiplier for how sensitive the helper should be about detecting
+   *                     the start of a drag. Larger values are more sensitive. 1.0f is normal.
+   * @param interpolator interpolator for scroller
+   * @param cb           Callback to provide information and receive events
+   * @return a new ViewDragHelper instance
+   */
+  public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Interpolator interpolator, Callback cb) {
+    final ViewDragHelper helper = create(forParent, interpolator, cb);
+    helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
+    return helper;
+  }
+
+  /**
+   * Return the currently configured minimum velocity. Any flings with a magnitude less
+   * than this value in pixels per second. Callback methods accepting a velocity will receive
+   * zero as a velocity value if the real detected velocity was below this threshold.
+   *
+   * @return the minimum velocity that will be detected
+   */
+  public float getMinVelocity() {
+    return mMinVelocity;
+  }
+
+  /**
+   * Set the minimum velocity that will be detected as having a magnitude greater than zero
+   * in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
+   *
+   * @param minVel Minimum velocity to detect
+   */
+  public void setMinVelocity(float minVel) {
+    mMinVelocity = minVel;
+  }
+
+  /**
+   * Retrieve the current drag state of this helper. This will return one of
+   * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
+   *
+   * @return The current drag state
+   */
+  public int getViewDragState() {
+    return mDragState;
+  }
+
+  /**
+   * Enable edge tracking for the selected edges of the parent view.
+   * The callback's {@link Callback#onEdgeTouched(int, int)} and
+   * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked
+   * for edges for which edge tracking has been enabled.
+   *
+   * @param edgeFlags Combination of edge flags describing the edges to watch
+   * @see #EDGE_LEFT
+   * @see #EDGE_TOP
+   * @see #EDGE_RIGHT
+   * @see #EDGE_BOTTOM
+   */
+  public void setEdgeTrackingEnabled(int edgeFlags) {
+    mTrackingEdges = edgeFlags;
+  }
+
+  /**
+   * Return the size of an edge. This is the range in pixels along the edges of this view
+   * that will actively detect edge touches or drags if edge tracking is enabled.
+   *
+   * @return The size of an edge in pixels
+   * @see #setEdgeTrackingEnabled(int)
+   */
+  public int getEdgeSize() {
+    return mEdgeSize;
+  }
+
+  /**
+   * Capture a specific child view for dragging within the parent. The callback will be notified
+   * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
+   * capture this view.
+   *
+   * @param childView       Child view to capture
+   * @param activePointerId ID of the pointer that is dragging the captured child view
+   */
+  public void captureChildView(View childView, int activePointerId) {
+    if (childView.getParent() != mParentView) {
+      throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
+        "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
+    }
+
+    mCapturedView = childView;
+    mActivePointerId = activePointerId;
+    mCallback.onViewCaptured(childView, activePointerId);
+    setDragState(STATE_DRAGGING);
+  }
+
+  /**
+   * @return The currently captured view, or null if no view has been captured.
+   */
+  public View getCapturedView() {
+    return mCapturedView;
+  }
+
+  /**
+   * @return The ID of the pointer currently dragging the captured view,
+   * or {@link #INVALID_POINTER}.
+   */
+  public int getActivePointerId() {
+    return mActivePointerId;
+  }
+
+  /**
+   * @return The minimum distance in pixels that the user must travel to initiate a drag
+   */
+  public int getTouchSlop() {
+    return mTouchSlop;
+  }
+
+  /**
+   * The result of a call to this method is equivalent to
+   * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
+   */
+  public void cancel() {
+    mActivePointerId = INVALID_POINTER;
+    clearMotionHistory();
+
+    if (mVelocityTracker != null) {
+      mVelocityTracker.recycle();
+      mVelocityTracker = null;
+    }
+  }
+
+  /**
+   * {@link #cancel()}, but also abort all motion in progress and snap to the end of any
+   * animation.
+   */
+  public void abort() {
+    cancel();
+    if (mDragState == STATE_SETTLING) {
+      final int oldX = mScroller.getCurrX();
+      final int oldY = mScroller.getCurrY();
+      mScroller.abortAnimation();
+      final int newX = mScroller.getCurrX();
+      final int newY = mScroller.getCurrY();
+      mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
+    }
+    setDragState(STATE_IDLE);
+  }
+
+  /**
+   * Animate the view <code>child</code> to the given (left, top) position.
+   * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
+   * on each subsequent frame to continue the motion until it returns false. If this method
+   * returns false there is no further work to do to complete the movement.
+   * <p>
+   * <p>This operation does not count as a capture event, though {@link #getCapturedView()}
+   * will still report the sliding view while the slide is in progress.</p>
+   *
+   * @param child     Child view to capture and animate
+   * @param finalLeft Final left position of child
+   * @param finalTop  Final top position of child
+   * @return true if animation should continue through {@link #continueSettling(boolean)} calls
+   */
+  public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
+    mCapturedView = child;
+    mActivePointerId = INVALID_POINTER;
+
+    return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
+  }
+
+  /**
+   * Settle the captured view at the given (left, top) position.
+   * The appropriate velocity from prior motion will be taken into account.
+   * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
+   * on each subsequent frame to continue the motion until it returns false. If this method
+   * returns false there is no further work to do to complete the movement.
+   *
+   * @param finalLeft Settled left edge position for the captured view
+   * @param finalTop  Settled top edge position for the captured view
+   * @return true if animation should continue through {@link #continueSettling(boolean)} calls
+   */
+  public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
+    if (!mReleaseInProgress) {
+      throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
+        "Callback#onViewReleased");
+    }
+
+    return forceSettleCapturedViewAt(finalLeft, finalTop,
+      (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
+      (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
+  }
+
+  /**
+   * Settle the captured view at the given (left, top) position.
+   *
+   * @param finalLeft Target left position for the captured view
+   * @param finalTop  Target top position for the captured view
+   * @param xvel      Horizontal velocity
+   * @param yvel      Vertical velocity
+   * @return true if animation should continue through {@link #continueSettling(boolean)} calls
+   */
+  private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
+    final int startLeft = mCapturedView.getLeft();
+    final int startTop = mCapturedView.getTop();
+    final int dx = finalLeft - startLeft;
+    final int dy = finalTop - startTop;
+
+    if (dx == 0 && dy == 0) {
+      // Nothing to do. Send callbacks, be done.
+      mScroller.abortAnimation();
+      setDragState(STATE_IDLE);
+      return false;
+    }
+
+    final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
+    mScroller.startScroll(startLeft, startTop, dx, dy, duration);
+
+    setDragState(STATE_SETTLING);
+    return true;
+  }
+
+  private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
+    xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
+    yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
+    final int absDx = Math.abs(dx);
+    final int absDy = Math.abs(dy);
+    final int absXVel = Math.abs(xvel);
+    final int absYVel = Math.abs(yvel);
+    final int addedVel = absXVel + absYVel;
+    final int addedDistance = absDx + absDy;
+
+    final float xweight = xvel != 0 ? (float) absXVel / addedVel :
+      (float) absDx / addedDistance;
+    final float yweight = yvel != 0 ? (float) absYVel / addedVel :
+      (float) absDy / addedDistance;
+
+    int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
+    int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
+
+    return (int) (xduration * xweight + yduration * yweight);
+  }
+
+  private int computeAxisDuration(int delta, int velocity, int motionRange) {
+    if (delta == 0) {
+      return 0;
+    }
+
+    final int width = mParentView.getWidth();
+    final int halfWidth = width / 2;
+    final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
+    final float distance = halfWidth + halfWidth *
+      distanceInfluenceForSnapDuration(distanceRatio);
+
+    int duration;
+    velocity = Math.abs(velocity);
+    if (velocity > 0) {
+      duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+    } else {
+      final float range = (float) Math.abs(delta) / motionRange;
+      duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
+    }
+    return Math.min(duration, MAX_SETTLE_DURATION);
+  }
+
+  /**
+   * Clamp the magnitude of value for absMin and absMax.
+   * If the value is below the minimum, it will be clamped to zero.
+   * If the value is above the maximum, it will be clamped to the maximum.
+   *
+   * @param value  Value to clamp
+   * @param absMin Absolute value of the minimum significant value to return
+   * @param absMax Absolute value of the maximum value to return
+   * @return The clamped value with the same sign as <code>value</code>
+   */
+  private int clampMag(int value, int absMin, int absMax) {
+    final int absValue = Math.abs(value);
+    if (absValue < absMin) return 0;
+    if (absValue > absMax) return value > 0 ? absMax : -absMax;
+    return value;
+  }
+
+  /**
+   * Clamp the magnitude of value for absMin and absMax.
+   * If the value is below the minimum, it will be clamped to zero.
+   * If the value is above the maximum, it will be clamped to the maximum.
+   *
+   * @param value  Value to clamp
+   * @param absMin Absolute value of the minimum significant value to return
+   * @param absMax Absolute value of the maximum value to return
+   * @return The clamped value with the same sign as <code>value</code>
+   */
+  private float clampMag(float value, float absMin, float absMax) {
+    final float absValue = Math.abs(value);
+    if (absValue < absMin) return 0;
+    if (absValue > absMax) return value > 0 ? absMax : -absMax;
+    return value;
+  }
+
+  private float distanceInfluenceForSnapDuration(float f) {
+    f -= 0.5f; // center the values about 0.
+    f *= 0.3f * Math.PI / 2.0f;
+    return (float) Math.sin(f);
+  }
+
+  /**
+   * Settle the captured view based on standard free-moving fling behavior.
+   * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
+   * to continue the motion until it returns false.
+   *
+   * @param minLeft Minimum X position for the view's left edge
+   * @param minTop  Minimum Y position for the view's top edge
+   * @param maxLeft Maximum X position for the view's left edge
+   * @param maxTop  Maximum Y position for the view's top edge
+   */
+  public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
+    if (!mReleaseInProgress) {
+      throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
+        "Callback#onViewReleased");
+    }
+
+    mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
+      (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
+      (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
+      minLeft, maxLeft, minTop, maxTop);
+
+    setDragState(STATE_SETTLING);
+  }
+
+  /**
+   * Move the captured settling view by the appropriate amount for the current time.
+   * If <code>continueSettling</code> returns true, the caller should call it again
+   * on the next frame to continue.
+   *
+   * @param deferCallbacks true if state callbacks should be deferred via posted message.
+   *                       Set this to true if you are calling this method from
+   *                       {@link android.view.View#computeScroll()} or similar methods
+   *                       invoked as part of layout or drawing.
+   * @return true if settle is still in progress
+   */
+  public boolean continueSettling(boolean deferCallbacks) {
+    // Make sure, there is a captured view
+    if (mCapturedView == null) {
+      return false;
+    }
+    if (mDragState == STATE_SETTLING) {
+      boolean keepGoing = mScroller.computeScrollOffset();
+      final int x = mScroller.getCurrX();
+      final int y = mScroller.getCurrY();
+      final int dx = x - mCapturedView.getLeft();
+      final int dy = y - mCapturedView.getTop();
+
+      if (!keepGoing && dy != 0) { //fix #525
+        //Invalid drag state
+        mCapturedView.setTop(0);
+        return true;
+      }
+
+      if (dx != 0) {
+        mCapturedView.offsetLeftAndRight(dx);
+      }
+      if (dy != 0) {
+        mCapturedView.offsetTopAndBottom(dy);
+      }
+
+      if (dx != 0 || dy != 0) {
+        mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
+      }
+
+      if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
+        // Close enough. The interpolator/scroller might think we're still moving
+        // but the user sure doesn't.
+        mScroller.abortAnimation();
+        keepGoing = mScroller.isFinished();
+      }
+
+      if (!keepGoing) {
+        if (deferCallbacks) {
+          mParentView.post(mSetIdleRunnable);
+        } else {
+          setDragState(STATE_IDLE);
+        }
+      }
+    }
+
+    return mDragState == STATE_SETTLING;
+  }
+
+  /**
+   * Like all callback events this must happen on the UI thread, but release
+   * involves some extra semantics. During a release (mReleaseInProgress)
+   * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
+   * or {@link #flingCapturedView(int, int, int, int)}.
+   */
+  private void dispatchViewReleased(float xvel, float yvel) {
+    mReleaseInProgress = true;
+    mCallback.onViewReleased(mCapturedView, xvel, yvel);
+    mReleaseInProgress = false;
+
+    if (mDragState == STATE_DRAGGING) {
+      // onViewReleased didn't call a method that would have changed this. Go idle.
+      setDragState(STATE_IDLE);
+    }
+  }
+
+  private void clearMotionHistory() {
+    if (mInitialMotionX == null) {
+      return;
+    }
+    Arrays.fill(mInitialMotionX, 0);
+    Arrays.fill(mInitialMotionY, 0);
+    Arrays.fill(mLastMotionX, 0);
+    Arrays.fill(mLastMotionY, 0);
+    Arrays.fill(mInitialEdgesTouched, 0);
+    Arrays.fill(mEdgeDragsInProgress, 0);
+    Arrays.fill(mEdgeDragsLocked, 0);
+    mPointersDown = 0;
+  }
+
+  private void clearMotionHistory(int pointerId) {
+    if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
+      return;
+    }
+    mInitialMotionX[pointerId] = 0;
+    mInitialMotionY[pointerId] = 0;
+    mLastMotionX[pointerId] = 0;
+    mLastMotionY[pointerId] = 0;
+    mInitialEdgesTouched[pointerId] = 0;
+    mEdgeDragsInProgress[pointerId] = 0;
+    mEdgeDragsLocked[pointerId] = 0;
+    mPointersDown &= ~(1 << pointerId);
+  }
+
+  private void ensureMotionHistorySizeForId(int pointerId) {
+    if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
+      float[] imx = new float[pointerId + 1];
+      float[] imy = new float[pointerId + 1];
+      float[] lmx = new float[pointerId + 1];
+      float[] lmy = new float[pointerId + 1];
+      int[] iit = new int[pointerId + 1];
+      int[] edip = new int[pointerId + 1];
+      int[] edl = new int[pointerId + 1];
+
+      if (mInitialMotionX != null) {
+        System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
+        System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
+        System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
+        System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
+        System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
+        System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
+        System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
+      }
+
+      mInitialMotionX = imx;
+      mInitialMotionY = imy;
+      mLastMotionX = lmx;
+      mLastMotionY = lmy;
+      mInitialEdgesTouched = iit;
+      mEdgeDragsInProgress = edip;
+      mEdgeDragsLocked = edl;
+    }
+  }
+
+  private void saveInitialMotion(float x, float y, int pointerId) {
+    ensureMotionHistorySizeForId(pointerId);
+    mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
+    mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
+    mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
+    mPointersDown |= 1 << pointerId;
+  }
+
+  private void saveLastMotion(MotionEvent ev) {
+    final int pointerCount = MotionEventCompat.getPointerCount(ev);
+    for (int i = 0; i < pointerCount; i++) {
+      final int pointerId = MotionEventCompat.getPointerId(ev, i);
+      final float x = MotionEventCompat.getX(ev, i);
+      final float y = MotionEventCompat.getY(ev, i);
+      // Sometimes we can try and save last motion for a pointer never recorded in initial motion. In this case we just discard it.
+      if (mLastMotionX != null && mLastMotionY != null
+        && mLastMotionX.length > pointerId && mLastMotionY.length > pointerId) {
+        mLastMotionX[pointerId] = x;
+        mLastMotionY[pointerId] = y;
+      }
+    }
+  }
+
+  /**
+   * Check if the given pointer ID represents a pointer that is currently down (to the best
+   * of the ViewDragHelper's knowledge).
+   * <p>
+   * <p>The state used to report this information is populated by the methods
+   * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
+   * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not
+   * been called for all relevant MotionEvents to track, the information reported
+   * by this method may be stale or incorrect.</p>
+   *
+   * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
+   * @return true if the pointer with the given ID is still down
+   */
+  public boolean isPointerDown(int pointerId) {
+    return (mPointersDown & 1 << pointerId) != 0;
+  }
+
+  void setDragState(int state) {
+    if (mDragState != state) {
+      mDragState = state;
+      mCallback.onViewDragStateChanged(state);
+      if (mDragState == STATE_IDLE) {
+        mCapturedView = null;
+      }
+    }
+  }
+
+  /**
+   * Attempt to capture the view with the given pointer ID. The callback will be involved.
+   * This will put us into the "dragging" state. If we've already captured this view with
+   * this pointer this method will immediately return true without consulting the callback.
+   *
+   * @param toCapture View to capture
+   * @param pointerId Pointer to capture with
+   * @return true if capture was successful
+   */
+  boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
+    if (toCapture == mCapturedView && mActivePointerId == pointerId) {
+      // Already done!
+      return true;
+    }
+    if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
+      mActivePointerId = pointerId;
+      captureChildView(toCapture, pointerId);
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Tests scrollability within child views of v given a delta of dx.
+   *
+   * @param v      View to test for horizontal scrollability
+   * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+   *               or just its children (false).
+   * @param dx     Delta scrolled in pixels along the X axis
+   * @param dy     Delta scrolled in pixels along the Y axis
+   * @param x      X coordinate of the active touch point
+   * @param y      Y coordinate of the active touch point
+   * @return true if child views of v can be scrolled by delta of dx.
+   */
+  protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {
+    if (v instanceof ViewGroup) {
+      final ViewGroup group = (ViewGroup) v;
+      final int scrollX = v.getScrollX();
+      final int scrollY = v.getScrollY();
+      final int count = group.getChildCount();
+      // Count backwards - let topmost views consume scroll distance first.
+      for (int i = count - 1; i >= 0; i--) {
+        // TODO: Add versioned support here for transformed views.
+        // This will not work for transformed views in Honeycomb+
+        final View child = group.getChildAt(i);
+        if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+          y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+          canScroll(child, true, dx, dy, x + scrollX - child.getLeft(),
+            y + scrollY - child.getTop())) {
+          return true;
+        }
+      }
+    }
+
+    return checkV && (ViewCompat.canScrollHorizontally(v, -dx) ||
+      ViewCompat.canScrollVertically(v, -dy));
+  }
+
+  /**
+   * Check if this event as provided to the parent view's onInterceptTouchEvent should
+   * cause the parent to intercept the touch event stream.
+   *
+   * @param ev MotionEvent provided to onInterceptTouchEvent
+   * @return true if the parent view should return true from onInterceptTouchEvent
+   */
+  public boolean shouldInterceptTouchEvent(MotionEvent ev) {
+    final int action = MotionEventCompat.getActionMasked(ev);
+    final int actionIndex = MotionEventCompat.getActionIndex(ev);
+
+    if (action == MotionEvent.ACTION_DOWN) {
+      // Reset things for a new event stream, just in case we didn't get
+      // the whole previous stream.
+      cancel();
+    }
+
+    if (mVelocityTracker == null) {
+      mVelocityTracker = VelocityTracker.obtain();
+    }
+    mVelocityTracker.addMovement(ev);
+
+    switch (action) {
+      case MotionEvent.ACTION_DOWN: {
+        final float x = ev.getX();
+        final float y = ev.getY();
+        final int pointerId = MotionEventCompat.getPointerId(ev, 0);
+        saveInitialMotion(x, y, pointerId);
+
+        final View toCapture = findTopChildUnder((int) x, (int) y);
+
+        // Catch a settling view if possible.
+        if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
+          tryCaptureViewForDrag(toCapture, pointerId);
+        }
+
+        final int edgesTouched = mInitialEdgesTouched[pointerId];
+        if ((edgesTouched & mTrackingEdges) != 0) {
+          mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+        }
+        break;
+      }
+
+      case MotionEventCompat.ACTION_POINTER_DOWN: {
+        final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
+        final float x = MotionEventCompat.getX(ev, actionIndex);
+        final float y = MotionEventCompat.getY(ev, actionIndex);
+
+        saveInitialMotion(x, y, pointerId);
+
+        // A ViewDragHelper can only manipulate one view at a time.
+        if (mDragState == STATE_IDLE) {
+          final int edgesTouched = mInitialEdgesTouched[pointerId];
+          if ((edgesTouched & mTrackingEdges) != 0) {
+            mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+          }
+        } else if (mDragState == STATE_SETTLING) {
+          // Catch a settling view if possible.
+          final View toCapture = findTopChildUnder((int) x, (int) y);
+          if (toCapture == mCapturedView) {
+            tryCaptureViewForDrag(toCapture, pointerId);
+          }
+        }
+        break;
+      }
+
+      case MotionEvent.ACTION_MOVE: {
+        // First to cross a touch slop over a draggable view wins. Also report edge drags.
+        final int pointerCount = MotionEventCompat.getPointerCount(ev);
+        for (int i = 0; i < pointerCount && mInitialMotionX != null && mInitialMotionY != null; i++) {
+          final int pointerId = MotionEventCompat.getPointerId(ev, i);
+          if (pointerId >= mInitialMotionX.length || pointerId >= mInitialMotionY.length) {
+            continue;
+          }
+          final float x = MotionEventCompat.getX(ev, i);
+          final float y = MotionEventCompat.getY(ev, i);
+          final float dx = x - mInitialMotionX[pointerId];
+          final float dy = y - mInitialMotionY[pointerId];
+
+          reportNewEdgeDrags(dx, dy, pointerId);
+          if (mDragState == STATE_DRAGGING) {
+            // Callback might have started an edge drag
+            break;
+          }
+
+          final View toCapture = findTopChildUnder((int) mInitialMotionX[pointerId], (int) mInitialMotionY[pointerId]);
+          if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
+            tryCaptureViewForDrag(toCapture, pointerId)) {
+            break;
+          }
+        }
+        saveLastMotion(ev);
+        break;
+      }
+
+      case MotionEventCompat.ACTION_POINTER_UP: {
+        final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
+        clearMotionHistory(pointerId);
+        break;
+      }
+
+      case MotionEvent.ACTION_UP:
+      case MotionEvent.ACTION_CANCEL: {
+        cancel();
+        break;
+      }
+    }
+
+    return mDragState == STATE_DRAGGING;
+  }
+
+  /**
+   * Process a touch event received by the parent view. This method will dispatch callback events
+   * as needed before returning. The parent view's onTouchEvent implementation should call this.
+   *
+   * @param ev The touch event received by the parent view
+   */
+  public void processTouchEvent(MotionEvent ev) {
+    final int action = MotionEventCompat.getActionMasked(ev);
+    final int actionIndex = MotionEventCompat.getActionIndex(ev);
+
+    if (action == MotionEvent.ACTION_DOWN) {
+      // Reset things for a new event stream, just in case we didn't get
+      // the whole previous stream.
+      cancel();
+    }
+
+    if (mVelocityTracker == null) {
+      mVelocityTracker = VelocityTracker.obtain();
+    }
+    mVelocityTracker.addMovement(ev);
+
+    switch (action) {
+      case MotionEvent.ACTION_DOWN: {
+        final float x = ev.getX();
+        final float y = ev.getY();
+        final int pointerId = MotionEventCompat.getPointerId(ev, 0);
+        final View toCapture = findTopChildUnder((int) x, (int) y);
+
+        saveInitialMotion(x, y, pointerId);
+
+        // Since the parent is already directly processing this touch event,
+        // there is no reason to delay for a slop before dragging.
+        // Start immediately if possible.
+        tryCaptureViewForDrag(toCapture, pointerId);
+
+        final int edgesTouched = mInitialEdgesTouched[pointerId];
+        if ((edgesTouched & mTrackingEdges) != 0) {
+          mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+        }
+        break;
+      }
+
+      case MotionEventCompat.ACTION_POINTER_DOWN: {
+        final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
+        final float x = MotionEventCompat.getX(ev, actionIndex);
+        final float y = MotionEventCompat.getY(ev, actionIndex);
+
+        saveInitialMotion(x, y, pointerId);
+
+        // A ViewDragHelper can only manipulate one view at a time.
+        if (mDragState == STATE_IDLE) {
+          // If we're idle we can do anything! Treat it like a normal down event.
+
+          final View toCapture = findTopChildUnder((int) x, (int) y);
+          tryCaptureViewForDrag(toCapture, pointerId);
+
+          final int edgesTouched = mInitialEdgesTouched[pointerId];
+          if ((edgesTouched & mTrackingEdges) != 0) {
+            mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+          }
+        } else if (isCapturedViewUnder((int) x, (int) y)) {
+          // We're still tracking a captured view. If the same view is under this
+          // point, we'll swap to controlling it with this pointer instead.
+          // (This will still work if we're "catching" a settling view.)
+
+          tryCaptureViewForDrag(mCapturedView, pointerId);
+        }
+        break;
+      }
+
+      case MotionEvent.ACTION_MOVE: {
+        if (mDragState == STATE_DRAGGING) {
+          final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+          final float x = MotionEventCompat.getX(ev, index);
+          final float y = MotionEventCompat.getY(ev, index);
+          final int idx = (int) (x - mLastMotionX[mActivePointerId]);
+          final int idy = (int) (y - mLastMotionY[mActivePointerId]);
+
+          dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
+
+          saveLastMotion(ev);
+        } else {
+          // Check to see if any pointer is now over a draggable view.
+          final int pointerCount = MotionEventCompat.getPointerCount(ev);
+          for (int i = 0; i < pointerCount; i++) {
+            final int pointerId = MotionEventCompat.getPointerId(ev, i);
+            final float x = MotionEventCompat.getX(ev, i);
+            final float y = MotionEventCompat.getY(ev, i);
+            final float dx = x - mInitialMotionX[pointerId];
+            final float dy = y - mInitialMotionY[pointerId];
+
+            reportNewEdgeDrags(dx, dy, pointerId);
+            if (mDragState == STATE_DRAGGING) {
+              // Callback might have started an edge drag.
+              break;
+            }
+
+            final View toCapture = findTopChildUnder((int) mInitialMotionX[pointerId], (int) mInitialMotionY[pointerId]);
+            if (checkTouchSlop(toCapture, dx, dy) &&
+              tryCaptureViewForDrag(toCapture, pointerId)) {
+              break;
+            }
+          }
+          saveLastMotion(ev);
+        }
+        break;
+      }
+
+      case MotionEventCompat.ACTION_POINTER_UP: {
+        final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
+        if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
+          // Try to find another pointer that's still holding on to the captured view.
+          int newActivePointer = INVALID_POINTER;
+          final int pointerCount = MotionEventCompat.getPointerCount(ev);
+          for (int i = 0; i < pointerCount; i++) {
+            final int id = MotionEventCompat.getPointerId(ev, i);
+            if (id == mActivePointerId) {
+              // This one's going away, skip.
+              continue;
+            }
+
+            final float x = MotionEventCompat.getX(ev, i);
+            final float y = MotionEventCompat.getY(ev, i);
+            if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
+              tryCaptureViewForDrag(mCapturedView, id)) {
+              newActivePointer = mActivePointerId;
+              break;
+            }
+          }
+
+          if (newActivePointer == INVALID_POINTER) {
+            // We didn't find another pointer still touching the view, release it.
+            releaseViewForPointerUp();
+          }
+        }
+        clearMotionHistory(pointerId);
+        break;
+      }
+
+      case MotionEvent.ACTION_UP: {
+        if (mDragState == STATE_DRAGGING) {
+          releaseViewForPointerUp();
+        }
+        cancel();
+        break;
+      }
+
+      case MotionEvent.ACTION_CANCEL: {
+        if (mDragState == STATE_DRAGGING) {
+          dispatchViewReleased(0, 0);
+        }
+        cancel();
+        break;
+      }
+    }
+  }
+
+  private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
+    int dragsStarted = 0;
+    if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
+      dragsStarted |= EDGE_LEFT;
+    }
+    if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
+      dragsStarted |= EDGE_TOP;
+    }
+    if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
+      dragsStarted |= EDGE_RIGHT;
+    }
+    if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
+      dragsStarted |= EDGE_BOTTOM;
+    }
+
+    if (dragsStarted != 0) {
+      mEdgeDragsInProgress[pointerId] |= dragsStarted;
+      mCallback.onEdgeDragStarted(dragsStarted, pointerId);
+    }
+  }
+
+  private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
+    final float absDelta = Math.abs(delta);
+    final float absODelta = Math.abs(odelta);
+
+    if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
+      (mEdgeDragsLocked[pointerId] & edge) == edge ||
+      (mEdgeDragsInProgress[pointerId] & edge) == edge ||
+      (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
+      return false;
+    }
+    if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
+      mEdgeDragsLocked[pointerId] |= edge;
+      return false;
+    }
+    return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
+  }
+
+  /**
+   * Check if we've crossed a reasonable touch slop for the given child view.
+   * If the child cannot be dragged along the horizontal or vertical axis, motion
+   * along that axis will not count toward the slop check.
+   *
+   * @param child Child to check
+   * @param dx    Motion since initial position along X axis
+   * @param dy    Motion since initial position along Y axis
+   * @return true if the touch slop has been crossed
+   */
+  private boolean checkTouchSlop(View child, float dx, float dy) {
+    if (child == null) {
+      return false;
+    }
+    final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
+    final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
+
+    if (checkHorizontal && checkVertical) {
+      return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
+    } else if (checkHorizontal) {
+      return Math.abs(dx) > mTouchSlop;
+    } else if (checkVertical) {
+      return Math.abs(dy) > mTouchSlop;
+    }
+    return false;
+  }
+
+  /**
+   * Check if any pointer tracked in the current gesture has crossed
+   * the required slop threshold.
+   * <p>
+   * <p>This depends on internal state populated by
+   * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
+   * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on
+   * the results of this method after all currently available touch data
+   * has been provided to one of these two methods.</p>
+   *
+   * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL},
+   *                   {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL}
+   * @return true if the slop threshold has been crossed, false otherwise
+   */
+  public boolean checkTouchSlop(int directions) {
+    final int count = mInitialMotionX.length;
+    for (int i = 0; i < count; i++) {
+      if (checkTouchSlop(directions, i)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Check if the specified pointer tracked in the current gesture has crossed
+   * the required slop threshold.
+   * <p>
+   * <p>This depends on internal state populated by
+   * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
+   * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on
+   * the results of this method after all currently available touch data
+   * has been provided to one of these two methods.</p>
+   *
+   * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL},
+   *                   {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL}
+   * @param pointerId  ID of the pointer to slop check as specified by MotionEvent
+   * @return true if the slop threshold has been crossed, false otherwise
+   */
+  public boolean checkTouchSlop(int directions, int pointerId) {
+    if (!isPointerDown(pointerId)) {
+      return false;
+    }
+
+    final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL;
+    final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL;
+
+    final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId];
+    final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId];
+
+    if (checkHorizontal && checkVertical) {
+      return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
+    } else if (checkHorizontal) {
+      return Math.abs(dx) > mTouchSlop;
+    } else if (checkVertical) {
+      return Math.abs(dy) > mTouchSlop;
+    }
+    return false;
+  }
+
+  /**
+   * Check if any of the edges specified were initially touched in the currently active gesture.
+   * If there is no currently active gesture this method will return false.
+   *
+   * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT},
+   *              {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and
+   *              {@link #EDGE_ALL}
+   * @return true if any of the edges specified were initially touched in the current gesture
+   */
+  public boolean isEdgeTouched(int edges) {
+    final int count = mInitialEdgesTouched.length;
+    for (int i = 0; i < count; i++) {
+      if (isEdgeTouched(edges, i)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Check if any of the edges specified were initially touched by the pointer with
+   * the specified ID. If there is no currently active gesture or if there is no pointer with
+   * the given ID currently down this method will return false.
+   *
+   * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT},
+   *              {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and
+   *              {@link #EDGE_ALL}
+   * @return true if any of the edges specified were initially touched in the current gesture
+   */
+  public boolean isEdgeTouched(int edges, int pointerId) {
+    return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0;
+  }
+
+  public boolean isDragging() {
+    return mDragState == STATE_DRAGGING;
+  }
+
+  private void releaseViewForPointerUp() {
+    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+    final float xvel = clampMag(
+      VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
+      mMinVelocity, mMaxVelocity);
+    final float yvel = clampMag(
+      VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
+      mMinVelocity, mMaxVelocity);
+    dispatchViewReleased(xvel, yvel);
+  }
+
+  private void dragTo(int left, int top, int dx, int dy) {
+    int clampedX = left;
+    int clampedY = top;
+    final int oldLeft = mCapturedView.getLeft();
+    final int oldTop = mCapturedView.getTop();
+    if (dx != 0) {
+      clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
+      mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
+    }
+    if (dy != 0) {
+      clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
+      mCapturedView.offsetTopAndBottom(clampedY - oldTop);
+    }
+
+    if (dx != 0 || dy != 0) {
+      final int clampedDx = clampedX - oldLeft;
+      final int clampedDy = clampedY - oldTop;
+      mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
+        clampedDx, clampedDy);
+    }
+  }
+
+  /**
+   * Determine if the currently captured view is under the given point in the
+   * parent view's coordinate system. If there is no captured view this method
+   * will return false.
+   *
+   * @param x X position to test in the parent's coordinate system
+   * @param y Y position to test in the parent's coordinate system
+   * @return true if the captured view is under the given point, false otherwise
+   */
+  public boolean isCapturedViewUnder(int x, int y) {
+    return isViewUnder(mCapturedView, x, y);
+  }
+
+  /**
+   * Determine if the supplied view is under the given point in the
+   * parent view's coordinate system.
+   *
+   * @param view Child view of the parent to hit test
+   * @param x    X position to test in the parent's coordinate system
+   * @param y    Y position to test in the parent's coordinate system
+   * @return true if the supplied view is under the given point, false otherwise
+   */
+  public boolean isViewUnder(View view, int x, int y) {
+    if (view == null) {
+      return false;
+    }
+    return x >= view.getLeft() &&
+      x < view.getRight() &&
+      y >= view.getTop() &&
+      y < view.getBottom();
+  }
+
+  /**
+   * Find the topmost child under the given point within the parent view's coordinate system.
+   * The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
+   *
+   * @param x X position to test in the parent's coordinate system
+   * @param y Y position to test in the parent's coordinate system
+   * @return The topmost child view under (x, y) or null if none found.
+   */
+  public View findTopChildUnder(int x, int y) {
+    final int childCount = mParentView.getChildCount();
+    for (int i = childCount - 1; i >= 0; i--) {
+      final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
+      if (x >= child.getLeft() && x < child.getRight() &&
+        y >= child.getTop() && y < child.getBottom()) {
+        return child;
+      }
+    }
+    return null;
+  }
+
+  private int getEdgesTouched(int x, int y) {
+    int result = 0;
+
+    if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
+    if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
+    if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
+    if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;
+
+    return result;
+  }
+
+  /**
+   * A Callback is used as a communication channel with the ViewDragHelper back to the
+   * parent view using it. <code>on*</code>methods are invoked on siginficant events and several
+   * accessor methods are expected to provide the ViewDragHelper with more information
+   * about the state of the parent view upon request. The callback also makes decisions
+   * governing the range and draggability of child views.
+   */
+  public static abstract class Callback {
+    /**
+     * Called when the drag state changes. See the <code>STATE_*</code> constants
+     * for more information.
+     *
+     * @param state The new drag state
+     * @see #STATE_IDLE
+     * @see #STATE_DRAGGING
+     * @see #STATE_SETTLING
+     */
+    public void onViewDragStateChanged(int state) {
+    }
+
+    /**
+     * Called when the captured view's position changes as the result of a drag or settle.
+     *
+     * @param changedView View whose position changed
+     * @param left        New X coordinate of the left edge of the view
+     * @param top         New Y coordinate of the top edge of the view
+     * @param dx          Change in X position from the last call
+     * @param dy          Change in Y position from the last call
+     */
+    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+    }
+
+    /**
+     * Called when a child view is captured for dragging or settling. The ID of the pointer
+     * currently dragging the captured view is supplied. If activePointerId is
+     * identified as {@link #INVALID_POINTER} the capture is programmatic instead of
+     * pointer-initiated.
+     *
+     * @param capturedChild   Child view that was captured
+     * @param activePointerId Pointer id tracking the child capture
+     */
+    public void onViewCaptured(View capturedChild, int activePointerId) {
+    }
+
+    /**
+     * Called when the child view is no longer being actively dragged.
+     * The fling velocity is also supplied, if relevant. The velocity values may
+     * be clamped to system minimums or maximums.
+     * <p>
+     * <p>Calling code may decide to fling or otherwise release the view to let it
+     * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
+     * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
+     * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
+     * and the view capture will not fully end until it comes to a complete stop.
+     * If neither of these methods is invoked before <code>onViewReleased</code> returns,
+     * the view will stop in place and the ViewDragHelper will return to
+     * {@link #STATE_IDLE}.</p>
+     *
+     * @param releasedChild The captured child view now being released
+     * @param xvel          X velocity of the pointer as it left the screen in pixels per second.
+     * @param yvel          Y velocity of the pointer as it left the screen in pixels per second.
+     */
+    public void onViewReleased(View releasedChild, float xvel, float yvel) {
+    }
+
+    /**
+     * Called when one of the subscribed edges in the parent view has been touched
+     * by the user while no child view is currently captured.
+     *
+     * @param edgeFlags A combination of edge flags describing the edge(s) currently touched
+     * @param pointerId ID of the pointer touching the described edge(s)
+     * @see #EDGE_LEFT
+     * @see #EDGE_TOP
+     * @see #EDGE_RIGHT
+     * @see #EDGE_BOTTOM
+     */
+    public void onEdgeTouched(int edgeFlags, int pointerId) {
+    }
+
+    /**
+     * Called when the given edge may become locked. This can happen if an edge drag
+     * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
+     * was called. This method should return true to lock this edge or false to leave it
+     * unlocked. The default behavior is to leave edges unlocked.
+     *
+     * @param edgeFlags A combination of edge flags describing the edge(s) locked
+     * @return true to lock the edge, false to leave it unlocked
+     */
+    public boolean onEdgeLock(int edgeFlags) {
+      return false;
+    }
+
+    /**
+     * Called when the user has started a deliberate drag away from one
+     * of the subscribed edges in the parent view while no child view is currently captured.
+     *
+     * @param edgeFlags A combination of edge flags describing the edge(s) dragged
+     * @param pointerId ID of the pointer touching the described edge(s)
+     * @see #EDGE_LEFT
+     * @see #EDGE_TOP
+     * @see #EDGE_RIGHT
+     * @see #EDGE_BOTTOM
+     */
+    public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+    }
+
+    /**
+     * Called to determine the Z-order of child views.
+     *
+     * @param index the ordered position to query for
+     * @return index of the view that should be ordered at position <code>index</code>
+     */
+    public int getOrderedChildIndex(int index) {
+      return index;
+    }
+
+    /**
+     * Return the magnitude of a draggable child view's horizontal range of motion in pixels.
+     * This method should return 0 for views that cannot move horizontally.
+     *
+     * @param child Child view to check
+     * @return range of horizontal motion in pixels
+     */
+    public int getViewHorizontalDragRange(View child) {
+      return 0;
+    }
+
+    /**
+     * Return the magnitude of a draggable child view's vertical range of motion in pixels.
+     * This method should return 0 for views that cannot move vertically.
+     *
+     * @param child Child view to check
+     * @return range of vertical motion in pixels
+     */
+    public int getViewVerticalDragRange(View child) {
+      return 0;
+    }
+
+    /**
+     * Called when the user's input indicates that they want to capture the given child view
+     * with the pointer indicated by pointerId. The callback should return true if the user
+     * is permitted to drag the given view with the indicated pointer.
+     * <p>
+     * <p>ViewDragHelper may call this method multiple times for the same view even if
+     * the view is already captured; this indicates that a new pointer is trying to take
+     * control of the view.</p>
+     * <p>
+     * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
+     * will follow if the capture is successful.</p>
+     *
+     * @param child     Child the user is attempting to capture
+     * @param pointerId ID of the pointer attempting the capture
+     * @return true if capture should be allowed, false otherwise
+     */
+    public abstract boolean tryCaptureView(View child, int pointerId);
+
+    /**
+     * Restrict the motion of the dragged child view along the horizontal axis.
+     * The default implementation does not allow horizontal motion; the extending
+     * class must override this method and provide the desired clamping.
+     *
+     * @param child Child view being dragged
+     * @param left  Attempted motion along the X axis
+     * @param dx    Proposed change in position for left
+     * @return The new clamped position for left
+     */
+    public int clampViewPositionHorizontal(View child, int left, int dx) {
+      return 0;
+    }
+
+    /**
+     * Restrict the motion of the dragged child view along the vertical axis.
+     * The default implementation does not allow vertical motion; the extending
+     * class must override this method and provide the desired clamping.
+     *
+     * @param child Child view being dragged
+     * @param top   Attempted motion along the Y axis
+     * @param dy    Proposed change in position for top
+     * @return The new clamped position for top
+     */
+    public int clampViewPositionVertical(View child, int top, int dy) {
+      return 0;
+    }
+  }
+}
diff --git a/slidingpanel/src/main/res/drawable/above_shadow.xml b/slidingpanel/src/main/res/drawable/above_shadow.xml
new file mode 100644
index 000000000..b51564520
--- /dev/null
+++ b/slidingpanel/src/main/res/drawable/above_shadow.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+  <gradient
+    android:angle="90"
+    android:endColor="@android:color/transparent"
+    android:startColor="#20000000"></gradient>
+</shape>
\ No newline at end of file
diff --git a/slidingpanel/src/main/res/drawable/below_shadow.xml b/slidingpanel/src/main/res/drawable/below_shadow.xml
new file mode 100644
index 000000000..fc63fea99
--- /dev/null
+++ b/slidingpanel/src/main/res/drawable/below_shadow.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+  <gradient
+    android:angle="270"
+    android:endColor="@android:color/transparent"
+    android:startColor="#20000000"></gradient>
+</shape>
\ No newline at end of file
diff --git a/slidingpanel/src/main/res/values/attrs.xml b/slidingpanel/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..89566734e
--- /dev/null
+++ b/slidingpanel/src/main/res/values/attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+  <declare-styleable name="SlidingUpPanelLayout">
+    <attr name="umanoPanelHeight" format="dimension" />
+    <attr name="umanoShadowHeight" format="dimension" />
+    <attr name="umanoParallaxOffset" format="dimension" />
+    <attr name="umanoFadeColor" format="color" />
+    <attr name="umanoFlingVelocity" format="integer" />
+    <attr name="umanoDragView" format="reference" />
+    <attr name="umanoAntiDragView" format="reference" />
+    <attr name="umanoScrollableView" format="reference" />
+    <attr name="umanoOverlay" format="boolean" />
+    <attr name="umanoClipPanel" format="boolean" />
+    <attr name="umanoAnchorPoint" format="float" />
+    <attr name="umanoInitialState" format="enum">
+      <enum name="expanded" value="0" />
+      <enum name="collapsed" value="1" />
+      <enum name="anchored" value="2" />
+      <enum name="hidden" value="3" />
+    </attr>
+    <attr name="umanoScrollInterpolator" format="reference" />
+  </declare-styleable>
+
+</resources>
-- 
GitLab