How to scroll webview horizontally inside ViewPager? [duplicate] - android

I have a WebView in a ViewPager.
And the ViewPager seems to consume all the horizontal scrolling, so that I can't scroll in the WebView (vertical works).
How can I achieve that the WebView has got priority consuming the horizontal scrolling?

don't ask me why this code gets formatted like this,
1. implement a custom ViewPager Instance like this:
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
public class CustomViewPager extends ViewPager {
private MagazineWebView_WithoutFlipWebView mCurrentPageWebView_; //custom webview
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (Constants.LOGGING) {
Log.v(Constants.LOG_OEAMTC_APP, "CustomViewPager - onInterceptTouchEvent");
}
// if view zoomed out (view starts at 33.12... scale level) ... allow
// zoom within webview, otherwise disallow (allow viewpager to change
// view)
if (mCurrentPageWebView_ != null && (mCurrentPageWebView_.getScale() * 100) > 34) {
Log.v(Constants.LOG_OEAMTC_APP, "CustomViewPager - intrcepted: " + String.valueOf((mCurrentPageWebView_.getScale() * > 100)));
this.requestDisallowInterceptTouchEvent(true);
}
else {
if (mCurrentPageWebView_ != null) {
Log.v(Constants.LOG_OEAMTC_APP,
"CustomViewPager - not intrcepted: " + String.valueOf(mCurrentPageWebView_.getScale() * 100));
}
this.requestDisallowInterceptTouchEvent(false);
}
return super.onInterceptTouchEvent(event);
}
public MagazineWebView_WithoutFlipWebView getCurrentPageWebView() {
return mCurrentPageWebView_;
}
public void setCurrentPageWebView(MagazineWebView_WithoutFlipWebView currentPageWebView) {
mCurrentPageWebView_ = currentPageWebView;
}
}
2. in your main (ViewPager) Activity add the following lines to the view pager
mViewPager_ = new AwesomePagerAdapter();
viewpapgerInLayout = (CustomViewPager) findViewById(R.id.awesomepager);
viewpapgerInLayout.setAdapter(mViewPager_);
viewpapgerInLayout.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
viewpapgerInLayout.setCurrentPageWebView(mLstPagesWebviews_.get(position));
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
3. finally, run it :=) if the zoom level is at initial zoom,
changes pages is allowed, all the other time you can navigate your web view
Ps.: *Don't forget* to change your ViewPager in your *.xml file with the CustomViewPager Class you just created
good luck :)

Try this
WebView mWebView = (WebView) findViewById(R.id.MyWebview);
mWebView.getSettings().setLoadWithOverviewMode(true);
mWebView.getSettings().setUseWideViewPort(true);
mWebView.getSettings().setSupportZoom(true);
mWebView.getSettings().setBuiltInZoomControls(true);
mWebView.getSettings().setUserAgentString("Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3");

Late to this question and I come up with a different approach to fix this issue by override the WebView onTouchEvent method. It works on both ViewPager and ViewPager2. Hope this would help the future SO.
Note: Replace your WebView with NestedScrollWebView
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.webkit.WebView
import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingChildHelper
import androidx.core.view.ViewCompat
class NestedScrollWebView : WebView, NestedScrollingChild {
companion object {
val TAG: String = NestedScrollWebView::class.java.simpleName
}
private var lastMotionX: Int = 0
private var lastMotionY: Int = 0
private val scrollOffset = IntArray(2)
private val scrollConsumed = IntArray(2)
private var nestedOffsetY: Int = 0
private var childHelper = NestedScrollingChildHelper(this)
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
isNestedScrollingEnabled = true
}
override fun onTouchEvent(event: MotionEvent): Boolean {
var result = false
val trackedEvent = MotionEvent.obtain(event)
val action = event.actionMasked
if (action == MotionEvent.ACTION_DOWN) {
nestedOffsetY = 0
}
val x = event.x.toInt()
val y = event.y.toInt()
event.offsetLocation(0f, nestedOffsetY.toFloat())
when (action) {
MotionEvent.ACTION_DOWN -> {
lastMotionX = x
lastMotionY = y
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
result = super.onTouchEvent(event)
}
MotionEvent.ACTION_MOVE -> {
var deltaY = lastMotionY - y
if (kotlin.math.abs(deltaY) > kotlin.math.abs(lastMotionX - x) &&
(canScrollVertically(1) || canScrollVertically(-1))
) {
requestDisallowInterceptTouchEvent(true)
}
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
deltaY -= scrollConsumed[1]
trackedEvent.offsetLocation(0f, scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
}
lastMotionY = y - scrollOffset[1]
val oldY = scrollY
val newScrollY = Math.max(0, oldY + deltaY)
val dyConsumed = newScrollY - oldY
val dyUnconsumed = deltaY - dyConsumed
if (dispatchNestedScroll(0, dyConsumed, 0, dyUnconsumed, scrollOffset)) {
lastMotionY -= scrollOffset[1]
trackedEvent.offsetLocation(0f, scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
}
result = super.onTouchEvent(trackedEvent)
trackedEvent.recycle()
}
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
stopNestedScroll()
requestDisallowInterceptTouchEvent(false)
result = super.onTouchEvent(event)
}
}
return result
}
// NestedScrollingChild
override fun setNestedScrollingEnabled(enabled: Boolean) {
childHelper.isNestedScrollingEnabled = enabled
}
override fun stopNestedScroll() {
childHelper.stopNestedScroll()
}
override fun isNestedScrollingEnabled() =
childHelper.isNestedScrollingEnabled
override fun startNestedScroll(axes: Int) =
childHelper.startNestedScroll(axes)
override fun hasNestedScrollingParent() =
childHelper.hasNestedScrollingParent()
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?) =
childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?) =
childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) =
childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) =
childHelper.dispatchNestedPreFling(velocityX, velocityY)
}

Related

Custom PinEntryView - default symbols

I have a custom EditText providig PinEntry functionality looking like this:
class PinEntryView : EditText {
private var mSpace = 15f
private var mCharSize = 0f
private var mNumChars = 4f
private var mLineSpacing = 8f
private val XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
private var mClickListener: View.OnClickListener? = null
private var mLineStroke = 1f
private var mLinesPaint: Paint? = null
private var mOnPinEnteredListener: OnPinEnteredListener? = null
private fun updateColorForLines(next: Boolean) {
if (isFocused) {
mLinesPaint!!.color = getColor(context, android.R.color.transparent)
if (next) {
mLinesPaint!!.color = getColor(context, R.color.edit_text_background)
}
} else {
mLinesPaint!!.color = getColor(context, android.R.color.transparent)
}
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(
context: Context, attrs: AttributeSet,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet) {
val multi = context.resources.displayMetrics.density
mLineStroke *= multi
mLinesPaint = Paint(paint)
mLinesPaint!!.strokeWidth = mLineStroke
mSpace *= multi
mLineSpacing *= multi
val mMaxLength = attrs.getAttributeIntValue(
XML_NAMESPACE_ANDROID,
"maxLength",
6
)
paint.color = getColor(context, android.R.color.white)
mNumChars = mMaxLength.toFloat()
super.setCustomSelectionActionModeCallback(
object : ActionMode.Callback {
override fun onPrepareActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode) {}
override fun onCreateActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode,
item: MenuItem
): Boolean {
return false
}
})
super.setOnClickListener { v ->
setSelection(text.length)
if (mClickListener != null) {
mClickListener!!.onClick(v)
}
}
}
override fun setOnClickListener(l: OnClickListener) {
mClickListener = l
}
override fun onDraw(canvas: Canvas) {
val availableWidth = width - paddingRight - paddingLeft
mCharSize = if (mSpace < 0) {
(availableWidth / (mNumChars * 2 - 1))
} else {
(availableWidth - mSpace * (mNumChars - 1)) / mNumChars
}
var startX = paddingLeft.toFloat()
val bottom = height.toFloat() - paddingBottom.toFloat()
val text = text
val textLength = text.length
val textWidths = FloatArray(textLength)
paint.getTextWidths(getText(), 0, textLength, textWidths)
for (i in 0 until mNumChars.toInt()) {
updateColorForLines(i == textLength)
canvas.drawRoundRect(
startX, 0f, startX + mCharSize, height.toFloat(), 5f, 5f, mLinesPaint
)
if (text.length > i) {
val middle = startX + mCharSize / 2
canvas.drawText(
"******",
i,
i + 1,
middle - textWidths[0] / 2,
bottom - mLineSpacing,
paint
)
}
startX += if (mSpace < 0) {
mCharSize * 2
} else {
mCharSize + mSpace
}
}
}
override fun onTextChanged(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) {
if (mOnPinEnteredListener != null && text.length.toFloat() == mNumChars) {
mOnPinEnteredListener!!.onPinEntered(text)
}
}
fun setOnPinEnteredListener(l: OnPinEnteredListener) {
mOnPinEnteredListener = l
}
interface OnPinEnteredListener {
fun onPinEntered(str: CharSequence)
}
}
I followed this tutorial: https://medium.com/#ali.muzaffar/building-a-pinentryedittext-in-android-5f2eddcae5d3
And I am getting behavior showed on picture:
But I would like to have a behavior as shown here:
How can I achieve that? How can I add those default dots/symbols? I tried adding it as a hint or default text, but it does not work because of my onDraw function
Hopefully you found already your solution. But here is how I would've done it:
The PinEntryEditText has an attribute you can set in the .xml file.
Adding this to the element will do the trick: app:pinSingleCharHint="•"
For more customisation check out the sample xml from this page

Recyclerview controll speed of scroll using scrollBy method

Dears I've managed to control the speed of scrolling using the scrollToPosition method via custom linear layout manager.
But I've used scrollBy(int dx, int dy) method and the scroll speed is still the default scrolling speed of the recyclerview speed.
Any Idea how to change the scrolling speed of this particular method?
Here's my layout's code:
package com.arabiaweather.maater.screens.livesatellite.components
import android.content.Context
import android.graphics.PointF
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.LinearSmoothScroller
import android.support.v7.widget.RecyclerView
import android.util.DisplayMetrics
class CustomLinearLayoutManager : LinearLayoutManager {
var millisPerInch = 250.0f
constructor(context: Context) : super(context) {}
constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout) {}
override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?, position: Int) {
val linearSmoothScroller = object : LinearSmoothScroller(recyclerView.context) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
return this#CustomLinearLayoutManager
.computeScrollVectorForPosition(targetPosition)
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return millisPerInch / displayMetrics.densityDpi
}
}
linearSmoothScroller.targetPosition = position
startSmoothScroll(linearSmoothScroller)
}
override fun startSmoothScroll(smoothScroller: RecyclerView.SmoothScroller?) {
super.startSmoothScroll(smoothScroller)
}
override fun scrollToPositionWithOffset(position: Int, offset: Int) {
super.scrollToPositionWithOffset(position, offset)
}
}
I think there is no way.
Looking at the Android code, I see that the ViewFlinger class contains a method that allows set the scroll by distance AND the velocity:
class ViewFlinger implements Runnable {
     ...
...
public void smoothScrollBy(int dx, int dy, int vx, int vy) {
            smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, vx, vy));
}
}
Unfortunately, I don't see a way to access this function from the RecyclerView as RecyclerView.mViewFlinger is private. I wish there was a way to access it from an inherited class.
Another way would be to extend the ViewFlinger class and override the function computeScrollDuration(), but I also don't see a way to do that (the class ViewFlinger is package-private in RecyclerView)
private int computeScrollDuration(int dx, int dy, int vx, int vy) {
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final boolean horizontal = absDx > absDy;
final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
final int delta = (int) Math.sqrt(dx * dx + dy * dy);
final int containerSize = horizontal ? getWidth() : getHeight();
final int halfContainerSize = containerSize / 2;
final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
final float distance = halfContainerSize + halfContainerSize
* distanceInfluenceForSnapDuration(distanceRatio);
final int duration;
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
float absDelta = (float) (horizontal ? absDx : absDy);
duration = (int) (((absDelta / containerSize) + 1) * 300);
}
return Math.min(duration, MAX_SCROLL_DURATION);
}
Ideally the RecyclerView.java class would add these lines to make it possible:
public void smoothScrollBy(int dx, int dy, int vx, int vy, Interpolator interpolator) {
if (mLayout == null) {
Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
+ "Call setLayoutManager with a non-null argument.");
return;
}
if (mLayoutFrozen) {
return;
}
if (!mLayout.canScrollHorizontally()) {
dx = 0;
}
if (!mLayout.canScrollVertically()) {
dy = 0;
}
if (dx != 0 || dy != 0) {
mViewFlinger.smoothScrollBy(dx, dy, vx, vy, interpolator);
}
}
So I don't see a way as of now.

Custom vertical viewpager not work with onClick listener on a child view

I have this custom ViewPager created from here (Android: Vertical ViewPager):
class VerticalViewPager : ViewPager {
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null)
: super(context, attrs){
setPageTransformer(false, DefaultTransformer())
overScrollMode = OVER_SCROLL_NEVER
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val intercepted = super.onInterceptTouchEvent(swapXY(ev))
swapXY(ev) // return touch coordinates to original reference frame for any child views
return intercepted
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
return super.onTouchEvent(ev)
}
/**
* Swaps the X and Y coordinates of your touch event.
*/
private fun swapXY(ev: MotionEvent): MotionEvent {
val width = width.toFloat()
val height = height.toFloat()
val newX = ev.y / height * width
val newY = ev.x / width * height
ev.setLocation(newX, newY)
return ev
}
private class VerticalPageTransformer : ViewPager.PageTransformer{
override fun transformPage(view: View, position: Float) {
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.alpha = 0f
} else if (position <= 1) { // [-1,1]
view.alpha = 1f
// Counteract the default slide transition
view.translationX = view.getWidth() * -position
//set Y position to swipe in from top
val yPosition = position * view.getHeight()
view.translationY = yPosition
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.alpha = 0f
}
}
}
}
I use an adapter that loads an ImageView inside each page. This ImageView has a onClickListener.
open class VerticalViewPagerAdapter(var mContext: Context, val photos : RealmList<ProfilePhoto>?, onPhotoClick: OnPhotoClick?) : PagerAdapter() {
var mLayoutInflater : LayoutInflater? = null
val onPhotoListener = onPhotoClick
init {
mLayoutInflater = mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
}
override fun getCount(): Int {
return photos?.size!!
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view === `object` as FrameLayout
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val itemView = mLayoutInflater?.inflate(R.layout.view_image_profile_viewpager_item, container, false)
val imageView = itemView?.findViewById(R.id.profileImageItemIv) as ImageView
imageView.setOnClickListener{
onPhotoListener?.onPhotoClick(position)
}
imageView.loadUrlImage(photos!![position]?.photo)
container.addView(itemView)
return itemView
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as FrameLayout)
}
}
The swipe on the custom viewpager doesn't work if the ImageView has this listener. If I remove the listener, works fine.
Anybody know what can be the problem?
Finally I solved the problem. I was to add logic to onInterceptTouchEvent to release the onTouch event if user only tap the viewpager and not swapping. This is the final code:
class VerticalViewPager : ViewPager {
var originalPosY = 0
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null)
: super(context, attrs){
setPageTransformer(false, DefaultTransformer())
}
private fun swapTouchEvent(event: MotionEvent): MotionEvent {
val width = width.toFloat()
val height = height.toFloat()
val swappedX = event.y / height * width
val swappedY = event.x / width * height
event.setLocation(swappedX, swappedY)
return event
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
var y : Int = event.rawY.toInt()
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
originalPosY = y
val intercept = super.onInterceptTouchEvent(swapTouchEvent(event))
swapTouchEvent(event)
false
}
MotionEvent.ACTION_MOVE -> {
val intercept = super.onInterceptTouchEvent(swapTouchEvent(event))
val i = swapTouchEvent(event)
true
}
MotionEvent.ACTION_UP -> {
if (Math.abs(originalPosY - y) > 10) {
super.onInterceptTouchEvent(swapTouchEvent(event))
val i = swapTouchEvent(event)
true
}else
false
}
else -> super.onInterceptTouchEvent(swapTouchEvent(event))
}
}
override fun onTouchEvent(event: MotionEvent): Boolean = super.onTouchEvent(swapTouchEvent(event))
}
The result is not all accurate that I want. If someone can improve it, it will be welcome

RecyclerView ItemTouchHelper Buttons on Swipe

I am trying to port some iOS functionality to Android.
I intent to create a table where on swipe to the left shows 2 button: Edit and Delete.
I have been playing with it and I know I am very close. The secret really lies on the method OnChildDraw.
I would like to Draw a Rect that fits the text Delete then draw the Edit text besides it with their respective background color. The remaining white space when clicked should restore the row to its initial position.
I have managed to paint the background while the user is swiping to the sides but I don't know how to add the listeners and once it is swiped to the side, the dragging function begins to misbehave.
I am working on Xamarin but pure java solutions also are accepted as I can easily port them to c#.
public class SavedPlacesItemTouchHelper : ItemTouchHelper.SimpleCallback
{
private SavedPlacesRecyclerviewAdapter adapter;
private Paint paint = new Paint();
private Context context;
public SavedPlacesItemTouchHelper(Context context, SavedPlacesRecyclerviewAdapter adapter) : base(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.Left)
{
this.context = context;
this.adapter = adapter;
}
public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
{
return false;
}
public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
{
}
public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
{
float translationX = dX;
View itemView = viewHolder.ItemView;
float height = (float)itemView.Bottom - (float)itemView.Top;
if (actionState == ItemTouchHelper.ActionStateSwipe && dX <= 0) // Swiping Left
{
translationX = -Math.Min(-dX, height * 2);
paint.Color = Color.Red;
RectF background = new RectF((float)itemView.Right + translationX, (float)itemView.Top, (float)itemView.Right, (float)itemView.Bottom);
c.DrawRect(background, paint);
//viewHolder.ItemView.TranslationX = translationX;
}
else if (actionState == ItemTouchHelper.ActionStateSwipe && dX > 0) // Swiping Right
{
translationX = Math.Min(dX, height * 2);
paint.Color = Color.Red;
RectF background = new RectF((float)itemView.Right + translationX, (float)itemView.Top, (float)itemView.Right, (float)itemView.Bottom);
c.DrawRect(background, paint);
}
base.OnChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
}
}
}
This is what I currently have.
If you know how to add listeners or any suggestions please leave a comment!
UPDATE:
I just realized that on double tap on the white remaining space of the row already restore the row to its initial state. Not a single tap though :(
I struggled with the same issue, and tried to find a solution online. Most of the solutions use a two-layer approach (one layer view item, another layer buttons), but I want to stick with ItemTouchHelper only. At the end, I came up with a worked solution. Please check below.
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
public abstract class SwipeHelper extends ItemTouchHelper.SimpleCallback {
public static final int BUTTON_WIDTH = YOUR_WIDTH_IN_PIXEL_PER_BUTTON
private RecyclerView recyclerView;
private List<UnderlayButton> buttons;
private GestureDetector gestureDetector;
private int swipedPos = -1;
private float swipeThreshold = 0.5f;
private Map<Integer, List<UnderlayButton>> buttonsBuffer;
private Queue<Integer> recoverQueue;
private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
for (UnderlayButton button : buttons){
if(button.onClick(e.getX(), e.getY()))
break;
}
return true;
}
};
private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent e) {
if (swipedPos < 0) return false;
Point point = new Point((int) e.getRawX(), (int) e.getRawY());
RecyclerView.ViewHolder swipedViewHolder = recyclerView.findViewHolderForAdapterPosition(swipedPos);
View swipedItem = swipedViewHolder.itemView;
Rect rect = new Rect();
swipedItem.getGlobalVisibleRect(rect);
if (e.getAction() == MotionEvent.ACTION_DOWN || e.getAction() == MotionEvent.ACTION_UP ||e.getAction() == MotionEvent.ACTION_MOVE) {
if (rect.top < point.y && rect.bottom > point.y)
gestureDetector.onTouchEvent(e);
else {
recoverQueue.add(swipedPos);
swipedPos = -1;
recoverSwipedItem();
}
}
return false;
}
};
public SwipeHelper(Context context, RecyclerView recyclerView) {
super(0, ItemTouchHelper.LEFT);
this.recyclerView = recyclerView;
this.buttons = new ArrayList<>();
this.gestureDetector = new GestureDetector(context, gestureListener);
this.recyclerView.setOnTouchListener(onTouchListener);
buttonsBuffer = new HashMap<>();
recoverQueue = new LinkedList<Integer>(){
#Override
public boolean add(Integer o) {
if (contains(o))
return false;
else
return super.add(o);
}
};
attachSwipe();
}
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int pos = viewHolder.getAdapterPosition();
if (swipedPos != pos)
recoverQueue.add(swipedPos);
swipedPos = pos;
if (buttonsBuffer.containsKey(swipedPos))
buttons = buttonsBuffer.get(swipedPos);
else
buttons.clear();
buttonsBuffer.clear();
swipeThreshold = 0.5f * buttons.size() * BUTTON_WIDTH;
recoverSwipedItem();
}
#Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return swipeThreshold;
}
#Override
public float getSwipeEscapeVelocity(float defaultValue) {
return 0.1f * defaultValue;
}
#Override
public float getSwipeVelocityThreshold(float defaultValue) {
return 5.0f * defaultValue;
}
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
int pos = viewHolder.getAdapterPosition();
float translationX = dX;
View itemView = viewHolder.itemView;
if (pos < 0){
swipedPos = pos;
return;
}
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
if(dX < 0) {
List<UnderlayButton> buffer = new ArrayList<>();
if (!buttonsBuffer.containsKey(pos)){
instantiateUnderlayButton(viewHolder, buffer);
buttonsBuffer.put(pos, buffer);
}
else {
buffer = buttonsBuffer.get(pos);
}
translationX = dX * buffer.size() * BUTTON_WIDTH / itemView.getWidth();
drawButtons(c, itemView, buffer, pos, translationX);
}
}
super.onChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
}
private synchronized void recoverSwipedItem(){
while (!recoverQueue.isEmpty()){
int pos = recoverQueue.poll();
if (pos > -1) {
recyclerView.getAdapter().notifyItemChanged(pos);
}
}
}
private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX){
float right = itemView.getRight();
float dButtonWidth = (-1) * dX / buffer.size();
for (UnderlayButton button : buffer) {
float left = right - dButtonWidth;
button.onDraw(
c,
new RectF(
left,
itemView.getTop(),
right,
itemView.getBottom()
),
pos
);
right = left;
}
}
public void attachSwipe(){
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this);
itemTouchHelper.attachToRecyclerView(recyclerView);
}
public abstract void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons);
public static class UnderlayButton {
private String text;
private int imageResId;
private int color;
private int pos;
private RectF clickRegion;
private UnderlayButtonClickListener clickListener;
public UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener) {
this.text = text;
this.imageResId = imageResId;
this.color = color;
this.clickListener = clickListener;
}
public boolean onClick(float x, float y){
if (clickRegion != null && clickRegion.contains(x, y)){
clickListener.onClick(pos);
return true;
}
return false;
}
public void onDraw(Canvas c, RectF rect, int pos){
Paint p = new Paint();
// Draw background
p.setColor(color);
c.drawRect(rect, p);
// Draw Text
p.setColor(Color.WHITE);
p.setTextSize(LayoutHelper.getPx(MyApplication.getAppContext(), 12));
Rect r = new Rect();
float cHeight = rect.height();
float cWidth = rect.width();
p.setTextAlign(Paint.Align.LEFT);
p.getTextBounds(text, 0, text.length(), r);
float x = cWidth / 2f - r.width() / 2f - r.left;
float y = cHeight / 2f + r.height() / 2f - r.bottom;
c.drawText(text, rect.left + x, rect.top + y, p);
clickRegion = rect;
this.pos = pos;
}
}
public interface UnderlayButtonClickListener {
void onClick(int pos);
}
}
Usage:
SwipeHelper swipeHelper = new SwipeHelper(this, recyclerView) {
#Override
public void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons) {
underlayButtons.add(new SwipeHelper.UnderlayButton(
"Delete",
0,
Color.parseColor("#FF3C30"),
new SwipeHelper.UnderlayButtonClickListener() {
#Override
public void onClick(int pos) {
// TODO: onDelete
}
}
));
underlayButtons.add(new SwipeHelper.UnderlayButton(
"Transfer",
0,
Color.parseColor("#FF9502"),
new SwipeHelper.UnderlayButtonClickListener() {
#Override
public void onClick(int pos) {
// TODO: OnTransfer
}
}
));
underlayButtons.add(new SwipeHelper.UnderlayButton(
"Unshare",
0,
Color.parseColor("#C7C7CB"),
new SwipeHelper.UnderlayButtonClickListener() {
#Override
public void onClick(int pos) {
// TODO: OnUnshare
}
}
));
}
};
Note: This helper class is designed for left swipe. You can change swipe direction in SwipeHelper's constructor, and making changes based on dX in onChildDraw method accordingly.
If you want to show image in the button, just make the use of imageResId in UnderlayButton, and re-implement the onDraw method.
There is a known bug, when you swipe an item diagonally from one item to another, the first touched item will flash a little. This could be addressed by decreasing the value of getSwipeVelocityThreshold, but this makes harder for user to swipe the item. You can also adjust the swiping feeling by changing two other values in getSwipeThreshold and getSwipeEscapeVelocity. Check into the ItemTouchHelper source code, the comments are very helpful.
I believe there is a lot place for optimization. This solution just gives an idea if you want to stick with ItemTouchHelper. Please let me know if you have problem using it. Below is a screenshot.
Acknowledgment: this solution is mostly inspired from AdamWei's answer in this post
Here is the Kotlin version based on the accepted answer approach. With some minor changes I managed to render the buttons width based on the intrinsic size of the text instead of using a fixed width.
Demo project: https://github.com/ntnhon/RecyclerViewRowOptionsDemo
Implementation of SwipeHelper:
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import java.util.*
import kotlin.math.abs
import kotlin.math.max
abstract class SwipeHelper(
private val recyclerView: RecyclerView
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.ACTION_STATE_IDLE,
ItemTouchHelper.LEFT
) {
private var swipedPosition = -1
private val buttonsBuffer: MutableMap<Int, List<UnderlayButton>> = mutableMapOf()
private val recoverQueue = object : LinkedList<Int>() {
override fun add(element: Int): Boolean {
if (contains(element)) return false
return super.add(element)
}
}
#SuppressLint("ClickableViewAccessibility")
private val touchListener = View.OnTouchListener { _, event ->
if (swipedPosition < 0) return#OnTouchListener false
buttonsBuffer[swipedPosition]?.forEach { it.handle(event) }
recoverQueue.add(swipedPosition)
swipedPosition = -1
recoverSwipedItem()
true
}
init {
recyclerView.setOnTouchListener(touchListener)
}
private fun recoverSwipedItem() {
while (!recoverQueue.isEmpty()) {
val position = recoverQueue.poll() ?: return
recyclerView.adapter?.notifyItemChanged(position)
}
}
private fun drawButtons(
canvas: Canvas,
buttons: List<UnderlayButton>,
itemView: View,
dX: Float
) {
var right = itemView.right
buttons.forEach { button ->
val width = button.intrinsicWidth / buttons.intrinsicWidth() * abs(dX)
val left = right - width
button.draw(
canvas,
RectF(left, itemView.top.toFloat(), right.toFloat(), itemView.bottom.toFloat())
)
right = left.toInt()
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val position = viewHolder.adapterPosition
var maxDX = dX
val itemView = viewHolder.itemView
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
if (dX < 0) {
if (!buttonsBuffer.containsKey(position)) {
buttonsBuffer[position] = instantiateUnderlayButton(position)
}
val buttons = buttonsBuffer[position] ?: return
if (buttons.isEmpty()) return
maxDX = max(-buttons.intrinsicWidth(), dX)
drawButtons(c, buttons, itemView, maxDX)
}
}
super.onChildDraw(
c,
recyclerView,
viewHolder,
maxDX,
dY,
actionState,
isCurrentlyActive
)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
if (swipedPosition != position) recoverQueue.add(swipedPosition)
swipedPosition = position
recoverSwipedItem()
}
abstract fun instantiateUnderlayButton(position: Int): List<UnderlayButton>
//region UnderlayButton
interface UnderlayButtonClickListener {
fun onClick()
}
class UnderlayButton(
private val context: Context,
private val title: String,
textSize: Float,
#ColorRes private val colorRes: Int,
private val clickListener: UnderlayButtonClickListener
) {
private var clickableRegion: RectF? = null
private val textSizeInPixel: Float = textSize * context.resources.displayMetrics.density // dp to px
private val horizontalPadding = 50.0f
val intrinsicWidth: Float
init {
val paint = Paint()
paint.textSize = textSizeInPixel
paint.typeface = Typeface.DEFAULT_BOLD
paint.textAlign = Paint.Align.LEFT
val titleBounds = Rect()
paint.getTextBounds(title, 0, title.length, titleBounds)
intrinsicWidth = titleBounds.width() + 2 * horizontalPadding
}
fun draw(canvas: Canvas, rect: RectF) {
val paint = Paint()
// Draw background
paint.color = ContextCompat.getColor(context, colorRes)
canvas.drawRect(rect, paint)
// Draw title
paint.color = ContextCompat.getColor(context, android.R.color.white)
paint.textSize = textSizeInPixel
paint.typeface = Typeface.DEFAULT_BOLD
paint.textAlign = Paint.Align.LEFT
val titleBounds = Rect()
paint.getTextBounds(title, 0, title.length, titleBounds)
val y = rect.height() / 2 + titleBounds.height() / 2 - titleBounds.bottom
canvas.drawText(title, rect.left + horizontalPadding, rect.top + y, paint)
clickableRegion = rect
}
fun handle(event: MotionEvent) {
clickableRegion?.let {
if (it.contains(event.x, event.y)) {
clickListener.onClick()
}
}
}
}
//endregion
}
private fun List<SwipeHelper.UnderlayButton>.intrinsicWidth(): Float {
if (isEmpty()) return 0.0f
return map { it.intrinsicWidth }.reduce { acc, fl -> acc + fl }
}
Usage:
private fun setUpRecyclerView() {
binding.recyclerView.adapter = Adapter(listOf(
"Item 0: No action",
"Item 1: Delete",
"Item 2: Delete & Mark as unread",
"Item 3: Delete, Mark as unread & Archive"
))
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
binding.recyclerView.layoutManager = LinearLayoutManager(this)
val itemTouchHelper = ItemTouchHelper(object : SwipeHelper(binding.recyclerView) {
override fun instantiateUnderlayButton(position: Int): List<UnderlayButton> {
var buttons = listOf<UnderlayButton>()
val deleteButton = deleteButton(position)
val markAsUnreadButton = markAsUnreadButton(position)
val archiveButton = archiveButton(position)
when (position) {
1 -> buttons = listOf(deleteButton)
2 -> buttons = listOf(deleteButton, markAsUnreadButton)
3 -> buttons = listOf(deleteButton, markAsUnreadButton, archiveButton)
else -> Unit
}
return buttons
}
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
}
private fun toast(text: String) {
toast?.cancel()
toast = Toast.makeText(this, text, Toast.LENGTH_SHORT)
toast?.show()
}
private fun deleteButton(position: Int) : SwipeHelper.UnderlayButton {
return SwipeHelper.UnderlayButton(
this,
"Delete",
14.0f,
android.R.color.holo_red_light,
object : SwipeHelper.UnderlayButtonClickListener {
override fun onClick() {
toast("Deleted item $position")
}
})
}
private fun markAsUnreadButton(position: Int) : SwipeHelper.UnderlayButton {
return SwipeHelper.UnderlayButton(
this,
"Mark as unread",
14.0f,
android.R.color.holo_green_light,
object : SwipeHelper.UnderlayButtonClickListener {
override fun onClick() {
toast("Marked as unread item $position")
}
})
}
private fun archiveButton(position: Int) : SwipeHelper.UnderlayButton {
return SwipeHelper.UnderlayButton(
this,
"Archive",
14.0f,
android.R.color.holo_blue_light,
object : SwipeHelper.UnderlayButtonClickListener {
override fun onClick() {
toast("Archived item $position")
}
})
}
I did the following to be able to draw a drawable instead of text:
In SwipeHelper, I changed
UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener)
to
UnderlayButton(String text, Bitmap bitmap, int color, UnderlayButtonClickListener clickListener)
Of course I removed imageResId and instead created a Bitmap bitmap and passed the constructor variable to it using this.bitmap = bitmap; as the rest of the variables.
In SwipeHelper.onDaw() you may then call drawBitmap() to apply your bitmap to the canvas. For example:
c.drawBitmap(bitmap, rect.left, rect.top, p);
Where c and p and your Canvas and Paint variables respectively.
In the activity where I call UnderlayButton, I convert my drawable (in my case it is a VectorDrawable) to a bitmap using this method:
int idDrawable = R.drawable.ic_delete_white;
Bitmap bitmap = getBitmapFromVectorDrawable(getContext(), idDrawable);
What remains to be done is the centring of the icon.
Full onDraw method with text and bitmap both centered:
public void onDraw(Canvas c, RectF rect, int pos){
Paint p = new Paint();
// Draw background
p.setColor(color);
c.drawRect(rect, p);
// Draw Text
p.setColor(Color.WHITE);
p.setTextSize(24);
float spaceHeight = 10; // change to whatever you deem looks better
float textWidth = p.measureText(text);
Rect bounds = new Rect();
p.getTextBounds(text, 0, text.length(), bounds);
float combinedHeight = bitmap.getHeight() + spaceHeight + bounds.height();
c.drawBitmap(bitmap, rect.centerX() - (bitmap.getWidth() / 2), rect.centerY() - (combinedHeight / 2), null);
//If you want text as well with bitmap
c.drawText(text, rect.centerX() - (textWidth / 2), rect.centerY() + (combinedHeight / 2), p);
clickRegion = rect;
this.pos = pos;
}
For all those who wanna use a library for this, check this out:
https://github.com/chthai64/SwipeRevealLayout
And, for a stripped down version of this lib, checkout:
https://android.jlelse.eu/android-recyclerview-swipeable-items-46a3c763498d
P.S. You can create any custom layout (even with Image Buttons) as your hidden layout using these.
Following Wenxi Zeng's answer here, if you want to have the text in the buttons on multiple lines, replace UnderlayButton's onDraw method with this:
public void onDraw(Canvas canvas, RectF rect, int pos){
Paint p = new Paint();
// Draw background
p.setColor(color);
canvas.drawRect(rect, p);
// Draw Text
TextPaint textPaint = new TextPaint();
textPaint.setTextSize(UtilitiesOperations.convertDpToPx(getContext(), 15));
textPaint.setColor(Color.WHITE);
StaticLayout sl = new StaticLayout(text, textPaint, (int)rect.width(),
Layout.Alignment.ALIGN_CENTER, 1, 1, false);
canvas.save();
Rect r = new Rect();
float y = (rect.height() / 2f) + (r.height() / 2f) - r.bottom - (sl.getHeight() /2);
canvas.translate(rect.left, rect.top + y);
sl.draw(canvas);
canvas.restore();
clickRegion = rect;
this.pos = pos;
}
If you want button(s) on left side as well when swipe in the other direction, just try to add this simple lines in the existing answer:
In the drawButtons method:
private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX) {
float right = itemView.getRight();
float left = itemView.getLeft();
float dButtonWidth = (-1) * dX / buffer.size();
for (UnderlayButton button : buffer) {
if (dX < 0) {
left = right - dButtonWidth;
button.onDraw(
c,
new RectF(
left,
itemView.getTop(),
right,
itemView.getBottom()
),
pos, dX //(to draw button on right)
);
right = left;
} else if (dX > 0) {
right = left - dButtonWidth;
button.onDraw(c,
new RectF(
right,
itemView.getTop(),
left,
itemView.getBottom()
), pos, dX //(to draw button on left)
);
}
}
}
In the onDraw method check the value of dX and set the text and the colour of the buttons:
public void onDraw(Canvas c, RectF rect, int pos, float dX) {
Paint p = new Paint();
// Draw background
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (dX > 0)
p.setColor(Color.parseColor("#23d2c5"));
else if (dX < 0)
p.setColor(Color.parseColor("#23d2c5"));
c.drawRect(rect, p);
// Draw Text
p.setColor(Color.WHITE);
p.setTextSize(36);
// p.setTextSize(LayoutHelper.getPx(MyApplication.getAppContext(), 12));
Rect r = new Rect();
float cHeight = rect.height();
float cWidth = rect.width();
p.setTextAlign(Paint.Align.LEFT);
p.getTextBounds(text, 0, text.length(), r);
float x = cWidth / 2f - r.width() / 2f - r.left;
float y = cHeight / 2f + r.height() / 2f - r.bottom;
if (dX > 0) {
p.setColor(Color.parseColor("#23d2c5"));
c.drawText("Reject", rect.left + x, rect.top + y, p);
} else if (dX < 0) {
c.drawText(text, rect.left + x, rect.top + y, p);
}
clickRegion = rect;
this.pos = pos;
}
}
I am late to the party but if anyone looks for an UIKit UITableView delete button behaviour then you can use something like this with a RecyclerView in Xamarin.Android:
public class SwipeDeleteHelper : ItemTouchHelper.Callback
{
private int _startingWidth = 0;
private bool? _rightAlignedText = null;
private bool _alreadyClicked = false;
private static float _previousDx = float.NegativeInfinity;
private static float _viewWidth = float.NegativeInfinity;
private static float _permanentlyDeleteThreshold = float.NegativeInfinity;
private static RecyclerView.ViewHolder _currentViewHolder;
private RecyclerView.ViewHolder CurrentViewHolder
{
get => _currentViewHolder;
set
{
_startingWidth = 0;
_rightAlignedText = null;
_alreadyClicked = false;
_previousDx = float.NegativeInfinity;
_currentViewHolder = value;
}
}
/*
You can create a method in a utility class for the buttonwidth conversion like this:
public static float GetPxFromDp(float dp)
{
return dp * Application.Context.ApplicationContext.Resources.DisplayMetrics.Density;
}
Also you can use text width measurement to determine the optimal width of the button for your delete text.
*/
public static int buttonWidth = 60 * Application.Context.ApplicationContext.Resources.DisplayMetrics.Density;
public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
{
if (viewHolder is EntryCell)
{
return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.Left | ItemTouchHelper.Start | ItemTouchHelper.Right | ItemTouchHelper.End);
}
return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.ActionStateIdle);
}
public override void OnSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
{
if (float.IsNegativeInfinity(_permanentlyDeleteThreshold))
{
_viewWidth = viewHolder.ItemView.Width;
_permanentlyDeleteThreshold = (viewHolder.ItemView.Width * 3f / 4f);
}
if (viewHolder != CurrentViewHolder)
{
if (viewHolder != null) // This is a new selection and the button of the previous viewHolder should get hidden.
{
(CurrentViewHolder as EntryCell)?.ResetView(CurrentViewHolder);
CurrentViewHolder = viewHolder;
}
else if (CurrentViewHolder != null) // This is the end of the previous selection
{
var hidden = CurrentViewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);
_previousDx = float.NegativeInfinity;
if (hidden.LayoutParameters.Width > _permanentlyDeleteThreshold && !_alreadyClicked) // released in permanent delete area
{
_alreadyClicked = true;
hidden.LayoutParameters.Width = CurrentViewHolder.ItemView.Width;
hidden.CallOnClick();
CurrentViewHolder = null;
}
else
{
_startingWidth = hidden.LayoutParameters.Width >= buttonWidth ? buttonWidth : 0;
hidden.LayoutParameters.Width = _startingWidth;
}
AlignDeleteButtonText(hidden);
hidden.RequestLayout();
}
}
base.OnSelectedChanged(viewHolder, actionState);
}
public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
{
if (actionState == ItemTouchHelper.ActionStateSwipe && !_alreadyClicked)
{
var hidden = viewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);
if (isCurrentlyActive) // swiping
{
if (float.IsNegativeInfinity(_previousDx)) // This is a new swipe
{
_previousDx = dX;
}
if (Math.Abs(dX - _previousDx) > 0.1f && Math.Abs(dX - (-_viewWidth)) > 0.1f)
{
hidden.LayoutParameters.Width = Math.Max(0, (int)Math.Round(hidden.LayoutParameters.Width - (dX >= _previousDx ? 1 : -1) * (Math.Abs(dX - _previousDx))));
_previousDx = dX;
AlignDeleteButtonText(hidden);
hidden.RequestLayout();
}
}
}
}
private void AlignDeleteButtonText(Button hidden)
{
if (_rightAlignedText != false && hidden.LayoutParameters.Width >= _permanentlyDeleteThreshold) // pulled into permanent delete area
{
hidden.Gravity = GravityFlags.AxisSpecified | GravityFlags.AxisPullBefore | GravityFlags.CenterVertical;
_rightAlignedText = false;
}
else if (_rightAlignedText != null && hidden.LayoutParameters.Width <= buttonWidth)
{
hidden.Gravity = GravityFlags.Center;
_rightAlignedText = null;
}
else if (_rightAlignedText != true && hidden.LayoutParameters.Width > buttonWidth && hidden.LayoutParameters.Width < _permanentlyDeleteThreshold) // pulled back from permanent delete area
{
hidden.Gravity = GravityFlags.AxisSpecified | GravityFlags.AxisPullAfter | GravityFlags.CenterVertical;
_rightAlignedText = true;
}
}
public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; }
public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction) { }
}
The EntryCell is a descendant of MvxRecyclerViewHolder and it should contain something like this:
public class EntryCell : MvxRecyclerViewHolder
{
public EntryCell(View itemView, IMvxAndroidBindingContext context) : base(itemView, context)
{
Button _delButton = itemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);
_delButton.Text = "Delete";
}
public void ResetView(RecyclerView.ViewHolder currentViewHolder)
{
var hidden = currentViewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);
hidden.LayoutParameters.Width = 0;
hidden.RequestLayout();
}
}
Your view should have a button (Referenced in EntryCell as Resource.Id.fileListDeleteButton so the ID of the button is fileListDeleteButton) in it. I use an XML as a view and it looks like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="vertical">
<!-- The rest of your code... -->
<Button
android:id="#+id/fileListDeleteButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:paddingHorizontal="#dimen/abc_button_padding_horizontal_material"
android:background="#f00"
android:textColor="#android:color/white"
android:textAllCaps="false"
android:singleLine="true"
android:ellipsize="none"
android:text="dummy" />
</RelativeLayout>
In your code, where the RecyclerView is, use it like this:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeDeleteHelper());
itemTouchHelper.AttachToRecyclerView(yourRecyclerView);
I hope this helps someone.
Since I haven't seen anywhere how to implement this and I did manage to get this to work, I will post a solution to this problem that is working however it is in c# Xamarin Android.
If you need it native android you will have to convert it android native which shouldn't be really hard. I might do this at a later date if very requested.
This is my ItemHelper base class:
internal abstract class ItemTouchHelperBase : ItemTouchHelper.Callback
{
protected RecyclerViewAdapterBase adapter;
public int currentPosition = -1;
public Rect ItemRect = new Rect();
private Paint backgroundPaint = new Paint();
private Rect backgroundBounds = new Rect();
private TextPaint textPaint = new TextPaint();
private string deleteText;
private readonly float textWidth;
private readonly float textHeight;
public ItemTouchHelperBase()
{
backgroundPaint.Color = new Color(ContextCompat.GetColor(Application.Context, Resource.Color.delete_red));
textPaint.Color = Color.White;
textPaint.AntiAlias = true;
textPaint.TextSize = FontHelper.GetFontSize(Application.Context, Resource.Dimension.font_size_button);
deleteText = " " + StringResource.delete + " ";
Rect textBounds = new Rect();
textPaint.GetTextBounds(deleteText, 0, deleteText.Length, textBounds);
textHeight = textBounds.Height();
textWidth = textPaint.MeasureText(deleteText);
}
public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
{
return false;
}
public override void ClearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
{
if (adapter != null)
{
ItemRect = new Rect();
}
base.ClearView(recyclerView, viewHolder);
}
public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
{
}
public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
{
// Note: Don't create variables inside OnDraw due to performance issues
try
{
if (actionState == ItemTouchHelper.ActionStateSwipe)
{
if (dX <= 0) // Left swipe
{
// Swipe up to text width accordingly to ratio
dX /= viewHolder.ItemView.Right / textWidth;
//Draw background
backgroundBounds = new Rect(
viewHolder.ItemView.Right + (int) dX,
viewHolder.ItemView.Top,
viewHolder.ItemView.Right,
viewHolder.ItemView.Bottom);
c.DrawRect(backgroundBounds, backgroundPaint);
if (adapter != null)
{
ItemRect = backgroundBounds;
}
//Draw text
c.DrawText(
deleteText,
(float) viewHolder.ItemView.Right - textWidth, viewHolder.ItemView.Top + (viewHolder.ItemView.Height / 2) + (textHeight / 2),
textPaint);
}
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
catch (Exception)
{
}
}
internal void AttachToRecyclerview(RecyclerView recycleview)
{
new ItemTouchHelper(this).AttachToRecyclerView(recycleview);
}
public void ClickOutsideDeleteButton()
{
try
{
if (currentPosition != -1)
{
PutRowBackToDefault();
}
}
catch (Exception)
{
}
}
protected void PutRowBackToDefault()
{
adapter.NotifyItemChanged(currentPosition);
currentPosition = -1;
}
}
Then on your item helper class you have:
internal class MyItemsTouchHelperCallback : ItemTouchHelperBase
{
public MyItemsTouchHelperCallback (MyAdapter adapter)
{
this.adapter = adapter;
}
public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
{
try
{
if (currentPosition != -1 && currentPosition != viewHolder.AdapterPosition)
{
PutRowBackToDefault();
}
currentPosition = viewHolder.AdapterPosition;
}
catch (Exception)
{
}
int swipeFlags = viewHolder is MyViewHolder ? ItemTouchHelper.Start : ItemTouchHelper.ActionStateIdle;
return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, swipeFlags);
}
}
Then on your activity you have:
Put this OnCreate
recycleViewLayoutManager = new LinearLayoutManager(this);
recycler_view_main.SetLayoutManager(recycleViewLayoutManager);
recyclerAdapter = new MyAdapter(this, this);
recycler_view_main.SetAdapter(recyclerAdapter);
myItemsTouchHelperCallback = new MyItemsTouchHelperCallback (recyclerAdapter);
myItemsTouchHelperCallback .AttachToRecyclerview(recycler_view_main);
Then on activity you override this method:
public override bool DispatchTouchEvent(MotionEvent e)
{
int[] recyclerviewLocationOnScreen = new int[2];
recycler_view_main.GetLocationOnScreen(recyclerviewLocationOnScreen);
TouchEventsHelper.TouchUpEvent(
e.Action,
e.GetX() - recyclerviewLocationOnScreen[0],
e.GetY() - recyclerviewLocationOnScreen[1],
myItemsTouchHelperCallback .ItemRect,
delegate
{
// Delete your row
},
delegate
{ myItemsTouchHelperCallback .ClickOutsideDeleteButton(); });
return base.DispatchTouchEvent(e);
}
This is the helper method i created to be used by the dispatch event:
internal static void TouchUpEvent(MotionEventActions eventActions, float x, float y, Rect rectangle, Action ActionDeleteClick, Action NormalClick)
{
try
{
if (rectangle.Contains((int) x, (int) y))
{
//inside delete button
if (eventActions == MotionEventActions.Down)
{
isClick = true;
}
else if (eventActions == MotionEventActions.Up || eventActions == MotionEventActions.Cancel)
{
if (isClick)
{
ActionDeleteClick.Invoke();
}
}
}
else if (eventActions == MotionEventActions.Up ||
eventActions == MotionEventActions.Cancel ||
eventActions == MotionEventActions.Down)
{
//click anywhere outside delete button
isClick = false;
if (eventActions == MotionEventActions.Down)
{
NormalClick.Invoke();
}
}
}
catch (Exception)
{
}
}
It is a bit complex but it works well. I have tested this in many ways. Let me know if you have any trouble implementing this
SwipeHelper in Kotlin, it show only icon not text but if you want I can give code for text. I changed it from Wenxi Zeng answer to Kotlin and added some additions, Thanh-Nhon Nguyen's answer doesn't work properly when you return swipped item.
SwipeHelper Class
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import java.util.*
#SuppressLint("ClickableViewAccessibility")
abstract class SwipeHelp( context: Context?, recyclerView: RecyclerView, animate: Boolean) :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
private val recyclerView: RecyclerView
private lateinit var buttons: MutableList<UnderlayButton>
private var swipedPos = -1
private var swipeThreshold = 0.5f
private val buttonsBuffer: MutableMap<Int, MutableList<UnderlayButton>>
private var animate: Boolean? = null
private val recoverQueue = object : LinkedList<Int>() {
override fun add(element: Int): Boolean {
return if (contains(element)) false else super.add(element)
}
}
private val gestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
for (button in buttons) {
if (button.onClick(e.x, e.y)) break
}
return true
}
}
private val gestureDetector = GestureDetector(context, gestureListener)
#SuppressLint("ClickableViewAccessibility")
private val onTouchListener = OnTouchListener { _, e ->
if (swipedPos < 0) {
return#OnTouchListener false
}
val point = Point(e.rawX.toInt(), e.rawY.toInt())
val swipedViewHolder = recyclerView.findViewHolderForAdapterPosition(swipedPos)
val swipedItem = swipedViewHolder!!.itemView
val rect = Rect()
swipedItem.getGlobalVisibleRect(rect)
if (e.action == MotionEvent.ACTION_DOWN || e.action == MotionEvent.ACTION_UP || e.action == MotionEvent.ACTION_MOVE) {
if (rect.top < point.y && rect.bottom > point.y) {
gestureDetector.onTouchEvent(e)
} else {
recoverQueue.add(swipedPos)
swipedPos = -1
recoverSwipedItem()
}
}
false
}
init {
this.animate = animate
this.recyclerView = recyclerView
buttons = ArrayList()
this.recyclerView.setOnTouchListener(onTouchListener)
buttonsBuffer = HashMap()
attachSwipe()
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val pos = viewHolder.adapterPosition
if (swipedPos != pos) recoverQueue.add(swipedPos)
swipedPos = pos
if (buttonsBuffer.containsKey(swipedPos))
buttons = buttonsBuffer[swipedPos]!!
else
buttons.clear()
buttonsBuffer.clear()
swipeThreshold = 0.5f * buttons.size * BUTTON_WIDTH
recoverSwipedItem()
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
return swipeThreshold
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return 0.1f * defaultValue
}
override fun getSwipeVelocityThreshold(defaultValue: Float): Float {
return 5.0f * defaultValue
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val pos = viewHolder.adapterPosition
var translationX = dX
val itemView = viewHolder.itemView
if (pos < 0) {
swipedPos = pos
return
}
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
if (dX < 0) {
var buffer: MutableList<UnderlayButton> = ArrayList()
if (!buttonsBuffer.containsKey(pos)) {
instantiateUnderlayButton(viewHolder, buffer)
buttonsBuffer[pos] = buffer
} else {
buffer = buttonsBuffer[pos]!!
}
translationX = dX * buffer.size * BUTTON_WIDTH / itemView.width
drawButtons(c, itemView, buffer, pos, translationX)
}
}
super.onChildDraw(
c,
recyclerView,
viewHolder,
translationX,
dY,
actionState,
isCurrentlyActive
)
}
#Synchronized
private fun recoverSwipedItem() {
while (!recoverQueue.isEmpty()) {
val pos = recoverQueue.poll()
if (pos!! > -1) {
recyclerView.adapter!!.notifyItemChanged(pos)
}
}
}
private fun drawButtons(
c: Canvas,
itemView: View,
buffer: List<UnderlayButton>,
pos: Int,
dX: Float
) {
var right = itemView.width.toFloat()
//val left = itemView.width * 0.75.toFloat()
for (button in buffer) {
val left = right - (-1 * dX / buffer.size)
button.onDraw(
c,
RectF(
left,
itemView.top.toFloat(),
right,
itemView.bottom.toFloat()
),
pos
)
right = left
}
}
private fun attachSwipe() {
val itemTouchHelper = ItemTouchHelper(this)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
abstract fun instantiateUnderlayButton(
viewHolder: RecyclerView.ViewHolder?,
underlayButtons: MutableList<UnderlayButton>?
)
class UnderlayButton(
private val imageResId: Drawable?,
private val buttonBackgroundColor: Int,
private val clickListener: UnderlayButtonClickListener
) {
private var pos = 0
private var clickRegion: RectF? = null
fun onClick(x: Float, y: Float): Boolean {
if (clickRegion != null && clickRegion!!.contains(x, y)) {
clickListener.onClick(pos)
return true
}
return false
}
fun onDraw(canvas: Canvas, rect: RectF, pos: Int) {
val p = Paint()
p.color = buttonBackgroundColor
canvas.drawRect(rect, p)
if (!animate) {
val cHeight = rect.height()
if (imageResId != null) {
imageResId.setBounds(
(rect.left + 50).toInt(),
(rect.top + cHeight / 2f).toInt(),
(rect.right - 50).toInt(),
(rect.bottom - cHeight / 10f).toInt()
)
imageResId.draw(canvas)
}
} else {
if (imageResId != null) {
imageResId.setBounds(
(rect.left + 50).toInt(),
(rect.top + 15).toInt(),
(rect.right - 50).toInt(),
(rect.bottom - 15).toInt()
)
imageResId.draw(canvas)
}
canvas.save()
canvas.restore()
}
clickRegion = rect
this.pos = pos
}
}
interface UnderlayButtonClickListener {
fun onClick(pos: Int)
}
companion object {
const val BUTTON_WIDTH = 200
private const val animate: Boolean = true
}
}
Usage:
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var adapter: SwipeAdapter
private val swipeArrayList: ArrayList<String> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
swipeArrayList.add("11111")
swipeArrayList.add("22222")
swipeArrayList.add("33333")
swipeArrayList.add("44444")
swipeArrayList.add("55555")
binding.recyclerView.layoutManager = LinearLayoutManager(this)
adapter = SwipeAdapter(swipeArrayList)
binding.recyclerView.adapter = adapter
object : SwipeHelp(this#MainActivity , binding.recyclerView, false) {
#SuppressLint("ResourceType")
override fun instantiateUnderlayButton(
viewHolder: RecyclerView.ViewHolder?,
underlayButtons: MutableList<UnderlayButton>?
) {
underlayButtons?.add(UnderlayButton(
ResourcesCompat.getDrawable (this#MainActivity.resources, R.drawable.trash_icon, null),
Color.BLUE,
object : UnderlayButtonClickListener {
override fun onClick(pos: Int) {
swipeArrayList.removeAt(viewHolder!!.adapterPosition)
adapter.notifyItemRemoved(viewHolder.adapterPosition)
}
}
))
}
}
}
}
If you use a RecyclerView, try to use OnScrollListener. Do something like this.
private class YourOnScrollListener extends RecyclerView.OnScrollListener {
private boolean directionLeft;
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (directionLeft) drawButtons();
//Draw buttons here if you want them to be drawn after scroll is finished
//here you can play with states, to draw buttons or erase it whenever you want
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dx < 0) directionLeft = true;
else directionLeft = false;
}
}
I wanted to use this touch gesture in my app too, after working too much with Itemtouchhelper I decided to write my own touch handler:
private class TouchHelper : Java.Lang.Object, View.IOnTouchListener
{
ViewHolder vh;
public TouchHelper(ViewHolder vh)
{ this.vh = vh; }
float DownX, DownY; bool isSliding;
TimeSpan tsDown;
public bool OnTouch(View v, MotionEvent e)
{
switch (e.Action)
{
case MotionEventActions.Down:
DownX = e.GetX(); DownY = e.GetY();
tsDown = TimeSpan.Now;
break;
case MotionEventActions.Move:
float deltaX = e.GetX() - DownX, deltaY = e.GetX() - DownY;
if (Math.Abs(deltaX) >= Values.ScreenWidth / 20 || Math.Abs(deltaY) >= Values.ScreenWidth / 20)
isSliding = Math.Abs(deltaX) > Math.Abs(deltaY);
//TextsPlace is the layout that moves with touch
if(isSliding)
vh.TextsPlace.TranslationX = deltaX / 2;
break;
case MotionEventActions.Cancel:
case MotionEventActions.Up:
//handle if touch was for clicking
if (Math.Abs(deltaX) <= 50 && (TimeSpan.Now - tsDown).TotalMilliseconds <= 400)
vh.OnTextsPlaceClick(vh.TextsPlace, null);
break;
}
return true;
}
}
Note: Set this as ontouchlistener of your viewholder content when creating the viewholder. You can add your animations to return the item to its first place.
You can also write your custom layoutmanager to block vertical scroll while item is sliding.
I have a much simpler solution:
Add a button to your row XML, width 0, floating on the right:
> <Button
> android:id="#+id/hidden"
> android:layout_width="0dp"
> android:layout_height="match_parent"
> android:layout_alignParentRight = "true">
in onChildDraw(), just increase its width by the dX value.
int position = viewHolder.getAdapterPosition();
View v = recyclerView.getLayoutManager().findViewByPosition(position);
Button hidden = v.findViewById(R.id.hidden);
hidden.setLayoutParams(new LinearLayout.LayoutParams((int)-dX, -1));
Make sure not to call the default super.onChildDraw()

Scroll Webview in Viewpager

I have a WebView in a ViewPager.
And the ViewPager seems to consume all the horizontal scrolling, so that I can't scroll in the WebView (vertical works).
How can I achieve that the WebView has got priority consuming the horizontal scrolling?
don't ask me why this code gets formatted like this,
1. implement a custom ViewPager Instance like this:
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
public class CustomViewPager extends ViewPager {
private MagazineWebView_WithoutFlipWebView mCurrentPageWebView_; //custom webview
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (Constants.LOGGING) {
Log.v(Constants.LOG_OEAMTC_APP, "CustomViewPager - onInterceptTouchEvent");
}
// if view zoomed out (view starts at 33.12... scale level) ... allow
// zoom within webview, otherwise disallow (allow viewpager to change
// view)
if (mCurrentPageWebView_ != null && (mCurrentPageWebView_.getScale() * 100) > 34) {
Log.v(Constants.LOG_OEAMTC_APP, "CustomViewPager - intrcepted: " + String.valueOf((mCurrentPageWebView_.getScale() * > 100)));
this.requestDisallowInterceptTouchEvent(true);
}
else {
if (mCurrentPageWebView_ != null) {
Log.v(Constants.LOG_OEAMTC_APP,
"CustomViewPager - not intrcepted: " + String.valueOf(mCurrentPageWebView_.getScale() * 100));
}
this.requestDisallowInterceptTouchEvent(false);
}
return super.onInterceptTouchEvent(event);
}
public MagazineWebView_WithoutFlipWebView getCurrentPageWebView() {
return mCurrentPageWebView_;
}
public void setCurrentPageWebView(MagazineWebView_WithoutFlipWebView currentPageWebView) {
mCurrentPageWebView_ = currentPageWebView;
}
}
2. in your main (ViewPager) Activity add the following lines to the view pager
mViewPager_ = new AwesomePagerAdapter();
viewpapgerInLayout = (CustomViewPager) findViewById(R.id.awesomepager);
viewpapgerInLayout.setAdapter(mViewPager_);
viewpapgerInLayout.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
viewpapgerInLayout.setCurrentPageWebView(mLstPagesWebviews_.get(position));
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
3. finally, run it :=) if the zoom level is at initial zoom,
changes pages is allowed, all the other time you can navigate your web view
Ps.: *Don't forget* to change your ViewPager in your *.xml file with the CustomViewPager Class you just created
good luck :)
Try this
WebView mWebView = (WebView) findViewById(R.id.MyWebview);
mWebView.getSettings().setLoadWithOverviewMode(true);
mWebView.getSettings().setUseWideViewPort(true);
mWebView.getSettings().setSupportZoom(true);
mWebView.getSettings().setBuiltInZoomControls(true);
mWebView.getSettings().setUserAgentString("Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3");
Late to this question and I come up with a different approach to fix this issue by override the WebView onTouchEvent method. It works on both ViewPager and ViewPager2. Hope this would help the future SO.
Note: Replace your WebView with NestedScrollWebView
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.webkit.WebView
import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingChildHelper
import androidx.core.view.ViewCompat
class NestedScrollWebView : WebView, NestedScrollingChild {
companion object {
val TAG: String = NestedScrollWebView::class.java.simpleName
}
private var lastMotionX: Int = 0
private var lastMotionY: Int = 0
private val scrollOffset = IntArray(2)
private val scrollConsumed = IntArray(2)
private var nestedOffsetY: Int = 0
private var childHelper = NestedScrollingChildHelper(this)
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
isNestedScrollingEnabled = true
}
override fun onTouchEvent(event: MotionEvent): Boolean {
var result = false
val trackedEvent = MotionEvent.obtain(event)
val action = event.actionMasked
if (action == MotionEvent.ACTION_DOWN) {
nestedOffsetY = 0
}
val x = event.x.toInt()
val y = event.y.toInt()
event.offsetLocation(0f, nestedOffsetY.toFloat())
when (action) {
MotionEvent.ACTION_DOWN -> {
lastMotionX = x
lastMotionY = y
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
result = super.onTouchEvent(event)
}
MotionEvent.ACTION_MOVE -> {
var deltaY = lastMotionY - y
if (kotlin.math.abs(deltaY) > kotlin.math.abs(lastMotionX - x) &&
(canScrollVertically(1) || canScrollVertically(-1))
) {
requestDisallowInterceptTouchEvent(true)
}
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
deltaY -= scrollConsumed[1]
trackedEvent.offsetLocation(0f, scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
}
lastMotionY = y - scrollOffset[1]
val oldY = scrollY
val newScrollY = Math.max(0, oldY + deltaY)
val dyConsumed = newScrollY - oldY
val dyUnconsumed = deltaY - dyConsumed
if (dispatchNestedScroll(0, dyConsumed, 0, dyUnconsumed, scrollOffset)) {
lastMotionY -= scrollOffset[1]
trackedEvent.offsetLocation(0f, scrollOffset[1].toFloat())
nestedOffsetY += scrollOffset[1]
}
result = super.onTouchEvent(trackedEvent)
trackedEvent.recycle()
}
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
stopNestedScroll()
requestDisallowInterceptTouchEvent(false)
result = super.onTouchEvent(event)
}
}
return result
}
// NestedScrollingChild
override fun setNestedScrollingEnabled(enabled: Boolean) {
childHelper.isNestedScrollingEnabled = enabled
}
override fun stopNestedScroll() {
childHelper.stopNestedScroll()
}
override fun isNestedScrollingEnabled() =
childHelper.isNestedScrollingEnabled
override fun startNestedScroll(axes: Int) =
childHelper.startNestedScroll(axes)
override fun hasNestedScrollingParent() =
childHelper.hasNestedScrollingParent()
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?) =
childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?) =
childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) =
childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) =
childHelper.dispatchNestedPreFling(velocityX, velocityY)
}

Categories

Resources