diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index b7a94b7167..2ccfa54201 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -115,6 +115,7 @@ import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet; import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet; import org.thoughtcrime.securesms.conversation.ConversationFragment; import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView; +import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp; import org.thoughtcrime.securesms.conversationlist.model.Conversation; import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments; import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo; @@ -280,7 +281,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode cameraFab.setVisibility(View.VISIBLE); CollapsingToolbarLayout collapsingToolbarLayout = view.findViewById(R.id.collapsing_toolbar); - int minHeight = (int) DimensionUnit.DP.toPixels(52); + int openHeight = (int) DimensionUnit.DP.toPixels(FilterLerp.FILTER_OPEN_HEIGHT); pullView.setOnFilterStateChanged(state -> { switch (state) { @@ -291,7 +292,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.setFiltered(true); break; case OPEN_APEX: - ViewUtil.setMinimumHeight(collapsingToolbarLayout, minHeight); + ViewUtil.setMinimumHeight(collapsingToolbarLayout, openHeight); break; case CLOSE_APEX: ViewUtil.setMinimumHeight(collapsingToolbarLayout, 0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt index 9cf51e64fe..958c1839d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt @@ -7,9 +7,9 @@ import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout import androidx.core.animation.doOnEnd -import org.signal.core.util.dp import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.databinding.ConversationListFilterPullViewBinding +import org.thoughtcrime.securesms.util.VibrateUtil /** * Encapsulates the push / pull latch for enabling and disabling @@ -51,13 +51,14 @@ class ConversationListFilterPullView @JvmOverloads constructor( setState(FilterPullState.CLOSED) } else if (state == FilterPullState.CLOSED && progress >= 1f) { setState(FilterPullState.OPEN_APEX) + vibrate() } else if (state == FilterPullState.OPEN && progress >= 1f) { setState(FilterPullState.CLOSE_APEX) + vibrate() } - // If we are pulling toward the open apex - if (state == FilterPullState.OPEN || state == FilterPullState.CLOSE_APEX || state == FilterPullState.CLOSING) { - binding.filterText.translationY = EVAL.evaluate(progress, 26.dp, -24.dp.toFloat()) + if (state == FilterPullState.OPEN || state == FilterPullState.OPEN_APEX || state == FilterPullState.CLOSE_APEX || state == FilterPullState.CLOSING) { + binding.filterText.translationY = FilterLerp.getPillLerp(progress) } else { binding.filterText.translationY = 0f } @@ -130,6 +131,12 @@ class ConversationListFilterPullView @JvmOverloads constructor( onFilterStateChanged?.newState(state) } + private fun vibrate() { + if (VibrateUtil.isHapticFeedbackEnabled(context)) { + VibrateUtil.vibrateTick(context) + } + } + interface OnFilterStateChanged { fun newState(state: FilterPullState) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterCircleView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterCircleView.kt index 0cfb79dff0..8556e4f8b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterCircleView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterCircleView.kt @@ -175,8 +175,8 @@ class FilterCircleView @JvmOverloads constructor( private fun evaluateBottomOffset(progress: Float, state: FilterPullState): Float { return when (state) { - FilterPullState.OPEN_APEX, FilterPullState.OPENING, FilterPullState.OPEN, FilterPullState.CLOSE_APEX -> CIRCLE_Y_EVALUATOR.evaluate(progress, (-46).dp, 55.dp) - FilterPullState.CLOSED, FilterPullState.CLOSING -> CIRCLE_Y_EVALUATOR.evaluate(progress, 0.dp, 55.dp) + FilterPullState.OPEN_APEX, FilterPullState.OPENING, FilterPullState.OPEN, FilterPullState.CLOSE_APEX -> FilterLerp.getOpenCircleBottomPadLerp(progress) + FilterPullState.CLOSED, FilterPullState.CLOSING -> FilterLerp.getClosedCircleBottomPadLerp(progress) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterLerp.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterLerp.kt new file mode 100644 index 0000000000..752f46ffdd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterLerp.kt @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.conversationlist.chatfilter + +import android.animation.FloatEvaluator +import org.signal.core.util.dp + +/** + * Centralized location for filter view linear interpolations. + */ +object FilterLerp { + + /** + * The minimum height of the filter pull when the filter is open. + */ + const val FILTER_OPEN_HEIGHT = 52f + + /** + * The maximum height of the filter pull. Note that this should match + * whatever value is set to in XML. + */ + private const val FILTER_APEX = 130f + + private val EVAL = FloatEvaluator() + + private val PILL_LERP = getFn( + Point(FILTER_OPEN_HEIGHT / FILTER_APEX, 0f), + Point(1f, ((FILTER_OPEN_HEIGHT - FILTER_APEX) / 2)) + ) + + private val OPEN_CIRCLE_BOTTOM_PAD_LERP = getFn( + Point(FILTER_OPEN_HEIGHT / FILTER_APEX, 8f), + Point(1f, FILTER_APEX * 0.55f) + ) + + private val CLOSED_CIRCLE_BOTTOM_PAD_LERP = getFn( + Point(0f, 0f), + Point(1f, FILTER_APEX * 0.55f) + ) + + /** + * Get the LERP for the "Filter enabled" pill. + */ + fun getPillLerp(fraction: Float): Float { + return getLerp(fraction, PILL_LERP) + } + + /** + * Get the LERP for the padding below the filter circle when the filter is open + */ + fun getOpenCircleBottomPadLerp(fraction: Float): Float { + return getLerp(fraction, OPEN_CIRCLE_BOTTOM_PAD_LERP) + } + + /** + * Get the LERP for the padding below the filter circle when the filter is closed + */ + fun getClosedCircleBottomPadLerp(fraction: Float): Float { + return getLerp(fraction, CLOSED_CIRCLE_BOTTOM_PAD_LERP) + } + + private fun getLerp(fraction: Float, fn: Fn): Float { + return EVAL.evaluate(fraction, fn(0f), fn(1f)).dp + } + + /** + * Gets the linear slope between two points using: + * + * m = (y2 - y1) / (x2 - x1) + */ + private fun getSlope( + a: Point, + b: Point + ): Float = (b.y - a.y) / (b.x - a.x) + + /** + * Gets the y-intercept between two points using: + * + * b = y - mx + */ + private fun getYIntercept( + a: Point, + b: Point + ): Float = a.y - getSlope(a, b) * a.x + + /** + * For a given set of points, generates a function in the form + * + * y = mx + b + */ + private fun getFn( + a: Point, + b: Point + ): Fn = Fn(getSlope(a, b), getYIntercept(a, b)) + + /** + * 2D cartesian coordinate. + */ + data class Point(val x: Float, val y: Float) + + /** + * LERP function defined as y = mx + b + */ + data class Fn(val m: Float, val b: Float) { + operator fun invoke(x: Float): Float { + return m * x + b + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VibrateUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/VibrateUtil.java index b7492aa6b9..280af14c77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VibrateUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VibrateUtil.java @@ -4,6 +4,7 @@ import android.content.Context; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.provider.Settings; import androidx.annotation.NonNull; @@ -27,4 +28,10 @@ public final class VibrateUtil { vibrator.vibrate(duration); } } + + public static boolean isHapticFeedbackEnabled(@NonNull Context context) { + int enabled = Settings.System.getInt(context.getContentResolver(), + android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED, 0); + return enabled != 0; + } } diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index bfa77e1154..8a1913a4d1 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -63,7 +63,7 @@