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