I have GridViewPager with 2 rows and 3 columns. As its default behavior If u swipe down on any page it will navigate to first page of next row. But i want to override that behaviorand stop Swipe Down on first row second and third columns.
I have tried putting 2 row one column GridViewPager and putting a ViewPager for first cell as well as using 1 row 3 column GridViewPager with putting Another 1 column 2 row GridViewPager in first cell. Both has similar scrolling problems.Bellow code is the custom GridViewPager I used to change scrolling behavior.
GridViewPager should be able swipe only as following picture.Swipe Example Image
public class GridViewPagerCustom extends GridViewPager {
private GestureDetector mGestureDetector;
private int px = 0;
private int py = 0;
public int getPy() {
return py;
}
public void setPy(int py) {
this.py = py;
}
public int getPx() {
return selectedPage;
}
public void setPx(int px) {
this.px = px;
}
public GridViewPagerCustom(Context context, AttributeSet attrs) {
super(context, attrs);
mGestureDetector = new GestureDetector(context, new YScrollDetector());
setFadingEdgeLength(0);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev);
}
// Return false if we're scrolling in the x direction
class YScrollDetector extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return (getPx() == 0 && getPy() == 0) ? Math.abs(distanceY) > Math.abs(distanceX) : false;
}
}
}
You should be able to control that by overriding canScrollVertically(int direction); I haven't tried that but sounds like the right place. That said, please be careful about changing that behavior since it will then be different from other apps that use this component and users of your app may get confused assuming that there are no other rows, etc.
Related
**All of the information below is with regards to the icons illustrated on the right side of the gif.
I have created a simple toolbar that has a button that changes the image based on the position of the fragment. Everything works fine, however, I can't seem to get the fading correctly. I want the images to fade nicely as the fragment changes. Right now, going from the center (position 1) to the left (position 0) has a nice fade. The icon changes from an equalizer to a chat icon. However, going back from left (position 0) to center (position 1) makes the image pop into frame (not a fade). Additionally the same is observed going from center (position 1) to the right (position 2) and vice versa. There is no fading in this case either.
I believe the issue is with the if else statements and the setAlpha.
Position 0 (Left side)
Position 1 (Center)
Position 2 (Right side)
Gif Demo (What current code does):
https://imgur.com/a/vNEJZAJ
Code:
public class TopVariableFunctionalityButtonView extends FrameLayout implements ViewPager.OnPageChangeListener {
// Initialize content
private ImageView mVariableFunctionalityButtonImageView;
private ImageView mVariableFunctionalityButtonBackgroundImageView;
private ArgbEvaluator mArgbEvaluator;
// Initialize color change
private int mCenterVariableFunctionalityButtonColor;
private int mSideVariableFunctionalityButtonColor;
private int mCenterVariableFunctionalityButtonBackgroundColor;
private int mSideVariableFunctionalityButtonBackgroundColor;
public TopVariableFunctionalityButtonView(#NonNull Context context) {
this(context, null);
}
public TopVariableFunctionalityButtonView(#NonNull Context context, #Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TopVariableFunctionalityButtonView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setUpWithViewPager(ViewPager viewPager) {
viewPager.addOnPageChangeListener(this);
mVariableFunctionalityButtonImageView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view) {
if (viewPager.getCurrentItem() == 2)
HomeActivity.openSettings(getContext());
}
});
}
private void init() {
LayoutInflater.from(getContext()).inflate(R.layout.view_top_variable_functionality_button, this, true);
// Initialize top variable functionality button image
mVariableFunctionalityButtonImageView = findViewById(R.id.view_top_variable_functionality_button_image_view);
// Initialize top variable functionality button image background
mVariableFunctionalityButtonBackgroundImageView = findViewById(R.id.view_top_variable_functionality_button_background_image_view);
// Set start color and end color for button icons
mCenterVariableFunctionalityButtonColor = ContextCompat.getColor(getContext(), R.color.white);
mSideVariableFunctionalityButtonColor = ContextCompat.getColor(getContext(), R.color.iconColor);
mCenterVariableFunctionalityButtonBackgroundColor = ContextCompat.getColor(getContext(), R.color.transparentSearch);
mSideVariableFunctionalityButtonBackgroundColor = ContextCompat.getColor(getContext(), R.color.secondaryColor);
// Evaluate the difference between colors
mArgbEvaluator = new ArgbEvaluator();
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (position == 0) {
// Change color of icons based on view
setColor(1 - positionOffset);
// Change variable button image
mVariableFunctionalityButtonImageView.setAlpha(1 - positionOffset);
mVariableFunctionalityButtonImageView.setBackgroundResource(R.drawable.ic_chat);
} else if (position == 1) {
// Change color of icons based on view
setColor(positionOffset + 1);
// Change variable button image
mVariableFunctionalityButtonImageView.setAlpha(positionOffset + 1);
mVariableFunctionalityButtonImageView.setBackgroundResource(R.drawable.ic_equalizer);
} else if (position == 2) {
// Change color of icons based on view
setColor(positionOffset + 1);
// Change variable button image
mVariableFunctionalityButtonImageView.setAlpha(positionOffset + 1);
mVariableFunctionalityButtonImageView.setBackgroundResource(R.drawable.ic_settings);
}
}
#Override
public void onPageSelected(int position) {
}
#Override
public void onPageScrollStateChanged(int state) {
}
private void setColor(float fractionFromCenter) {
int variableFunctionalityButtonColor = (int) mArgbEvaluator.evaluate(fractionFromCenter,
mCenterVariableFunctionalityButtonColor, mSideVariableFunctionalityButtonColor);
int variableFunctionalityButtonBackgroundColor = (int) mArgbEvaluator.evaluate(fractionFromCenter,
mCenterVariableFunctionalityButtonBackgroundColor, mSideVariableFunctionalityButtonBackgroundColor);
mVariableFunctionalityButtonImageView.setColorFilter(variableFunctionalityButtonColor);
mVariableFunctionalityButtonBackgroundImageView.setColorFilter(variableFunctionalityButtonBackgroundColor);
}
}
I have a RecyclerView (say, rootRecyclerView) that can have different kinds of rows depending on some API response. I implemented one of them is a horizontal ViewPager2 and another one is implemented with horizontal RecyclerView (say, childRecyclerView).
The rootRecyclerView swipes vertically whereas the viewPager2 and childRecyclerView swipes horizontally.
The Problem:
When I swipe on the screen, if the swipe is on the the viewPager2 or childRecyclerView, the swipe MUST go perfectly straight horizontally. Otherwise, they won't scroll horizontally; the swipe is taken by the rootRecyclerView and so the you would see vertical movement.
So, this happens because your thumb would move in a curved/circular direction creating movement in both the X axis and Y axis, and the so the rootRecyclerView intercepts the swipe creating this unpleasant user experience.
I did try to solve the issue, such as adding an OnItemTouchListener to the childRecyclerView like this:
private float Y_BUFFER = ViewConfiguration.get(getContext())
.getScaledPagingTouchSlop(); // 10;
private float preX = 0f;
private float preY = 0f;
childRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(#NonNull RecyclerView rv, #NonNull MotionEvent e) {
if(e.getAction()==MotionEvent.ACTION_DOWN){
childRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
}
if(e.getAction() == MotionEvent.ACTION_MOVE){
if (Math.abs(e.getX() - preX) > Math.abs(e.getY() - preY)) {
childRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
} else if (Math.abs(e.getY() - preY) > Y_BUFFER) {
childRecyclerView.getParent().requestDisallowInterceptTouchEvent(false);
}
}
preX = e.getX();
preY = e.getY();
return false;
}
// ... rest of the code
It solves the problem only for the childRecyclerView, but I could not solve it for the ViewPager2.
I have also tried to use GestureDetector as described in this answer link, and some other combinations of code, but I could not make it work.
Could anyone help me?
Okay, so after some research, I came to the conclusion of substituting my ViewPager2 with a recyclerView that will 'behave like' a viewPager :/ .
First I replaced my viewPager2 with a horizontal recyclerView. To make it behave like a viewpager, use SnapHelper.
RecyclerView childRecyclerView2 = findViewById(R.id.previously_viewPager);
// other init like setup layout manager, adapter etc
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(replacedRecyclerView); // <-- this makes out rv behave like a viewPager
After that, you have to add an OnItemTouchListener and override onInterceptTouchEvent just like the code segment in my question:
childRecyclerView2.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(#NonNull RecyclerView rv, #NonNull MotionEvent e) {
// same as the code segment in the question,
//so skipping this part.
//just copy it from my question
}
// ...
}
Optional:
In viewPager2, you can get the current focus with getCurrentItem(), but since we have replaced out viewpager2 with recyclerview, we don't have that method. So, we need to implement our own equivalent version. If you are a Kotlin guy, you can directly jump to the reference 2 and skip this part. Here is the java version if you need, I'll skip the explanation though.
Create SnapHelperExt.java
public class SnapHelperExt {
public SnapHelperExt(){}
public int getSnapPosition(RecyclerView recyclerView, SnapHelper snapHelper){
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
View snapView = snapHelper.findSnapView(layoutManager);
if (snapView != null) {
return layoutManager.getPosition(snapView);
}else{
return -1;
}
}
}
Next create an interface OnSnapPositionChangeListener as our listener :
public interface OnSnapPositionChangeListener {
void onSnapPositionChange(int position);
}
After that, create SnapOnScrollListener.java:
public class SnapOnScrollListener extends RecyclerView.OnScrollListener {
public enum Behavior {
NOTIFY_ON_SCROLL,
NOTIFY_ON_SCROLL_STATE_IDLE
}
private SnapHelperExt snapHelperExt;
private SnapHelper snapHelper;
private Behavior behavior;
private OnSnapPositionChangeListener onSnapPositionChangeListener;
private int snapPosition = RecyclerView.NO_POSITION;
public SnapOnScrollListener(SnapHelper snapHelper, Behavior behavior, OnSnapPositionChangeListener onSnapPositionChangeListener){
this.snapHelper = snapHelper;
this.behavior = behavior;
this.onSnapPositionChangeListener = onSnapPositionChangeListener;
this.snapHelperExt = new SnapHelperExt();
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (behavior == Behavior.NOTIFY_ON_SCROLL) {
maybeNotifySnapPositionChange(recyclerView);
}
}
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
maybeNotifySnapPositionChange(recyclerView);
}
}
private void maybeNotifySnapPositionChange(RecyclerView recyclerView){
int prevPosition = this.snapHelperExt.getSnapPosition(recyclerView, snapHelper);
boolean snapPositionIsChanged = (this.snapPosition!=prevPosition);
if(snapPositionIsChanged){
onSnapPositionChangeListener.onSnapPositionChange(prevPosition);
this.snapPosition = prevPosition;
}
}
}
Finally, use it in this way:
SnapOnScrollListener snapOnScrollListener = new SnapOnScrollListener(
snapHelper,
SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL,
position -> {
Log.e(TAG, "currently focused page no = "+position);
// your code here, do whatever you want
}
);
childRecyclerView2.addOnScrollListener(snapOnScrollListener);
References:
create-viewpager-using-recyclerview
detecting-snap-changes-with-androids-recyclerview
I have a RelativeLayout that contains many child views with various touch events. I want to get notified when the user swipes anywhere on the parent RelativeLayout so I can update some UI while still letting the child views handle their own touch/drag events. What is the standard way of accomplishing this for Android?
I was thinking that I could put an overlay over all the views and have it detect swipe gestures and if it wasn't a swipe I could pass the touch event on to other views in the hierarchy. It doesn't seem like Android supports that sort of touch detection and once one view decides to see if a event is a certain gesture no other views will be able to see the events.
A swipe gesture consists of three touch events: ACTION_DOWN, ACTION_MOVE and ACTION_UP. You need to record all three events and then see if it was a swipe or not. If it was not a swipe then we would need to pass those events to other child views to see if it meets their criteria for the gesture they are looking for. If it is a swipe we would want to block the events from being sent to the child view. Just not sure if this is actually possible.
Update
Using the ideas of the users in the answers section I was able to write a layout that met my specification. This RelativeLayout just handles right and left swipes but could be added to to handle more directions. OnSwipeListener is just an interface with two methods void swipedLeft() and void swipedRight().
public class SwipeRelativeLayout extends RelativeLayout {
public OnSwipeListener mSwipeListener = null;
private static final int SWIPE_DISTANCE_THRESHOLD = 100;
private float mStartX = 0;
private float mStartY = 0;
private float mEndX = 0;
private float mEndY = 0;
public SwipeRelativeLayout(Context context) {
super(context);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SwipeRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean handled = onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) return handled;
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mStartX = event.getRawX();
mStartY = event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
float distanceX = event.getRawX() - mStartX;
float distanceY = event.getRawY() - mStartY;
if (Math.abs(distanceX) > Math.abs(distanceY) && Math.abs(distanceX) > SWIPE_DISTANCE_THRESHOLD) {
if (distanceX > 0) {
if (mSwipeListener != null) mSwipeListener.swipedRight();
} else {
if (mSwipeListener != null) mSwipeListener.swipedLeft();
}
return true;
}
return false;
}
}
return true;
}
}
When a touch event occurs it is first passed to the parent layout view and passed on to child view via onInterceptTouchEvent returning true or false. You want to override and intercept the touch events on the parent RelativeLayout and determine if you have seen a swipe gesture or not. If you have seen a swipe you want to return that you have handled it. In this case ACTION_UP is the end of a possible swipe and if your onTouchEvent handled the event then you can return true and the views below it will not get the finishing event and thus ignore their gestures.
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean handled = onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) return handled;
return false;
}
I am creating this app.
code of my onsingletapup.cs file
class SingleTapUp : Android.Views.GestureDetector.SimpleOnGestureListener
{
public override bool OnSingleTapUp(MotionEvent e) {
// Toast.MakeText(this,, ToastLength.Long).Show();
return true;
}
}
here is my mainactivity.cs
public class MainActivity : ActionBarActivity, View.IOnTouchListener
{
GestureDetector gestureDetector;
float _viewX;
float _viewY;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
PopulateListView(someList,anynumbertoshow)
}
private void QueueListView(Queue<FeedItem> feedItemsList, int count)
{
RelativeLayout rl = this.FindViewById<RelativeLayout>(Resource.Id.newsContainer);
if(rl.Visibility == ViewStates.Gone)
{
this.FindViewById<LinearLayout>(Resource.Id.newsList).Visibility = ViewStates.Gone;
rl.Visibility = ViewStates.Visible;
}
Paint layerPaint = new Paint();
layerPaint.AntiAlias = true;
layerPaint.FilterBitmap = true;
layerPaint.Dither = true;
// RelativeLayout parentLayout = (RelativeLayout)LayoutInflater.from(context).inflate(R.layout.myLayout, null);
rl.SetLayerType(LayerType.Hardware, layerPaint);
rl.SetClipChildren(false);
Random rnd = new Random();
//this.progressDialog.Dismiss();
for (int i = 0; i < count; i++)
{
FeedItem rss = theNewsQueue.Dequeue();
var viewObj = this.LayoutInflater.Inflate(Resource.Layout.NewTile, rl, false);
TextView tv = viewObj.FindViewById<TextView>(Resource.Id.textView2);
TextView link = viewObj.FindViewById<TextView>(Resource.Id.link);
link.Text = rss.Link;
tv.Text = rss.Title;
viewObj.Rotation = angle;
angle = rnd.Next(-3, 3);
viewObj.SetLayerType(LayerType.Hardware, layerPaint);
rl.AddView(viewObj);
gestureDetector = new GestureDetector(this, new SingleTapUp());
viewObj.SetOnTouchListener(this); //Here I am adding my listener to all my control
rl.SetLayerType(LayerType.Hardware, layerPaint);
theNewsQueue.Enqueue(rss);
rss = null;
}
}
public bool OnTouch(View v, MotionEvent e)
{
if (gestureDetector.OnTouchEvent(e))
{
//will detect a click and open in browser
return true;
}
else
{
int initialTouchX = 0, initialTouchY = 0;
int newx = 0;
var x = v.Left;
switch (e.Action)
{
case MotionEventActions.Down:
{
_viewX = e.GetX();
_viewY = e.GetY();
initialTouchX = (int)e.RawX;
initialTouchY = (int)e.RawY;
break;
}
case MotionEventActions.Up:
{
int lastX = (int)e.GetX();
int lastY = (int)e.GetY();
if ((x - newx) > 40)
{
//right Swipe
sendViewToBack(v);
}
else if ((newx - x > 40))
{
//left Swipe
sendViewToBack(v);
}
break;
}
case MotionEventActions.Move:
{
// click = false;
var left = (int)(e.RawX - _viewX);
newx = left;
var right = (int)(left + v.Width);
var top = (int)(e.RawY - _viewY);
var bottom = (int)(top + v.Height);
v.Layout(left, top, right, bottom);
break;
}
}
}
// _gestureDetector.OnTouchEvent(e);
return true;
}
public void sendViewToBack(View child)
{
var parent = (ViewGroup)child.Parent;
if (null != parent)
{
parent.RemoveView(child);
if(viewType==0)
parent.AddView(QueueListView (theNewsQueue), 0);
else
parent.AddView(QueueListView (theNewsQueue), parent.ChildCount-1);
}
}
}
Now my question is on some devices my current code is giving some abnormal behavior. Like even if I perform OnSingleTapUp() which is supposed to perform click operation but it is performing a move operation. My question is what is wrong with my code so that it is not working correctly. Any help will be greatly appreciated. Thank you.
The onTouch and onClick doesn't work together. In all the cases the onTouch is going to get the priority, in fact onClick in also sort of fine implementation of onTouch. If you want to have onClick sort of functionality, let go the original onClick and try to handle that in onTouch. You can take help of the GestureDetector.SimpleOnGestureListener in Xamarin. For an example to override the double tap you can do it like this
class MyDoubleTapListener : GestureDetector.SimpleOnGestureListener
{
public override bool OnDoubleTap(MotionEvent e)
{
//Your code here
return false;
}
}
and then in your activity
public class Test : Activity, View.IOnTouchListener
{
private GestureDetector _gestureDetector = null;
protected override void OnCreate (Bundle bundle)
{
_gestureDetector = new GestureDetector(new MyDoubleTapListener (this));
_editText.SetOnTouchListener(this);
}
public bool OnTouch(View v, MotionEvent e)
{
return _gestureDetector.OnTouchEvent(e);
}
}
GestureDetector also provides you other methods that you can overide to suit your need. Follow this, https://developer.xamarin.com/api/type/Android.Views.GestureDetector/
I couldn't get exactly what your are trying to do inside onTouch interface. Anyway there's some points you must know:
1) When you handle a touch event, onTouch method returns a boolean that indicates if event was consumed (true) or not (false). If you consume touch event related to click, click listener won't be triggered. So, make sure you are only consuming what is desired.
2) When you set listeners as onClick or onTouch to some view, it becomes clickable and touchable, respectively, if it wasn't. If you are setting this attributes to false in some part of your code make sure it's enabled again when you want to handle such events.
i'm trying to drag and drop a button the problem is that when i use getX() at motion event it works but the button starts to tremble . When i call the method getRawX() it does not tremble but it jumps at least 80px right before i start the drag and drop .
how can i managed that , i'll post my code here:
public class MyButton extends Button {
private final static int START_DRAGGING = 0;
private final static int STOP_DRAGGING = 1;
private int status;
private LinearLayout parentLayout;
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
// Log.i("teste", "Button width: " + btWidth + ", Height : "+ btHeight);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
status = START_DRAGGING;
Log.i("teste", "Coordenada on ACTION_DOWN: " + (int) event.getRawX());
break;
case MotionEvent.ACTION_UP:
status = STOP_DRAGGING;
Log.i("teste", "Coordenada on ACTION_UP: " + (int) event.getRawX());
break;
case MotionEvent.ACTION_MOVE:
if(status == START_DRAGGING){
parentLayout.setPadding((int)event.getRawX(), 0,0,0);
parentLayout.invalidate();
Log.i("teste", "Coordenada on ACTION_MOVE: " + (int) event.getRawX());
}
break;
}
return true;
}
}
event.getX() returns touch coordinates relative to your view (the button), event.getRawX() returns touch coordinates relative to the display, so I would think the first way is the correct one, if you set the padding of the button, instead of the layout. But you'll still have the "jump" problem because you're supposed to touch the button, not its edge, and the first move will put the edge under your finger.
I would try using a GestureDetector, its OnGestureListener has an onScroll() method that gives you the scrolling distance (it does the job of remembering last position and giving a relative motion), so that you can add that value to the padding, that is, you drag 10px => you add 10px of padding.
code example:
private GestureDetector gd =
new GestureDetector(getContext(), new SimpleOnGestureListener() {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
setPadding((int)(getPaddingLeft()+distanceX),0,0,0);
}
#Override
public boolean onDown(MotionEvent e) {
return true; // else no event will be handled
}
I would also add some checks to prevent negative or excessive paddings.
I have been working on touching and moving objects too. I started out making something move in a similar way: changing the top and left padding. Pretty quickly, as I added more moveable objects to the screen, things got a bit confusing. I would touch one spot on the screen and something else would start moving. What was going on was there were multiple overlapping views on the screen. Only the top view would receive the touch events.
I found a post that suggested taking a look at the Android Launcher code and seeing how they did drag and drop. I think their approach is really good. You do have to add a ViewGroup to hold your moveable objects, but that works out. The bounds of views match what you see on the screen and you end up with no surprises. Events go to the view you expect.
If it turns out you are going to have more than one moveable object, you might want to take a look at my blog post: "Moving Views In Android". More explanations about the Android Launcher and source code are there.