kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add new gift opening animation and confirmation haptic.
rodzic
d49c8d5184
commit
6bd8bc08d8
|
@ -25,4 +25,31 @@ interface OpenableGift {
|
||||||
* Clears any callback created to start the open animation
|
* Clears any callback created to start the open animation
|
||||||
*/
|
*/
|
||||||
fun clearOpenGiftCallback()
|
fun clearOpenGiftCallback()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate sign for the animation evaluators:
|
||||||
|
*
|
||||||
|
* - Incoming and LTR -> Positive
|
||||||
|
* - Incoming and RTL -> Negative
|
||||||
|
* - Outgoing and LTR -> Negative
|
||||||
|
* - Outgoing and RTL -> Positive
|
||||||
|
*/
|
||||||
|
fun getAnimationSign(): AnimationSign
|
||||||
|
|
||||||
|
enum class AnimationSign(val sign: Float) {
|
||||||
|
POSITIVE(1f),
|
||||||
|
NEGATIVE(-1f);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun get(isLtr: Boolean, isOutgoing: Boolean): AnimationSign {
|
||||||
|
return when {
|
||||||
|
isLtr && isOutgoing -> NEGATIVE
|
||||||
|
isLtr -> POSITIVE
|
||||||
|
isOutgoing -> POSITIVE
|
||||||
|
else -> NEGATIVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
@ -13,8 +14,10 @@ import android.view.animation.AnticipateInterpolator
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.toRect
|
import androidx.core.graphics.toRect
|
||||||
|
import androidx.core.graphics.withRotation
|
||||||
import androidx.core.graphics.withSave
|
import androidx.core.graphics.withSave
|
||||||
import androidx.core.graphics.withTranslation
|
import androidx.core.graphics.withTranslation
|
||||||
|
import androidx.core.view.animation.PathInterpolatorCompat
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
@ -191,7 +194,7 @@ class OpenableGiftItemDecoration(context: Context) : RecyclerView.ItemDecoration
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTranslation(progress: Float): Double {
|
private fun getTranslation(progress: Float): Double {
|
||||||
val interpolated = INTERPOLATOR.getInterpolation(progress)
|
val interpolated = TRANSLATION_X_INTERPOLATOR.getInterpolation(progress)
|
||||||
val evaluated = EVALUATOR.evaluate(interpolated, 0f, 360f)
|
val evaluated = EVALUATOR.evaluate(interpolated, 0f, 360f)
|
||||||
|
|
||||||
return 0.25f * sin(4 * evaluated * PI / 180f) * 180f / PI
|
return 0.25f * sin(4 * evaluated * PI / 180f) * 180f / PI
|
||||||
|
@ -199,17 +202,60 @@ class OpenableGiftItemDecoration(context: Context) : RecyclerView.ItemDecoration
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpenAnimationState(openableGift: OpenableGift, startTime: Long) : GiftAnimationState(openableGift, startTime, OPEN_DURATION_MILLIS) {
|
class OpenAnimationState(openableGift: OpenableGift, startTime: Long) : GiftAnimationState(openableGift, startTime, OPEN_DURATION_MILLIS) {
|
||||||
override fun update(canvas: Canvas, projection: Projection, progress: Float, lastFrameProgress: Float, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit) {
|
|
||||||
val interpolatedProgress = INTERPOLATOR.getInterpolation(progress)
|
|
||||||
val evaluatedValue = EVALUATOR.evaluate(interpolatedProgress, 0f, DimensionUnit.DP.toPixels(161f))
|
|
||||||
|
|
||||||
val interpolatedY = TRANSLATION_Y_INTERPOLATOR.getInterpolation(progress)
|
private val bowRotationPath = Path().apply {
|
||||||
|
lineTo(0.13f, -0.75f)
|
||||||
|
lineTo(0.26f, 0f)
|
||||||
|
lineTo(0.73f, -1.375f)
|
||||||
|
lineTo(1f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val boxRotationPath = Path().apply {
|
||||||
|
lineTo(0.63f, -1.6f)
|
||||||
|
lineTo(1f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val bowRotationInterpolator = PathInterpolatorCompat.create(bowRotationPath)
|
||||||
|
|
||||||
|
private val boxRotationInterpolator = PathInterpolatorCompat.create(boxRotationPath)
|
||||||
|
|
||||||
|
override fun update(canvas: Canvas, projection: Projection, progress: Float, lastFrameProgress: Float, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit) {
|
||||||
|
val sign = openableGift.getAnimationSign().sign
|
||||||
|
|
||||||
|
val boxStartDelay: Float = OPEN_BOX_START_DELAY / duration.toFloat()
|
||||||
|
val boxProgress: Float = max(0f, progress - boxStartDelay) / (1f - boxStartDelay)
|
||||||
|
|
||||||
|
val bowStartDelay: Float = OPEN_BOW_START_DELAY / duration.toFloat()
|
||||||
|
val bowProgress: Float = max(0f, progress - bowStartDelay) / (1f - bowStartDelay)
|
||||||
|
|
||||||
|
val interpolatedX = TRANSLATION_X_INTERPOLATOR.getInterpolation(boxProgress)
|
||||||
|
val evaluatedX = EVALUATOR.evaluate(interpolatedX, 0f, DimensionUnit.DP.toPixels(18f * sign))
|
||||||
|
|
||||||
|
val interpolatedY = TRANSLATION_Y_INTERPOLATOR.getInterpolation(boxProgress)
|
||||||
val evaluatedY = EVALUATOR.evaluate(interpolatedY, 0f, DimensionUnit.DP.toPixels(355f))
|
val evaluatedY = EVALUATOR.evaluate(interpolatedY, 0f, DimensionUnit.DP.toPixels(355f))
|
||||||
|
|
||||||
canvas.translate(evaluatedValue, evaluatedY)
|
val interpolatedBowRotation = bowRotationInterpolator.getInterpolation(bowProgress)
|
||||||
|
val evaluatedBowRotation = EVALUATOR.evaluate(interpolatedBowRotation, 0f, 8f * sign)
|
||||||
|
|
||||||
drawBox(canvas, projection)
|
val interpolatedBoxRotation = boxRotationInterpolator.getInterpolation(boxProgress)
|
||||||
drawBow(canvas, projection)
|
val evaluatedBoxRotation = EVALUATOR.evaluate(interpolatedBoxRotation, 0f, -5f * sign)
|
||||||
|
|
||||||
|
canvas.withTranslation(evaluatedX, evaluatedY) {
|
||||||
|
canvas.withRotation(
|
||||||
|
degrees = evaluatedBoxRotation,
|
||||||
|
pivotX = projection.x + projection.width / 2f,
|
||||||
|
pivotY = projection.y + projection.height / 2f
|
||||||
|
) {
|
||||||
|
drawBox(this, projection)
|
||||||
|
canvas.withRotation(
|
||||||
|
degrees = evaluatedBowRotation,
|
||||||
|
pivotX = projection.x + projection.width / 2f,
|
||||||
|
pivotY = projection.y + projection.height / 2f
|
||||||
|
) {
|
||||||
|
drawBow(this, projection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,11 +295,13 @@ class OpenableGiftItemDecoration(context: Context) : RecyclerView.ItemDecoration
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TRANSLATION_Y_INTERPOLATOR = AnticipateInterpolator(3f)
|
private val TRANSLATION_Y_INTERPOLATOR = AnticipateInterpolator(3f)
|
||||||
private val INTERPOLATOR = AccelerateDecelerateInterpolator()
|
private val TRANSLATION_X_INTERPOLATOR = AccelerateDecelerateInterpolator()
|
||||||
private val EVALUATOR = FloatEvaluator()
|
private val EVALUATOR = FloatEvaluator()
|
||||||
|
|
||||||
private const val SHAKE_DURATION_MILLIS = 1000L
|
private const val SHAKE_DURATION_MILLIS = 1000L
|
||||||
private const val OPEN_DURATION_MILLIS = 700L
|
private const val OPEN_DURATION_MILLIS = 1400L
|
||||||
|
private const val OPEN_BOX_START_DELAY = 400L
|
||||||
|
private const val OPEN_BOW_START_DELAY = 50L
|
||||||
private const val ONE_FRAME_RELATIVE_TO_30_FPS_MILLIS = 33
|
private const val ONE_FRAME_RELATIVE_TO_30_FPS_MILLIS = 33
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.graphics.PorterDuff;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.Annotation;
|
import android.text.Annotation;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
|
@ -41,6 +42,7 @@ import android.text.style.URLSpan;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.TouchDelegate;
|
import android.view.TouchDelegate;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -2149,6 +2151,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
bodyBubble.setOnClickListener(unused -> {
|
bodyBubble.setOnClickListener(unused -> {
|
||||||
openGift.invoke(this);
|
openGift.invoke(this);
|
||||||
eventListener.onGiftBadgeRevealed(messageRecord);
|
eventListener.onGiftBadgeRevealed(messageRecord);
|
||||||
|
bodyBubble.performHapticFeedback(Build.VERSION.SDK_INT >= 30 ? HapticFeedbackConstants.CONFIRM
|
||||||
|
: HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
});
|
});
|
||||||
giftViewStub.get().onGiftNotOpened();
|
giftViewStub.get().onGiftNotOpened();
|
||||||
}
|
}
|
||||||
|
@ -2163,6 +2167,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull AnimationSign getAnimationSign() {
|
||||||
|
return AnimationSign.get(ViewUtil.isLtr(this), messageRecord.isOutgoing());
|
||||||
|
}
|
||||||
|
|
||||||
private class SharedContactEventListener implements SharedContactView.EventListener {
|
private class SharedContactEventListener implements SharedContactView.EventListener {
|
||||||
@Override
|
@Override
|
||||||
public void onAddToContactsClicked(@NonNull Contact contact) {
|
public void onAddToContactsClicked(@NonNull Contact contact) {
|
||||||
|
|
Ładowanie…
Reference in New Issue