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)
}
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);
}
}
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With