I have ViewPager with three fragments and one of them is FrameLayout with ScrollView inside:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/table"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</ScrollView>
<Button
android:id="#+id/buttonRemove"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/bg_button_red"
android:layout_gravity="bottom"
android:layout_margin="4dp"
android:text="#string/button_remove_last_round"
android:textColor="#ffffff"
android:textSize="24sp"/>
</FrameLayout>
and if I swipe over button, it works. But if I swipe over the ScrollView, it doesn't work, only scroll up/down works
EDIT: I tried to override OnTouchEvent of Scrollview:
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
touchX = ev.getX();
touchY = ev.getY();
return super.onTouchEvent(ev);//if I change it to 'break', then only swipe works, but no scroll
case MotionEvent.ACTION_MOVE:
if(Math.abs(touchX-ev.getX())<40){
return super.onTouchEvent(ev);//Scroll works perfectly
}else{
return false;//Scroll disabled, but swipe still not working
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchX=0;
touchY=0;
break;
}
return false;
}
Now I can disable scroll, but swipe still not working, if the difference between X points more than 40, I pass the event to the viewpager, but the viewpager's onTouchListener doesn't get this event;
Ok, I found the solution with help of #yedidyak. I wrote my custom ScrollView:
public class CustomScrollView extends ScrollView {
float touchX = 0;
float touchY = 0;
ViewPager parentPager;
public void setParentPager(ViewPager parentPager) {
this.parentPager = parentPager;
}
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomScrollView(Context context) {
super(context);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()){
case MotionEvent.ACTION_DOWN:
touchX = ev.getX();
touchY = ev.getY();
return super.onTouchEvent(ev);
case MotionEvent.ACTION_MOVE:
if(Math.abs(touchX-ev.getX())<40){
return super.onTouchEvent(ev);
}else{
if (parentPager==null) {
return false;
} else {
return parentPager.onTouchEvent(ev);
}
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchX=0;
touchY=0;
break;
}
return super.onTouchEvent(ev);
}
}
then in the fragment I put the viewpager to this view and it works perfectly
The problem is that it isn't clear what scroll you want to happen, that of the ViewPager or that of the scrollview. If you really need a scroll-inside-a-scroll, then you need to override the OnTouchListener of the inner scrollview and add code that decides when to catch and use the touch, and when to pass it back to the parent views.
In this case, you can do something that tests if the swipe is up/down, and then keep the touch, otherwise if the swipe is sideways then passes it back.
For Future Readers living in +2019
use ViewPager2 to avoid this problem.
you can find good example of ViewPager2 implementation at this topic.
Related
I have added drag functinality to my custom editext by overriding the touchevent()
Now the problem is after the edittext is dragged and dropped in a particular position and i want to input text into the edittext by clicking on it, it still getting dragged maybe because the touch event has been overriden and keyboard does not appear to input text
The workaround maybe triggering the dragfunctionality on long press but now the default longpress functionality of the edittext may change
I dont want this to happen
What to do.
mainactivity.java
public class MainActivity extends Activity
{
RelativeLayout dropLayout;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
dropLayout = (RelativeLayout) findViewById(R.id.ondraglayout);
dropLayout.setOnDragListener(new MyDragListener());
EditText et = (EditText) findViewById(R.id.mainEditText1);
}
}
my customedittext.java
public class CustomEdittext extends EditText
{
public CustomEdittext(Context context){
super(context);
}
public CustomEdittext(Context context, AttributeSet attr){
super(context, attr);
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
switch(event.getAction()){
case MotionEvent.ACTION_DOWN :
break;
case MotionEvent.ACTION_MOVE :
ClipData dragdata = ClipData.newPlainText("","");
View.DragShadowBuilder shdwbldr = new View.DragShadowBuilder(this);
this.startDrag(dragdata, shdwbldr, this, 0);
this.setVisibility(View.INVISIBLE);
break;
}
return true;
}
}
mydraglistener.java
public class MyDragListener implements OnDragListener
{
private RelativeLayout.LayoutParams params;
#Override
public boolean onDrag(View v, DragEvent event)
{
View view = (View) event.getLocalState();
switch(event.getAction())
{
case DragEvent.ACTION_DRAG_STARTED:
params = (RelativeLayout.LayoutParams) view.getLayoutParams();
break;
case DragEvent.ACTION_DRAG_ENTERED:
int x = (int) event.getX();
int y = (int) event.getY();
break;
case DragEvent.ACTION_DRAG_EXITED :
break;
case DragEvent.ACTION_DRAG_LOCATION :
x= (int) event.getX();
y = (int) event.getY();
break;
case DragEvent.ACTION_DRAG_ENDED :
break;
case DragEvent.ACTION_DROP:
x = (int) event.getX();
y = (int) event.getY();
params.leftMargin = x;
params.topMargin = y;
view.setLayoutParams(params);
view.setVisibility(View.VISIBLE);
break;
default: break;
}
return true;
}
}
my main.xml
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="#CDC2C0"
android:id="#+id/ondraglayout">
<com.mycompany.myapp.CustomEdittext
android:layout_height="wrap_content"
android:ems="10"
android:layout_width="wrap_content"
android:id="#+id/mainEditText1"/>
Your whole code not making sense at all: First let discuss what happening inside your onTouchEvent:
#Override
public boolean onTouchEvent(MotionEvent event)
{
switch(event.getAction()){
case MotionEvent.ACTION_DOWN :
break;
case MotionEvent.ACTION_MOVE :
ClipData dragdata = ClipData.newPlainText("","");
View.DragShadowBuilder shdwbldr = new View.DragShadowBuilder(this);
this.startDrag(dragdata, shdwbldr, this, 0);
this.setVisibility(View.INVISIBLE);
break;
}
return true;
When you put your finger in the screen, the system first trigger MotionEvent.ACTION_DOWN, and afterwards, she will deliver events of MotionEvent.ACTION_MOVE as long as nothing else in the system will consume this events. The meaning of this is your call for startDrag() will call on each movement of the finger, not really make sense right? So first I suggest you to move the code from MotionEvent.ACTION_MOVE into MotionEvent.ACTION_DOWN.
Now to the more important part. When you assign a DragListener to View, the meaning is that this View will receive all the DragEvent that the system will deliver as long as the Listener returns true for the DragEvent.ACTION_DRAG_STARTED.
Now, you assigned your DragListener, to the RelativeLayout. So lets look into your code:
case DragEvent.ACTION_DROP:
x = (int) event.getX();
y = (int) event.getY();
params.leftMargin = x;
params.topMargin = y;
view.setLayoutParams(params);
view.setVisibility(View.VISIBLE);
break;
You set new position for the LayoutParams, but who is the View that will receive this params? The RelativeLayout, not your EditText. Actually your EditText is now INVISBLE as you set it visibilty to INVISIBLE inside your onTouchEvent and never changed it back. Your code "view.setVisibility(View.VISIBLE);" inside the ACTION_DROP is not referring to the EditText but to the RelativeLayout. Unless there is other code you not shown here, this is the case.
Anyway, in your case I would recommend you to move your startDrag() call to a LongCLickListener. I have no clue which functionality of the EditText you think that may change ,as, at least as far as I know, there is no functionality for LongClick in EditText. If you want to avoid it you can also add a flag to your code and turn it on after the ACTION_DROP, and then make the code inside the ACTION_DOWN to run just if this flag is set to false.
I have a single vertical nestedscrollview that contains a bunch of recyclerview with a horizontal layoutmanager setup. The idea is pretty similar to how the new google play store looks. I'm able to make it functional but it isn't smooth at all. Here are the problems:
1) The horizontal recyclerview item fails to intercept the touch event most of the times even though i tap right on it. The scroll view seems to take precedence for most of the motions. It's hard for me to get a hook onto the horizontal motion. This UX is frustrating as I need to try a few times before it works. If you check the play store, it is able to intercept the touch event really well and it just works well. I noticed in the play store the way they set it up is many horizontal recyclerviews inside one vertical recyclerview. No scrollview.
2) The height of the horizontal recyclerviews have to be manually set and there is no easy way to calculate the height of the children elements.
Here is the layout I'm using:
<android.support.v4.widget.NestedScrollView
android:id="#+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:background="#color/dark_bgd"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="#+id/main_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="gone"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/starring_list"
android:paddingLeft="#dimen/spacing_major"
android:paddingRight="#dimen/spacing_major"
android:layout_width="match_parent"
android:layout_height="180dp" />
This UI pattern is very basic and most likely used in many different apps. I've read many SO's where ppl say it's a bad idea to put a list within a list, but it is a very common and modern UI pattern used all over the place.Think of netflix like interface with a series of horizontal scroll lists inside a vertical list. Isn't there a smooth way to accomplish this?
Example image from the store:
So the smooth scrolling issue is fixed now. It was caused by a bug in the NestedScrollView in the Design Support Library (currently 23.1.1).
You can read about the issue and the simple fix here:
https://code.google.com/p/android/issues/detail?id=194398
In short, after you performed a fling, the nestedscrollview didn't register a complete on the scroller component and so it needed an additional 'ACTION_DOWN' event to release the parent nestedscrollview from intercepting(eating up) the subsequent events. So what happened was if you tried scrolling your child list(or viewpager), after a fling, the first touch releases the parent NSV bind and the subsequent touches would work. That was making the UX really bad.
Essentially need to add this line on the ACTION_DOWN event of the NSV:
computeScroll();
Here is what I'm using:
public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;
public MyNestedScrollView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
ViewConfiguration config = ViewConfiguration.get(context);
slop = config.getScaledEdgeSlop();
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private float xDistance, yDistance, lastX, lastY;
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
lastX = ev.getX();
lastY = ev.getY();
// This is very important line that fixes
computeScroll();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - lastX);
yDistance += Math.abs(curY - lastY);
lastX = curX;
lastY = curY;
if (xDistance > yDistance) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}
Use this class in place of your nestedscrollview in the xml file, and the child lists should intercept and handle the touch events properly.
Phew, there are actually quite a few bugs like these that makes me want to ditch the design support library altogether and revisit it when its more mature.
Since falc0nit3 solution doesn't work anymore (currently the project using 28.0.0 version of support library), i have found an another one.
The background reason of the issue is still the same, scrollable view eats on down event by returning true on the second tap, where it shouldn't, because naturally second tap on the fling view stops scrolling and may be used with next move event to start opposite scroll
The issue is reproduced as with NestedScrollView as with RecyclerView.
My solution is to stop scrolling manually before native view will be able to intercept it in onInterceptTouchEvent. In this case it won't eat the ACTION_DOWN event, because it have been stopped already.
So, for NestedScrollView:
class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
NestedScrollView(context, attrs) {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev)
}
return super.onInterceptTouchEvent(ev)
}
}
For RecyclerView:
class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
RecyclerView(context, attrs) {
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
this.stopScroll()
}
return super.onInterceptTouchEvent(e)
}
}
Despite solution for RecyclerView looks easy to read, for NestedScrollView it's a bit complicated.
Unfortunately, there is no clear way to stop scrolling manually in widget, which the only responsibility is to manage scroll (omg). I'm interesting in abortAnimatedScroll() method, but it is private. It is possible to use reflection to get around it, but for me better is to call method, which calls abortAnimatedScroll() itself.
Look at onTouchEvent handling of ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
Log.i(TAG, "abort animated scroll");
abortAnimatedScroll();
}
Basically stopping fling is managed in this method, but a bit later, than we have to call it to fix the bug
Unfortunately due to this we can't just create OnTouchListener and set it outside, so only inheritance fits the requirements
I've succeded in doing horizontal scrolling in a vertically scrolling parent with a ViewPager :
<android.support.v4.widget.NestedScrollView
...
<android.support.v4.view.ViewPager
android:id="#+id/pager_known_for"
android:layout_width="match_parent"
android:layout_height="350dp"
android:minHeight="350dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:clipToPadding="false"/>
public class UniversityKnownForPagerAdapter extends PagerAdapter {
public UniversityKnownForPagerAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);
...
container.addView(rootView);
return rootView;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
#Override
public int getCount() {
return 4;
}
#Override
public boolean isViewFromObject(View view, Object object) {
return (view == object);
}
Only issue : you must provide a fixed height to the view pager
I have a layout with 2 LinearLayout. The first one is used as a container to contain a graph, and the second one contains few buttons.
When the app starts, at first instance the LinearLayout1 which contains the graph is hidden View.GONE.
Then when I touch a button from LinearLayout2, this layout goes back to its original place using a translate animation.
Finally, I should have the capability to hide again the LinearLayout1. I would like to achieve this by draggin up the LinearLayout2, so when the user has moved a bit upwards the LinearLayout2, the LinearLayouy1 would become again hidden by View.GONE.
This final part is the one with I need some help. I have tried something using an OnTochListener() but I haven't worked too much with this and i'm not sure about how to do it. This is code snnipet where I do this:
/*Layout views*/
private View graphContainer; //This is the LinearLayout1
private View valuesContainer; //This is the LinearLayout2
private float oldY;
private float newY;
...
valuesContainer.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float y = event.getY();
oldY = y;
break;
case MotionEvent.ACTION_MOVE:
float y2 = event.getRawY();
newY = y2;
if (oldY < newY) {
graphContainer.setVisibility(View.GONE);
}
break;
}
return true;
}
});
Depending on where I touch to do the movement I get to set the visibility to GONE, but the movement is not as I would like, I don't get to move the LinearLayout2.
What you did above is hiding layout2 when the user moves it's finger up.
You say that "you don't get to move the LinearLayout2" -> in order to move a view you need to update it's LayoutParams. You can see an example to this here : How to move a view in Android?
This way you can "push" layout2 up and at some point hide layout1 (using animation, or pushing layout1 as well). Hope this helps.
Edit (a requested code sample) :
BTW - animations transitions is the better way to go when view's params needs changing. It's not the answer to your question since you want a real "drag" feel (when going up).
Also - android L has some beautiful animations (I haven't used yet) that we should keep an eye on.
So... using a layout as follows :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:paddingBottom="#dimen/activity_vertical_margin"
tools:context=".MyActivity">
<LinearLayout
android:id="#+id/first_layout"
android:background="#android:color/darker_gray"
android:layout_width="match_parent"
android:layout_height="100dp" >
</LinearLayout>
<LinearLayout
android:id="#+id/second_layout"
android:background="#android:color/holo_green_dark"
android:layout_width="match_parent"
android:layout_height="100dp" >
</LinearLayout>
</LinearLayout>
and a corresponding activity as follows :
public class MyActivity extends Activity {
int yDown = 0;
int initialTopMargin = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
LinearLayout layout2 = (LinearLayout)findViewById(R.id.second_layout);
layout2.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent event)
{
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
switch (event.getAction())
{
case MotionEvent.ACTION_MOVE:
params.topMargin = initialTopMargin - (yDown - (int)event.getRawY());
view.setLayoutParams(params);
break;
case MotionEvent.ACTION_DOWN:
yDown = (int)event.getRawY();
initialTopMargin = params.topMargin;
break;
}
return true;
}
});
}
}
how about using onDragListener ?
valuesContainer.setOnDragListener(new OnDragListener() {
#Override
public boolean onDrag(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float y = event.getY();
oldY = y;
break;
case MotionEvent.ACTION_MOVE:
float y2 = event.getRawY();
newY = y2;
if (oldY < newY) {
graphContainer.setVisibility(View.GONE);
}
break;
}
return true;
}
});
I have got viewpager.
At the my viewpagers first item i used lock pattern view (from google code) my problem is when i try to use lockview my view pager start to swipe how can i fix my problem.
I tried lots of thing but I cant do it I hope anyone help me
<android.support.v4.view.ViewPager
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="#+id/viewPager" />
Add this to your view:
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
pager.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
pager.requestDisallowInterceptTouchEvent(false);
break;
}
}
requestDisallowInterceptTouchEvent:
Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
I have a ScrollView on top of another view(with Buttons). The ScrollView is taking the whole screen and is obscuring the view that is beneath it.
At some point in my app I need the ScrollView to be disabled (but still visible) and transfer all the touch events to the Buttons that are beneath the ScrollView. How can I do that? Some views like Buttons are automatically doing that when disabled but a ScrollView is not doing that.
Try to implement your own ScrollView which has a flag to indicate the status(disabled/enabled) and also overrides the onTouchEvent and dispatchTouchEvent to let the touch events get pass the ScrollView. Here is an example:
public class DisabledScrollView extends ScrollView {
private boolean mIsDisable = false;
// if status is true, disable the ScrollView
public void setDisableStatus(boolean status) {
mIsDisable = status;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
// no more tocuh events for this ScrollView
if (mIsDisable) {
return false;
}
return super.onTouchEvent(ev);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
// although the ScrollView doesn't get touch events , its children will get them so intercept them.
if (mIsDisable) {
return false;
}
return super.dispatchTouchEvent(ev);
}
}
Then all you have to do is change the value of that flag. See if it works.
In my case, I just needed to handle the touch event in View A, which was overlaping View B and then send the event to View B. Both views were child of the same RelativeLayout, but there was no parent-child relation between views A and B. This worked for me:
viewA.setOnTouchListener( new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
// do my stuff here
viewB.dispatchTouchEvent( event );
}
}
In this case I have a recyclerview under a scrollview. The top of scrollview is in vertical scroll, and the recyclerview is in horizontal scroll. The scrollview have top padding, making the recyclerview is visible through the transparency in the scrollview padding. I have to make it this way because when the scrollview is scrolled the recyclerview will scroll vertically to like parallax effect (this effect is in another code). This code below is working for my case, might help
scrollView.setOnTouchListener(new View.OnTouchListener() {
float mDownX,mDownY;
boolean mIsSwiping,isDown;
#Override
public boolean onTouch(View v, MotionEvent event) {
if(mIsSwiping){
recyclerView.dispatchTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mIsSwiping = false;
isDown = true;
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if(isDown){
float deltaX = Math.abs(event.getX() - mDownX);
float deltaY = Math.abs(event.getY() - mDownY);
mDownX = event.getX();
mDownY = event.getY();
if(deltaX!=deltaY){
isDown = false;
if(deltaX>deltaY){
mIsSwiping = true;
}
}
}
}
return mIsSwiping;
}
});
This is the layout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/gray0"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingBottom="70dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="320dp"
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
<ScrollView
android:id="#+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="301.75dp"
android:paddingBottom="23.5dp"
android:clipChildren="false"
android:clipToPadding="false">
.
.
.