ViewPager dans un ScrollView ne fait pas défiler correclty

J’ai une «page» qui contient un certain nombre de composants et dont le contenu est plus long que la hauteur de l’appareil. Bien, il suffit de mettre toute la mise en page (la page entière) dans un ScrollView , pas de problème.

Un des composants est un ViewPager . Cela se traduit correctement, mais la réponse à un glissement / une lecture ne fonctionne pas correctement, il est nerveux et ne fonctionne pas toujours. Il semble être “confus” avec ScrollView , donc ne fonctionne que 100% lorsque vous jetez une ligne horizontale exacte.

Si je supprime le ScrollView , le ViewPager répond parfaitement.

J’ai fait une recherche autour de moi et je n’ai pas trouvé cela comme un défaut connu. Quelqu’un d’autre a-t-il vécu cela?

  • Version de la plate-forme: 1.6
  • Bibliothèque de compatibilité v4.
  • Appareil: HTC Incredible S

Vous trouverez ci-dessous un exemple de code à tester, commentez ScrollView pour le voir fonctionner correctement.

Activité:

 package com.ss.activities; import com.ss.R; import android.app.Activity; import android.content.Context; import android.graphics.Color; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.view.View; import android.widget.TextView; public class PagerInsideScollViewActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ViewPager vp = (ViewPager) findViewById(R.id.viewpager); vp.setAdapter(new MyPagerAdapter(this)); } } class MyPagerAdapter extends PagerAdapter { private Context ctx; public MyPagerAdapter(Context context) { ctx = context; } @Override public int getCount() { return 2; } @Override public Object instantiateItem(View collection, int position) { TextView tv = new TextView(ctx); tv.setTextSize(50); tv.setTextColor(Color.WHITE); tv.setText("SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, " + "SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, " + "SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, SMILE DUDE, " + "SMILE DUDE, SMILE DUDE, SMILE DUDE"); ((ViewPager) collection).addView(tv); return tv; } @Override public void destroyItem(View collection, int position, Object view) { ((ViewPager) collection).removeView((View) view); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Parcelable saveState() { return null; } @Override public void restoreState(Parcelable arg0, ClassLoader arg1) { } @Override public void startUpdate(View arg0) { } @Override public void finishUpdate(View arg0) { } } 

Disposition:

       

J’ai eu le même problème. Ma solution consistait à appeler requestDisallowInterceptTouchEvent lorsque le défilement de ViewPager commençait.

Voici mon code:

 pager.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { v.getParent().requestDisallowInterceptTouchEvent(true); return false; } }); pager.setOnPageChangeListener(new SimpleOnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { pager.getParent().requestDisallowInterceptTouchEvent(true); } }); 

Une lecture plus approfondie a révélé qu’il existe des problèmes avec les composants de défilement à l’intérieur des composants de défilement.

Une solution consiste à “désactiver” le défilement vertical de ScrollView sur la zone du composant déroulant contenu, dans mon cas, un ViewPager.

Le code de cette solution est détaillé ici (et c’est tout simplement génial!)

Voici une solution:

  mPager.setOnTouchListener(new View.OnTouchListener() { int dragthreshold = 30; int downX; int downY; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = (int) event.getRawX(); downY = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int distanceX = Math.abs((int) event.getRawX() - downX); int distanceY = Math.abs((int) event.getRawY() - downY); if (distanceY > distanceX && distanceY > dragthreshold) { mPager.getParent().requestDisallowInterceptTouchEvent(false); mScrollView.getParent().requestDisallowInterceptTouchEvent(true); } else if (distanceX > distanceY && distanceX > dragthreshold) { mPager.getParent().requestDisallowInterceptTouchEvent(true); mScrollView.getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: mScrollView.getParent().requestDisallowInterceptTouchEvent(false); mPager.getParent().requestDisallowInterceptTouchEvent(false); break; } return false; } }); 

Définissez essentiellement les valeurs X, Y lorsque vous appuyez et calculez la distance tout en faisant glisser pour déterminer la direction à suivre. Jouez avec le seuil de traînée pour optimiser votre cas.

Avec un ViewPager, vous pouvez capturer des événements de changement de page et empêcher ScrollView d’intercepter l’événement tactile à l’origine du changement de page.

C’est très simple, en utilisant ViewGroup.requestDisallowInterceptTouchEvent(boolean) . Il permet également à l’utilisateur de faire glisser le ScrollView même s’il commence à faire glisser le ViewPager, mais le glissement horizontal sur le pager fonctionnera toujours sans que ScrollView interfère.

  @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Add android:id for your ScrollView in your layout final ScrollView sv = (ScrollView) findViewById(R.id.scrollview); final ViewPager vp = (ViewPager) findViewById(R.id.viewpager); vp.setAdapter(new MyPagerAdapter(this)); // Use a page-change listener to respond to begin-drag events: vp.setOnPageChangeListener(new OnPageChangeListener() { @Override public void onPageSelected(int newState) { if (newState == ViewPager.SCROLL_STATE_DRAGGING) { // Prevent the ScrollView from intercepting this event now that the page is changing. // When this drag ends, the ScrollView will start accepting touch events again. sv.requestDisallowInterceptTouchEvent(true); } } @Override public void onPageScrolled(int arg0, float arg1, int arg2) { } @Override public void onPageScrollStateChanged(int arg0) { } }); } 

Cela fonctionne pour moi avec la bibliothèque Android Support v4 sur Android 2.3.4 et 4.2.1.

J’ai adapté la solution de @Michael Herbig. L’avantage est que cela fonctionne sur n’importe quelle vue qui permet de setOnTouchListener , pas seulement un ViewPager (par exemple ViewPagerIndicator) et c’est une classe autonome.

Exemple d’utilisation:

 // runStatsPager is a android.support.v4.view.ViewPager; runStatsPager.setOnTouchListener(new ViewInScrollViewTouchHelper(runStatsPager)); // runStatsPagerIndicator is a com.viewpagerindicator.TitlePageIndicator runStatsPagerIndicator.setOnTouchListener(new ViewInScrollViewTouchHelper(runStatsPagerIndicator)); 

Et la classe:

 class ViewInScrollViewTouchHelper implements View.OnTouchListener { private final ScrollView scrollView; private final View viewInScrollView; int dragthreshold = 30; int downX; int downY; public ViewInScrollViewTouchHelper(View viewInScrollView) { if (viewInScrollView == null) { throw new IllegalArgumentException("viewInScrollView cannot be null."); } ViewParent parent = viewInScrollView.getParent(); ScrollView scrollView = null; do { if (parent instanceof ScrollView) { scrollView = (ScrollView) parent; break; } } while(parent != null && (parent = parent.getParent()) != null); if (scrollView == null) { throw new IllegalArgumentException("View does not have a ScrollView in its parent hierarchy."); } this.scrollView = scrollView; this.viewInScrollView = viewInScrollView; } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = (int) event.getRawX(); downY = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int distanceX = Math.abs((int) event.getRawX() - downX); int distanceY = Math.abs((int) event.getRawY() - downY); if (distanceY > distanceX && distanceY > dragthreshold) { viewInScrollView.getParent().requestDisallowInterceptTouchEvent(false); scrollView.getParent().requestDisallowInterceptTouchEvent(true); } else if (distanceX > distanceY && distanceX > dragthreshold) { viewInScrollView.getParent().requestDisallowInterceptTouchEvent(true); scrollView.getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: scrollView.getParent().requestDisallowInterceptTouchEvent(false); viewInScrollView.getParent().requestDisallowInterceptTouchEvent(false); break; } return false; } } 
 public class WrapContentHeightViewPager extends ViewPager { public WrapContentHeightViewPager(Context context) { super(context); } public WrapContentHeightViewPager(Context context, AtsortingbuteSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int height = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.measure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); int h = child.getMeasuredHeight(); if (h > height) height = h; } heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } 

}

@Override public boolean onTouch (événement View v, MotionEvent) {

  int dragthreshold = 30; int downX = 0; int downY = 0; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = (int) event.getRawX(); downY = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int distanceX = Math.abs((int) event.getRawX() - downX); int distanceY = Math.abs((int) event.getRawY() - downY); if (distanceY > distanceX && distanceY > dragthreshold) { mViewPager.getParent().requestDisallowInterceptTouchEvent(false); mScrollView.getParent().requestDisallowInterceptTouchEvent(true); } else if (distanceX > distanceY && distanceX > dragthreshold) { mViewPager.getParent().requestDisallowInterceptTouchEvent(true); mScrollView.getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: mScrollView.getParent().requestDisallowInterceptTouchEvent(false); mViewPager.getParent().requestDisallowInterceptTouchEvent(false); break; } return false; } 

Utilisez ci-dessus deux et cela fonctionnera très bien. J’ai aussi utilisé dans mon code.

Donc, avec cette approche, je laisse défiler le ViewPager dans la direction X sans avoir à ScrollView soucier du fait que ScrollView (parent) vole l’événement et annule le parchemin actuel. Plus important encore, cela laisse également la zone où se trouve le ViewPager défilant dans la direction Y

 public class CustomViewPager extends ViewPager { private GestureDetector mGestureDetector; View.OnTouchListener mGestureListener; public CustomViewPager(Context context, AtsortingbuteSet attrs) { super(context, attrs); mGestureDetector = new GestureDetector(context, new Detector()); } @Override public boolean onTouchEvent(MotionEvent motionEvent) { mGestureDetector.onTouchEvent(motionEvent); return super.onTouchEvent(motionEvent); } class Detector extends SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { requestDisallowInterceptTouchEvent(true); return false; } } } 

Aucune des solutions proposées ici n’a bien fonctionné pour moi, car c’est soit la modification de la logique tactile de ScrollView soit la logique de ScrollView . Je devais mettre en œuvre les deux, maintenant ça marche comme un charme.

 public class TouchGreedyViewPager extends ViewPager { private float xDistance, yDistance, lastX, lastY; public TouchGreedyViewPager(Context context, AtsortingbuteSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; lastX = ev.getX(); lastY = ev.getY(); 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) / 3; // favor X events lastX = curX; lastY = curY; if (xDistance > yDistance) { return true; } } return super.onInterceptTouchEvent(ev); } } public class TouchHumbleScrollView extends ScrollView { private float xDistance, yDistance, lastX, lastY; public TouchHumbleScrollView(Context context, AtsortingbuteSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; lastX = ev.getX(); lastY = ev.getY(); 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) / 3; // favor X events lastX = curX; lastY = curY; if (xDistance > yDistance) { return false; } } return super.onInterceptTouchEvent(ev); } } 

Vous pouvez remplacer la méthode instantiateItem dans votre classe PagerAdapter en ajoutant un scrollView à chaque page. Voici un code:

 @Override public Object instantiateItem(View collection, int position) { ScrollView sc = new ScrollView(cxt); sc.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); sc.setFillViewport(true); TextView tv = new TextView(cxt); tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); tv.setText(pages[position]); tv.setPadding(5,5,5,5); sc.addView(tv); ((ViewPager) collection).addView(sc); return sc; }