Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make swipe left to reply animation in chat app?

I am working on a messenger project and it is about completed. But I wanted to make a feature for replying to a specific message by swiping. I searched about it and found an amazing article. So I have just implemented it and it worked as it should be.

Now the question is how to make it work for the right side messages, swipe left to reply. Or we can say just the opposite of the usual. I just want to make it looks professional like WhatsApp.

I have tried this way But it just swipes to left, No reply animation no vibration. Source Code

override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
    mView = viewHolder.itemView
    imageDrawable = context.getDrawable(R.drawable.ic_reply_black_24dp)!!
    shareRound = context.getDrawable(R.drawable.ic_round_shape)!!
    val direction = if (viewHolder.itemViewType != MessageType.SEND) {
        RIGHT
    } else {
        LEFT
    }
    return ItemTouchHelper.Callback.makeMovementFlags(ACTION_STATE_IDLE, direction)
}
like image 823
mr. groot Avatar asked Oct 24 '25 14:10

mr. groot


2 Answers

public class SwipeReply extends ItemTouchHelper.Callback {

    private Drawable imageDrawable;
    private Drawable shareRound;
    private RecyclerView.ViewHolder currentItemViewHolder;
    private View mView;
    private float dX = 0f;
    private float replyButtonProgress = 0f;
    private long lastReplyButtonAnimationTime = 0;
    private boolean swipeBack = false;
    private boolean isVibrate = false;
    private boolean startTracking = false;
    private float density;
    private final Context context;
    private final SwipeControllerActions swipeControllerActions;
    public SwipeReply(@NotNull Context context, @NotNull SwipeControllerActions swipeControllerActions) {
        super();
        this.context = context;
        this.swipeControllerActions = swipeControllerActions;
        this.density = 1.0F;
    }
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        mView = viewHolder.itemView;
        if(viewHolder.getItemViewType()== MessageAdapter.RECEIVER_VIEW_TYPE||viewHolder.getItemViewType()== MessageAdapter.RECEIVER_VIEW_IMAGE||viewHolder.getItemViewType()== MessageAdapter.RECEIVED_MESSAGE_IMAGE){
            imageDrawable = context.getDrawable(R.drawable.ic_reply);
            shareRound = context.getDrawable(R.drawable.ic_round);
            return ItemTouchHelper.Callback.makeMovementFlags(ACTION_STATE_IDLE, RIGHT);
        }
        if(viewHolder.getItemViewType()== MessageAdapter.SENDER_VIEW_TYPE||viewHolder.getItemViewType()== MessageAdapter.SENDER_VIEW_IMAGE||viewHolder.getItemViewType()== MessageAdapter.SENDER_MESSAGE_IMAGE){
            imageDrawable = context.getDrawable(R.drawable.ic_reply);
            shareRound = context.getDrawable(R.drawable.ic_round);
            return ItemTouchHelper.Callback.makeMovementFlags(ACTION_STATE_IDLE, LEFT);
        }

        return 0;
    }
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        return false;
    }
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
    }
    @Override
    public int convertToAbsoluteDirection(int flags, int layoutDirection) {
        if (swipeBack) {
            swipeBack = false;
            return 0;
        }
        return super.convertToAbsoluteDirection(flags, layoutDirection);
    }
    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        if (actionState == ACTION_STATE_SWIPE) {
            setTouchListener(recyclerView, viewHolder);
        }
        super.onChildDraw(c, recyclerView, viewHolder, dX/2, dY, actionState, isCurrentlyActive);
        this.dX = dX;
        startTracking = true;
        currentItemViewHolder = viewHolder;
        drawReplyButton(c);
    }
    @SuppressLint("ClickableViewAccessibility")
    private void setTouchListener(RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder) {
        recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                swipeBack = motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP;
                if (swipeBack) {
                    if (Math.abs(mView.getTranslationX()) >=convertTodp(80)) {
                        swipeControllerActions.showReplyUI(viewHolder.getAdapterPosition());
                    }
                }
                return false;
            }
        });
    }
    private void drawReplyButton(Canvas canvas) {
        if (currentItemViewHolder == null) {
            return;
        }
        float translationX = mView.getTranslationX();
        long newTime = System.currentTimeMillis();
        long dt = Math.min(17, newTime - lastReplyButtonAnimationTime)/2;
        lastReplyButtonAnimationTime = newTime;
        boolean showing = translationX >= convertTodp(30);
        boolean showing1 = translationX <=-convertTodp(30);
        if (showing|showing1) {
            if (replyButtonProgress < 1.0f) {
                replyButtonProgress += dt / 180.0f;
                if (replyButtonProgress > 1.0f) {
                    replyButtonProgress = 1.0f;
                } else {
                    mView.invalidate();
                }
            }
        } else if (translationX == 0.0f) {
            replyButtonProgress = 0f;
            startTracking = false;
            isVibrate = false;
        }else {
            if (replyButtonProgress > 0.0f) {
                replyButtonProgress -= dt / 180.0f;
                if (replyButtonProgress < 0.1f) {
                    replyButtonProgress = 0f;
                } else {
                    mView.invalidate();
                }
            }
        }
        int alpha;
        float scale;
        if (showing||showing1) {
            scale = this.replyButtonProgress <= 0.8F ? 1.2F * (this.replyButtonProgress / 0.8F) : 1.2F - 0.2F * ((this.replyButtonProgress - 0.8F) / 0.2F);
            alpha = (int) Math.min(255.0F, (float) 255 * (this.replyButtonProgress / 0.8F));
        } else {
            scale = this.replyButtonProgress;
            alpha = (int) Math.min(255.0F, (float) 255 * this.replyButtonProgress);
        }
        shareRound.setAlpha(alpha);
        imageDrawable.setAlpha(alpha);
        if (startTracking) {
            if (!isVibrate && (mView.getTranslationX() >= convertTodp(80)||mView.getTranslationX() <= -convertTodp(80))) {
                mView.performHapticFeedback(
                        HapticFeedbackConstants.KEYBOARD_TAP,
                        HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
                );
                isVibrate = true;
            }
        }

        int x;
        float y;
        y = (float) ((mView.getTop() + mView.getMeasuredHeight() / 2));
        if(mView.getTranslationX()>0){
            if (mView.getTranslationX() > (float) this.convertTodp(130)) {
                x = this.convertTodp(130) / 2;
            }else {
                x = (int) (mView.getTranslationX() / (float) 2);
            }
            shareRound.setBounds((int) ((float) x - (float) this.convertTodp(16) * scale), (int) (y - (float) this.convertTodp(16) * scale), (int) ((float) x + (float) this.convertTodp(16) * scale), (int) (y + (float) this.convertTodp(16) * scale));
            shareRound.draw(canvas);
            imageDrawable.setBounds((int) ((float) x - (float) this.convertTodp(10) * scale), (int) (y - (float) this.convertTodp(10) * scale), (int) ((float) x + (float) this.convertTodp(10) * scale), (int) (y + (float) this.convertTodp(8) * scale));
            imageDrawable.draw(canvas);
            shareRound.setAlpha(255);
            imageDrawable.setAlpha(255);
        }
        else if(0>mView.getTranslationX()){
            if (mView.getTranslationX() < -(float) this.convertTodp(130)) {
                x = mView.getRight()+(int) (mView.getTranslationX() / (float) 2);
            }else {
                x = mView.getRight()+(int) (mView.getTranslationX() / (float) 2);
            }
            shareRound.setBounds((int) ((float) x - (float) this.convertTodp(16) * scale), (int) (y - (float) this.convertTodp(16) * scale), (int) ((float) x + (float) this.convertTodp(16) * scale), (int) (y + (float) this.convertTodp(16) * scale));
            shareRound.draw(canvas);
            imageDrawable.setBounds((int) ((float) x - (float) this.convertTodp(10) * scale), (int) (y - (float) this.convertTodp(10) * scale), (int) ((float) x + (float) this.convertTodp(10) * scale), (int) (y + (float) this.convertTodp(8) * scale));
            imageDrawable.draw(canvas);
            shareRound.setAlpha(255);
            imageDrawable.setAlpha(255);

        }
    }


    private int convertTodp(int pixel) {
        return this.dp((float) pixel, this.context);
    }


    public int dp(Float value, Context context) {
        if (this.density == 1.0F) {
            this.checkDisplaySize(context);
        }

        return value == 0.0F ? 0 : (int) Math.ceil((double) (this.density * value));
    }

    private void checkDisplaySize(Context context) {
        try {
            this.density = context.getResources().getDisplayMetrics().density;
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public interface SwipeControllerActions {
        void showReplyUI(int position);
    }

}

like image 154
Imtiaj Ahmed Avatar answered Oct 26 '25 05:10

Imtiaj Ahmed


The Kotlin version

class SwipeReply(private val context: Context, private val swipeControllerActions: SwipeControllerActions) :
ItemTouchHelper.Callback() {

private var replyDrawable: Drawable? = null
private var currentItemViewHolder: RecyclerView.ViewHolder? = null
private var mView: View? = null
private var dX = 0f
private var replyButtonProgress = 0f
private var lastReplyButtonAnimationTime: Long = 0
private var swipeBack = false
private var isVibrate = false
private var startTracking = false
private val density: Float = context.resources.displayMetrics.density

private val SWIPE_THRESHOLD = convertToDp(40)
private val DRAWABLE_SIZE = convertToDp(16)
private val MAX_TRANSLATION = convertToDp(80)

@SuppressLint("UseCompatLoadingForDrawables")
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
    mView = viewHolder.itemView
    replyDrawable = context.getDrawable(R.drawable.share_small)

    return when (viewHolder.itemViewType) {
        C.ONE_MESSAGE_TYPE.OTHER_USER.value, C.ONE_MESSAGE_TYPE.CURRENT_USER.value -> makeMovementFlags(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.RIGHT)
        else -> 0
    }
}

override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean = false

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}

override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int {
    if (swipeBack) {
        swipeBack = false
        return 0
    }
    return super.convertToAbsoluteDirection(flags, layoutDirection)
}

override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        setTouchListener(recyclerView, viewHolder)
    }
    super.onChildDraw(c, recyclerView, viewHolder, dX / 2, dY, actionState, isCurrentlyActive)
    this.dX = dX
    startTracking = true
    currentItemViewHolder = viewHolder
    drawReplyButton(c)
}

@SuppressLint("ClickableViewAccessibility")
private fun setTouchListener(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
    recyclerView.setOnTouchListener { _, motionEvent ->
        swipeBack = motionEvent.action == MotionEvent.ACTION_CANCEL || motionEvent.action == MotionEvent.ACTION_UP
        if (swipeBack && abs(mView!!.translationX) >= SWIPE_THRESHOLD) {
            swipeControllerActions.showReplyUI(viewHolder.adapterPosition)
        }
        false
    }
}

private fun drawReplyButton(canvas: Canvas) {
    currentItemViewHolder ?: return
    val translationX = mView!!.translationX
    val newTime = System.currentTimeMillis()
    val dt = min(17, newTime - lastReplyButtonAnimationTime) / 2
    lastReplyButtonAnimationTime = newTime
    val showing = translationX >= convertToDp(30)
    val showing1 = translationX <= -convertToDp(30)
    updateReplyButtonProgress(showing, showing1, dt.toFloat())
    val (alpha, scale) = calculateAlphaAndScale(showing, showing1)
    replyDrawable?.alpha = alpha
    if (startTracking) {
        checkVibration()
    }
    drawReplyDrawable(canvas, scale)
}

private fun updateReplyButtonProgress(showing: Boolean, showing1: Boolean, dt: Float) {
    when {
        showing || showing1 -> {
            if (replyButtonProgress < 1.0f) {
                replyButtonProgress += dt / 180.0f
                if (replyButtonProgress > 1.0f) {
                    replyButtonProgress = 1.0f
                } else {
                    mView?.invalidate()
                }
            }
        }
        mView!!.translationX == 0.0f -> {
            replyButtonProgress = 0f
            startTracking = false
            isVibrate = false
        }
        else -> {
            if (replyButtonProgress > 0.0f) {
                replyButtonProgress -= dt / 180.0f
                if (replyButtonProgress < 0.1f) {
                    replyButtonProgress = 0f
                } else {
                    mView?.invalidate()
                }
            }
        }
    }
}

private fun calculateAlphaAndScale(showing: Boolean, showing1: Boolean): Pair<Int, Float> {
    val alpha: Int
    val scale: Float
    if (showing || showing1) {
        scale = if (replyButtonProgress <= 0.8f) 1.2f * (replyButtonProgress / 0.8f) else 1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f)
        alpha = min(255.0f, 255f * (replyButtonProgress / 0.8f)).toInt()
    } else {
        scale = replyButtonProgress
        alpha = min(255.0f, 255f * replyButtonProgress).toInt()
    }
    return Pair(alpha, scale)
}

private fun checkVibration() {
    if (!isVibrate && (mView!!.translationX >= SWIPE_THRESHOLD || mView!!.translationX <= -SWIPE_THRESHOLD)) {
        Functions.vibrateMedium(context)
        isVibrate = true
    }
}

private fun drawReplyDrawable(canvas: Canvas, scale: Float) {
    val y = (mView!!.top + mView!!.measuredHeight / 2).toFloat()
    val x = calculateDrawablePosition()
    replyDrawable?.setBounds(
        (x.toFloat() - DRAWABLE_SIZE.toFloat() * scale).toInt(),
        (y - DRAWABLE_SIZE.toFloat() * scale).toInt(),
        (x.toFloat() + DRAWABLE_SIZE.toFloat() * scale).toInt(),
        (y + DRAWABLE_SIZE.toFloat() * scale).toInt()
    )
    replyDrawable?.draw(canvas)
    replyDrawable?.alpha = 255
}

private fun calculateDrawablePosition(): Int {
    return when {
        mView!!.translationX > 0 -> {
            if (mView!!.translationX > MAX_TRANSLATION.toFloat()) {
                MAX_TRANSLATION / 2
            } else {
                (mView!!.translationX / 2f).toInt()
            }
        }
        0 > mView!!.translationX -> {
            if (mView!!.translationX < -MAX_TRANSLATION.toFloat()) {
                mView!!.right + (mView!!.translationX / 2f).toInt()
            } else {
                mView!!.right + (mView!!.translationX / 2f).toInt()
            }
        }
        else -> 0
    }
}

private fun convertToDp(pixel: Int): Int {
    return dp(pixel.toFloat())
}

private fun dp(value: Float): Int {
    return if (value == 0.0f) 0 else ceil((density * value).toDouble()).toInt()
}

interface SwipeControllerActions {
    fun showReplyUI(position: Int)
}
}

How to use it:

    val messageSwipeController = SwipeReply(requireContext(), this)
    val itemTouchHelper = ItemTouchHelper(messageSwipeController)
    itemTouchHelper.attachToRecyclerView(binding.mainRv)
like image 27
Alex Busuioc Avatar answered Oct 26 '25 04:10

Alex Busuioc



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!