oreo - android pie
smoothScrollToPositionFromTop() no siempre funciona como debería (3)
Durante un tiempo he estado intentando que funcione SmileScrollToPositionFromTop (), pero no siempre se desplaza a la posición correcta.
Tengo un ListView (con 10 elementos) en un diseño con 10 botones en el lateral, por lo que puedo desplazarme a todos los elementos de la lista. Por lo general, cuando me desplazo una posición hacia adelante o hacia adelante, funciona bien, pero a menudo cuando trato de desplazarme más de 3 posiciones hacia atrás o hacia adelante, el ListView no termina exactamente en la posición seleccionada. Cuando falla, por lo general termina con 0,5 a 1,5 elementos desactivados y no es realmente predecible cuando falla el desplazamiento.
También he comprobado que smoothScrollToPosition después de NotifyDataSetChanged no funciona en Android , pero esta solución no me funciona y no cambio ningún dato.
Realmente me gustaría desplazarme automáticamente a los artículos seleccionados, pero no puedo entender cómo. ¿Alguien ha tenido este problema antes y sabe cómo solucionarlo?
Aquí hay una implementación de la solución.
void smoothScrollToPositionFromTopWithBugWorkAround(final AbsListView listView,
final int position,
final int offset,
final int duration){
//the bug workaround involves listening to when it has finished scrolling, and then
//firing a new scroll to the same position.
//the bug is the case that sometimes smooth Scroll To Position sort of misses its intended position.
//more info here : https://code.google.com/p/android/issues/detail?id=36062
listView.smoothScrollToPositionFromTop(position, offset, duration);
listView.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState==OnScrollListener.SCROLL_STATE_IDLE){
listView.setOnScrollListener(null);
listView.smoothScrollToPositionFromTop(position, offset, duration);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
}
Como se menciona en la página de Google issuetracker en el cuarto piso : https://issuetracker.google.com/issues/36952786
La solución dada anteriormente, "Una solución alternativa por ahora es escuchar SCROLL_STATE_IDLE al iniciar el desplazamiento, y smoothScrollToPositionFromTop nuevamente a la misma posición". no siempre funcionará tampoco.
En realidad, la llamada a onScrollStateChanged con SCROLL_STATE_IDLE no significa necesariamente que el desplazamiento haya finalizado. Como resultado, aún no se puede garantizar que la vista de lista se desplace a una posición correcta cada vez, especialmente cuando las vistas de elementos de lista no están todas en la misma altura.
Después de investigar, encontré otro enfoque que funciona de manera perfectamente correcta y razonable. Como se sabe, Listview proporciona un método scrollListBy (int y), que nos permite desplazar la vista de lista hacia arriba con y píxeles instantáneamente. Luego, con la ayuda de un temporizador, podemos desplazarnos por la lista sin problemas y correctamente por nosotros mismos.
Lo primero que debemos hacer es calcular la altura de cada vista de elemento de lista, incluidas las vistas fuera de la pantalla. Como los datos de la lista y los tipos de vistas secundarias ya se conocen anteriormente, es posible calcular la altura de cada vista de elemento de la lista. Por lo tanto, dada la posición de destino para desplazarnos suavemente, podemos calcular su distancia de desplazamiento en la dirección y. Además, el cálculo se debe realizar después de finalizar la inicialización de ListView.
Lo segundo es combinar un temporizador y el método scrollListBy (int). En realidad, podemos usar el método sendEmptyMessageDelayed () de android.os.Handler. Así, la solución puede ser:
/**
* Created by CaiHaozhong on 2017/9/29.
*/
public class ListViewSmoothScroller {
private final static int MSG_ACTION_SCROLL = 1;
private final static int MSG_ACTION_ADJUST = 2;
private ListView mListView = null;
/* The accumulated height of each list item view */
protected int[] mItemAccumulateHeight = null;
protected int mTimeStep = 20;
protected int mHeaderViewHeight;
private int mPos;
private Method mTrackMotionScrollMethod = null;
protected int mScrollUnit = 0;
protected int mTotalMove = 0;
protected int mTargetScrollDis = 0;
private Handler mMainHandler = new Handler(Looper.getMainLooper()){
public void handleMessage(Message msg) {
int what = msg.what;
switch (what){
case MSG_ACTION_SCROLL: {
int scrollDis = mScrollUnit;
if(mTotalMove + mScrollUnit > mTargetScrollDis){
scrollDis = mTargetScrollDis - mTotalMove;
}
if(Build.VERSION.SDK_INT >= 19) {
mListView.scrollListBy(scrollDis);
}
else{
if(mTrackMotionScrollMethod != null){
try {
mTrackMotionScrollMethod.invoke(mListView, -scrollDis, -scrollDis);
}catch(Exception ex){
ex.printStackTrace();
}
}
}
mTotalMove += scrollDis;
if(mTotalMove < mTargetScrollDis){
mMainHandler.sendEmptyMessageDelayed(MSG_ACTION_SCROLL, mTimeStep);
}else {
mMainHandler.sendEmptyMessageDelayed(MSG_ACTION_ADJUST, mTimeStep);
}
break;
}
case MSG_ACTION_ADJUST: {
mListView.setSelection(mPos);
break;
}
}
}
};
public ListViewSmoothScroller(Context context, ListView listView){
mListView = listView;
mScrollUnit = Tools.dip2px(context, 60);
mPos = -1;
try {
mTrackMotionScrollMethod = AbsListView.class.getDeclaredMethod("trackMotionScroll", int.class, int.class);
}catch (NoSuchMethodException ex){
ex.printStackTrace();
mTrackMotionScrollMethod = null;
}
if(mTrackMotionScrollMethod != null){
mTrackMotionScrollMethod.setAccessible(true);
}
}
/* scroll to a target position smoothly */
public void smoothScrollToPosition(int pos){
if(mListView == null)
return;
if(mItemAccumulateHeight == null || pos >= mItemAccumulateHeight.length){
return ;
}
mPos = pos;
mTargetScrollDis = mItemAccumulateHeight[pos];
mMainHandler.sendEmptyMessage(MSG_ACTION_SCROLL);
}
/* call after initializing ListView */
public void doMeasureOnLayoutChange(){
if(mListView == null){
return;
}
int headerCount = mListView.getHeaderViewsCount();
/* if no list item */
if(mListView.getChildCount() < headerCount + 1){
return ;
}
mHeaderViewHeight = 0;
for(int i = 0; i < headerCount; i++){
mHeaderViewHeight += mListView.getChildAt(i).getHeight();
}
View firstListItemView = mListView.getChildAt(headerCount);
computeAccumulateHeight(firstListItemView);
}
/* calculate the accumulated height of each list item */
protected void computeAccumulateHeight(View firstListItemView){
int len = listdata.size();// count of list item
mItemAccumulateHeight = new int[len + 2];
mItemAccumulateHeight[0] = 0;
mItemAccumulateHeight[1] = mHeaderViewHeight;
int currentHeight = mHeaderViewHeight;
for(int i = 2; i < len + 2; i++){
currentHeight += getItemHeight(firstListItemView);
mItemAccumulateHeight[i] = currentHeight;
}
}
/* get height of a list item. You may need to pass the listdata of the list item as parameter*/
protected int getItemHeight(View firstListItemView){
// Considering the structure of listitem View and the list data in order to calculate the height.
}
}
Después de finalizar la inicialización de nuestro ListView, invocamos el método doMeasureOnLayoutChange (). Después de eso, podemos desplazar el ListView por el método smoothScrollToPosition (int pos). Podemos invocar el método doMeasureOnLayoutChange () de esta manera:
mListAdapter.notifyDataSetChanged();
mListView.post(new Runnable() {
@Override
public void run() {
mListViewSmoothScroller.doMeasureOnLayoutChange();
}
});
Finalmente, nuestro ListView se puede desplazar a una posición de destino sin problemas y, lo que es más importante, correctamente.
Este es un error conocido. Consulte https://code.google.com/p/android/issues/detail?id=36062
Sin embargo, implementé esta solución que trata con todos los casos de borde que pueden ocurrir:
Primero llame a smothScrollToPositionFromTop(position)
y luego, cuando haya finalizado el desplazamiento, llame a setSelection(position)
. La última llamada corrige el desplazamiento incompleto saltando directamente a la posición deseada. Al hacerlo, el usuario todavía tiene la impresión de que está siendo desplazado por animación a esta posición.
Implementé esta solución en dos métodos auxiliares:
smoothScrollToPositionFromTop ()
public static void smoothScrollToPositionFromTop(final AbsListView view, final int position) {
View child = getChildAtPosition(view, position);
// There''s no need to scroll if child is already at top or view is already scrolled to its end
if ((child != null) && ((child.getTop() == 0) || ((child.getTop() > 0) && !view.canScrollVertically(1)))) {
return;
}
view.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
view.setOnScrollListener(null);
// Fix for scrolling bug
new Handler().post(new Runnable() {
@Override
public void run() {
view.setSelection(position);
}
});
}
}
@Override
public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
final int totalItemCount) { }
});
// Perform scrolling to position
new Handler().post(new Runnable() {
@Override
public void run() {
view.smoothScrollToPositionFromTop(position, 0);
}
});
}
getChildAtPosition ()
public static View getChildAtPosition(final AdapterView view, final int position) {
final int index = position - view.getFirstVisiblePosition();
if ((index >= 0) && (index < view.getChildCount())) {
return view.getChildAt(index);
} else {
return null;
}
}