我正在开发一个即时通讯项目,即将完成。但我想制作一个通过滑动回复特定消息的功能。我搜索了一下,发现了一篇很棒的文章。所以我刚刚实现了它并且它按预期工作了。
现在的问题是如何使其适用于右侧消息,向左滑动进行回复。或者我们可以说与平常相反。我只是想让它看起来像 WhatsApp 一样专业。
我已经尝试过这种方式,但它只是向左滑动,没有回复动画没有振动。 源代码
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);
}
}
Kotlin 版本
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)
}
}
使用方法:
val messageSwipeController = SwipeReply(requireContext(), this)
val itemTouchHelper = ItemTouchHelper(messageSwipeController)
itemTouchHelper.attachToRecyclerView(binding.mainRv)