web-dev-qa-db-fra.com

Animation de défilement d'élément ListView (semblable à "UIKit Dynamics")

J'essaie d'animer les éléments ListView lorsqu'un défilement a lieu. Plus précisément, j'essaie d'émuler les animations de défilement de l'application iMessage sur iOS 7. J'ai trouvé un exemple similaire en ligne :

Pour clarifier, j'essaie d'obtenir l'effet de mouvement "fluide" sur les éléments lorsque l'utilisateur défile, pas l'animation lorsqu'un nouvel élément est ajouté. J'ai essayé de modifier les vues dans mon BaseAdapter et j'ai cherché dans la source AbsListView pour voir si je pouvais en quelque sorte attacher un AccelerateInterpolator quelque part qui ajusterait le tirage coordonnées envoyées aux vues enfants (si c'est même ainsi que AbsListView est conçu). Je n'ai pu faire aucun progrès jusqu'à présent.

Quelqu'un at-il des idées sur la façon de reproduire ce comportement?


Pour mémoire pour aider à googler: cela s'appelle "UIKit Dynamics" sur ios.

Comment répliquer des messages faisant rebondir des bulles dans iOS 7

Il est intégré aux versions récentes d'iOS. Cependant, il est encore un peu difficile à utiliser. (2014) Ceci est le post sur lequel tout le monde copie: article largement copié Étonnamment, UIKit Dynamics n'est disponible que sur la "vue de collection" d'Apple, pas sur la "vue de table" d'Apple, donc tous les debs iOS ont pour convertir des éléments de la vue table en "vue collection"

La bibliothèque que tout le monde utilise comme point de départ est BPXLFlowLayout , car cette personne a à peu près craqué en copiant la sensation de l'application de messages texte iphone. En fait, si vous le portiez vers Android je suppose que vous pourriez utiliser les paramètres là-dedans pour obtenir la même sensation. Pour info j'ai remarqué dans mon Android fone collection, les téléphones HTC ont cet effet, sur leur interface utilisateur. J'espère que cela aide. Android roches!

62
b_yng

Cette implémentation fonctionne plutôt bien. Il y a cependant un certain scintillement, probablement à cause des indices modifiés lorsque l'adaptateur ajoute de nouvelles vues en haut ou en bas .. Cela pourrait être résolu en surveillant les changements dans l'arborescence et en déplaçant les indices à la volée ..

public class ElasticListView extends GridView implements AbsListView.OnScrollListener,      View.OnTouchListener {

private static int SCROLLING_UP = 1;
private static int SCROLLING_DOWN = 2;

private int mScrollState;
private int mScrollDirection;
private int mTouchedIndex;

private View mTouchedView;

private int mScrollOffset;
private int mStartScrollOffset;

private boolean mAnimate;

private HashMap<View, ViewPropertyAnimator> animatedItems;


public ElasticListView(Context context) {
    super(context);
    init();
}

public ElasticListView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

public ElasticListView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
}

private void init() {
    mScrollState = SCROLL_STATE_IDLE;
    mScrollDirection = 0;
    mStartScrollOffset = -1;
    mTouchedIndex = Integer.MAX_VALUE;
    mAnimate = true;
    animatedItems = new HashMap<>();
    this.setOnTouchListener(this);
    this.setOnScrollListener(this);

}


@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (mScrollState != scrollState) {
        mScrollState = scrollState;
        mAnimate = true;

    }
    if (scrollState == SCROLL_STATE_IDLE) {
        mStartScrollOffset = Integer.MAX_VALUE;
        mAnimate = true;
        startAnimations();
    }

}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

    if (mScrollState == SCROLL_STATE_TOUCH_SCROLL) {

        if (mStartScrollOffset == Integer.MAX_VALUE) {
            mTouchedView = getChildAt(mTouchedIndex - getPositionForView(getChildAt(0)));
            if (mTouchedView == null) return;

            mStartScrollOffset = mTouchedView.getTop();
        } else if (mTouchedView == null) return;

        mScrollOffset = mTouchedView.getTop() - mStartScrollOffset;
        int tmpScrollDirection;
        if (mScrollOffset > 0) {

            tmpScrollDirection = SCROLLING_UP;

        } else {
            tmpScrollDirection = SCROLLING_DOWN;
        }

        if (mScrollDirection != tmpScrollDirection) {
            startAnimations();
            mScrollDirection = tmpScrollDirection;
        }


        if (Math.abs(mScrollOffset) > 200) {
            mAnimate = false;
            startAnimations();
        }
        Log.d("test", "direction:" + (mScrollDirection == SCROLLING_UP ? "up" : "down") + ", scrollOffset:" + mScrollOffset + ", toucheId:" + mTouchedIndex + ", fvisible:" + firstVisibleItem + ", " +
            "visibleItemCount:" + visibleItemCount + ", " +
            "totalCount:" + totalItemCount);
        int indexOfLastAnimatedItem = mScrollDirection == SCROLLING_DOWN ?
            getPositionForView(getChildAt(0)) + getChildCount() :
            getPositionForView(getChildAt(0));

        //check for bounds
        if (indexOfLastAnimatedItem >= getChildCount()) {
            indexOfLastAnimatedItem = getChildCount() - 1;
        } else if (indexOfLastAnimatedItem < 0) {
            indexOfLastAnimatedItem = 0;
        }

        if (mScrollDirection == SCROLLING_DOWN) {
            setAnimationForScrollingDown(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
        } else {
            setAnimationForScrollingUp(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
        }
        if (Math.abs(mScrollOffset) > 200) {
            mAnimate = false;
            startAnimations();
            mTouchedView = null;
            mScrollDirection = 0;
            mStartScrollOffset = -1;
            mTouchedIndex = Integer.MAX_VALUE;
            mAnimate = true;
        }
    }
}

private void startAnimations() {
    for (ViewPropertyAnimator animator : animatedItems.values()) {
        animator.start();
    }
    animatedItems.clear();
}

private void setAnimationForScrollingDown(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
    for (int i = indexOfTouchedChild + 1; i <= indexOflastAnimatedChild; i++) {
        View v = getChildAt(i);
        v.setTranslationY((-1f * mScrollOffset));
        if (!animatedItems.containsKey(v)) {
            animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * i));
        }

    }
}

private void setAnimationForScrollingUp(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
    for (int i = indexOfTouchedChild - 1; i > 0; i--) {
        View v = getChildAt(i);

        v.setTranslationY((-1 * mScrollOffset));
        if (!animatedItems.containsKey(v)) {
            animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * (indexOfTouchedChild - i)));
        }

    }
}


@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            Rect rect = new Rect();
            int childCount = getChildCount();
            int[] listViewCoords = new int[2];
            getLocationOnScreen(listViewCoords);
            int x = (int)event.getRawX() - listViewCoords[0];
            int y = (int)event.getRawY() - listViewCoords[1];
            View child;
            for (int i = 0; i < childCount; i++) {
                child = getChildAt(i);
                child.getHitRect(rect);
                if (rect.contains(x, y)) {
                    mTouchedIndex = getPositionForView(child); 
                    break;
                }
            }
            return false;

    }
    return false;

}

}
17
simekadam

Essayez ceci en le mettant dans votre méthode getView () Juste avant de retourner votre convertView:

Animation animationY = new TranslateAnimation(0, 0, holder.llParent.getHeight()/4, 0);
animationY.setDuration(1000);
Yourconvertview.startAnimation(animationY);  
animationY = null; 

Où llParent = RootLayout qui consiste en votre élément de ligne personnalisé.

16
Sagar Shah

J'ai pris quelques minutes pour explorer cela et il semble que cela puisse être fait assez facilement avec l'API 12 et au-dessus (j'espère que je ne manque rien ...). Pour obtenir l'effet de carte très basique, il suffit de quelques lignes de code à la fin de getView () dans votre adaptateur juste avant de le renvoyer à la liste. Voici l'adaptateur entier:

    public class MyAdapter extends ArrayAdapter<String>{

        private int mLastPosition;

        public MyAdapter(Context context, ArrayList<String> objects) {
            super(context, 0, objects);
        }

        private class ViewHolder{
            public TextView mTextView;
        }

        @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;

            if (convertView == null) {
                holder = new ViewHolder();
                convertView = LayoutInflater.from(getContext()).inflate(R.layout.grid_item, parent, false);
                holder.mTextView = (TextView) convertView.findViewById(R.id.checkbox);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            holder.mTextView.setText(getItem(position));

            // This tells the view where to start based on the direction of the scroll.
            // If the last position to be loaded is <= the current position, we want
            // the views to start below their ending point (500f further down).
            // Otherwise, we start above the ending point.
            float initialTranslation = (mLastPosition <= position ? 500f : -500f);

            convertView.setTranslationY(initialTranslation);
            convertView.animate()
                    .setInterpolator(new DecelerateInterpolator(1.0f))
                    .translationY(0f)
                    .setDuration(300l)
                    .setListener(null);

            // Keep track of the last position we loaded
            mLastPosition = position;

            return convertView;
        }


    }

Notez que je garde une trace de la dernière position à charger (mLastPosition) afin de déterminer s'il faut animer les vues de bas en haut (si vous faites défiler vers le bas) ou de bas en haut (si nous faisons défiler vers le haut).

Ce qui est merveilleux, c'est que vous pouvez faire bien plus en modifiant simplement les propriétés initiales convertView (par exemple convertView.setScaleX (échelle flottante)) et la chaîne convertView.animate () (par exemple .scaleX (flottant)).

enter image description here

16
Justin Pollard

Honnêtement, cela va être beaucoup de travail et assez mathématiquement intense, mais j'aurais pensé que vous pouviez faire en sorte que les dispositions de l'élément de liste aient un rembourrage en haut et en bas et que vous pouviez ajuster ce rembourrage pour chaque élément afin que les éléments individuels deviennent plus ou moins espacées. Comment vous suivriez par combien et comment vous connaîtriez la vitesse à laquelle les éléments défilent, eh bien ce serait la partie difficile.

1
SDJMcHattie

Puisque nous voulons que les éléments apparaissent chaque fois qu'ils apparaissent en haut ou en bas de notre liste, le meilleur endroit pour le faire est la méthode getView () de l'adaptateur:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    animatePostHc(position, v);
} else {
    animatePreHc(position, v);
}
0
gargAman