From 659386bd5e93317c5dbf5bf6b79a5e2ae4c90d99 Mon Sep 17 00:00:00 2001 From: Emily Kager Date: Thu, 11 Apr 2019 17:17:10 -0700 Subject: [PATCH] For #176 - Start swipe to close gesture --- CHANGELOG.md | 1 + .../sessioncontrol/SessionControlAdapter.kt | 10 +- .../sessioncontrol/SessionControlUIView.kt | 8 + .../sessioncontrol/SwipeToDeleteCallback.kt | 152 ++++++++++++++++++ .../viewholders/SessionViewHolder.kt | 2 +- .../main/res/drawable/session_background.xml | 11 +- 6 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 024c9d1bc..461eeebf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #1397 - Adds favicons to the history view - #1375 - Added setting for turning off history suggestions - #1139 - Resolved a 170ms delay on cold start +- #176 - Added a swipe to delete gesture on home screen ### Changed - #1429 - Updated site permissions ui for MVP diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 535f8af5b..f6c717566 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -62,7 +62,10 @@ class SessionControlAdapter( TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, actionEmitter) TabViewHolder.LAYOUT_ID -> TabViewHolder(view, actionEmitter, job) ArchiveTabsViewHolder.LAYOUT_ID -> ArchiveTabsViewHolder(view, actionEmitter) - PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, actionEmitter) + PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder( + view, + actionEmitter + ) DeleteTabsViewHolder.LAYOUT_ID -> DeleteTabsViewHolder(view, actionEmitter) SessionHeaderViewHolder.LAYOUT_ID -> SessionHeaderViewHolder(view) SessionPlaceholderViewHolder.LAYOUT_ID -> SessionPlaceholderViewHolder(view) @@ -87,7 +90,10 @@ class SessionControlAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is TabViewHolder -> holder.bindSession((items[position] as AdapterItem.TabItem).tab, position) + is TabViewHolder -> holder.bindSession( + (items[position] as AdapterItem.TabItem).tab, + position + ) is SessionViewHolder -> holder.bind((items[position] as AdapterItem.SessionItem).session) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt index 88cc53ce2..e0b7e10e3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt @@ -13,6 +13,7 @@ import io.reactivex.Observer import io.reactivex.functions.Consumer import org.mozilla.fenix.R import org.mozilla.fenix.mvi.UIView +import androidx.recyclerview.widget.ItemTouchHelper // Convert HomeState into a data structure HomeAdapter understands @SuppressWarnings("ComplexMethod") @@ -62,6 +63,13 @@ class SessionControlUIView( view.apply { adapter = sessionControlAdapter layoutManager = LinearLayoutManager(container.context) + val itemTouchHelper = + ItemTouchHelper( + SwipeToDeleteCallback( + actionEmitter + ) + ) + itemTouchHelper.attachToRecyclerView(this) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt new file mode 100644 index 000000000..09ba541c6 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.home.sessioncontrol + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import io.reactivex.Observer +import org.mozilla.fenix.DefaultThemeManager +import org.mozilla.fenix.R +import org.mozilla.fenix.home.sessioncontrol.viewholders.SessionViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder +import android.graphics.drawable.Drawable + +class SwipeToDeleteCallback( + val actionEmitter: Observer +) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + // We don't support drag and drop so this method will never be called + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + if (viewHolder is TabViewHolder) { + actionEmitter.onNext(TabAction.Close(viewHolder.tab?.sessionId!!)) + } + if (viewHolder is SessionViewHolder) { + viewHolder.session?.apply { actionEmitter.onNext(ArchivedSessionAction.Delete(this)) } + } + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + val icon = ContextCompat.getDrawable(recyclerView.context, R.drawable.ic_delete) + val background = ContextCompat.getDrawable( + recyclerView.context, + R.drawable.session_background + ) + + background?.let { + icon?.let { + val itemView = viewHolder.itemView + val iconLeft: Int + val iconRight: Int + val margin = convertDpToPixel(MARGIN.toFloat()) + val iconWidth = icon.intrinsicWidth + val iconHeight = icon.intrinsicHeight + val cellHeight = itemView.bottom - itemView.top + val iconTop = itemView.top + (cellHeight - iconHeight) / 2 + val iconBottom = iconTop + iconHeight + + when { + dX > 0 -> { // Swiping to the right + iconLeft = itemView.left + margin + iconRight = itemView.left + margin + iconWidth + background.setBounds( + itemView.left, itemView.top, + (itemView.left + dX).toInt() + BACKGROUND_CORNER_OFFSET, + itemView.bottom + ) + icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) + draw(background, icon, c, recyclerView.context, iconLeft, iconBottom) + } + dX < 0 -> { // Swiping to the left + iconLeft = itemView.right - margin - iconWidth + iconRight = itemView.right - margin + background.setBounds( + (itemView.right + dX).toInt() - BACKGROUND_CORNER_OFFSET, + itemView.top, itemView.right, itemView.bottom + ) + icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) + draw(background, icon, c, recyclerView.context, iconLeft, iconBottom) + } + else -> { // View not swiped + background.setBounds(0, 0, 0, 0) + icon.setBounds(0, 0, 0, 0) + } + } + } + } + } + + override fun getSwipeDirs( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return if (viewHolder is TabViewHolder || viewHolder is SessionViewHolder) super.getSwipeDirs( + recyclerView, + viewHolder + ) else 0 + } + + companion object { + const val BACKGROUND_CORNER_OFFSET = 40 + const val MARGIN = 32 + const val TEXT_MARGIN = 12 + const val TEXT_SIZE = 36f + const val TEXT_MARGIN_X = 14 + const val DENSITY_CONVERSION = 160f + + @Suppress("LongParameterList") + private fun draw( + background: Drawable, + icon: Drawable, + c: Canvas, + context: Context, + iconLeft: Int, + iconBottom: Int + ) { + background.draw(c) + icon.draw(c) + val textPaint = Paint() + textPaint.color = ContextCompat.getColor( + context, + DefaultThemeManager.resolveAttribute(R.attr.deleteColor, context) + ) + textPaint.textSize = TEXT_SIZE + val textX = iconLeft - TEXT_MARGIN_X + val textY = iconBottom + convertDpToPixel(TEXT_MARGIN.toFloat()) + c.drawText( + context.getString(R.string.current_session_delete), + textX.toFloat(), + textY.toFloat(), + textPaint + ) + } + + private fun convertDpToPixel(dp: Float): Int { + val metrics = Resources.getSystem().displayMetrics + val px = dp * (metrics.densityDpi / DENSITY_CONVERSION) + return Math.round(px) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SessionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SessionViewHolder.kt index 244e34eeb..7454d2332 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SessionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SessionViewHolder.kt @@ -89,7 +89,7 @@ class SessionViewHolder( private val actionEmitter: Observer, override val containerView: View? = view ) : RecyclerView.ViewHolder(view), LayoutContainer { - private var session: ArchivedSession? = null + internal var session: ArchivedSession? = null init { session_item.setOnClickListener { diff --git a/app/src/main/res/drawable/session_background.xml b/app/src/main/res/drawable/session_background.xml index 3fbf808b1..3b1d5b4d9 100644 --- a/app/src/main/res/drawable/session_background.xml +++ b/app/src/main/res/drawable/session_background.xml @@ -3,9 +3,8 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - - - \ No newline at end of file + + + +