Элемент прокрутки элемента ListView ( "UIKit Dynamics" )

Я пытаюсь оживить элементы ListView при прокрутке. В частности, я пытаюсь подражать анимации прокрутки из приложения iMessage на iOS 7. Я нашел аналогичный пример онлайн:

moments-ios7.gif

Чтобы прояснить, я пытаюсь добиться эффекта "текучего" движения элементов, когда пользователь прокручивает, а не анимацию при добавлении нового элемента. Я попытался изменить представления в моем BaseAdapter, и я просмотрел источник AbsListView, чтобы узнать, могу ли я каким-то образом прикрепить AccelerateInterpolator где-нибудь, чтобы скорректировать координаты рисования, отправленные детям Views (если это даже как AbsListView). До сих пор я не смог добиться какого-либо прогресса.

Есть ли у кого-нибудь идеи о том, как воспроизвести это поведение?


Для записи, чтобы помочь с googling: это называется "UIKit Dynamics" на ios.

Как реплицировать сообщения, прыгающие с пузырьков в iOS 7

Он встроен в последние выпуски iOS. Однако это все еще сложно использовать. (2014) Это сообщение, на котором все копируют: широко скопированная статья. Удивительно, что UIKit Dynamics доступна только в виде коллекции "apple view", а не на "табличном представлении" яблока, поэтому все дебаты iOS должны преобразовать материал из табличного представления в "представление коллекции"

Библиотека, которую каждый использует в качестве отправной точки, BPXLFlowLayout, так как этот человек в значительной степени взломан, копируя ощущение приложения текстовых сообщений iphone. На самом деле, если вы портировали его на Android, я думаю, вы могли бы использовать параметры там, чтобы получить то же самое. FYI, я заметил в своей коллекции андроида, телефоны HTC имеют этот эффект в своем интерфейсе. Надеюсь, поможет. Скалы Android!

Ответ 1

Эта реализация работает неплохо. Существует некоторое мерцание, возможно, из-за измененных индексов, когда адаптер добавляет новые виды вверх или вниз. Это можно было бы решить, наблюдая за изменениями в дереве и смещением индексов на лету.

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;

}

}

Ответ 2

Я потратил всего несколько минут, чтобы изучить это, и похоже, что это можно сделать довольно легко с API 12 и выше (надеюсь, что я чего-то не пропустил...). Чтобы получить самый базовый эффект карты, все, что требуется, это пара строк кода в конце getView() в вашем адаптере, прежде чем вы вернете его в список. Здесь весь адаптер:

    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;
        }


    }

Обратите внимание, что я отслеживаю последнюю позицию для загрузки (mLastPosition), чтобы определить, анимировать представления снизу (если прокрутка вниз) или вниз сверху (если мы прокручиваем вверх).

Замечательно то, что вы можете сделать гораздо больше, просто изменив начальные свойства convertView (например, convertView.setScaleX(поплавковая шкала)) и цепочку convertView.animate() (например .scaleX(float)).

enter image description here

Ответ 3

Попробуйте это, поместив это в свой метод getView() Как раз перед возвратом convertView:

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

Где llParent = RootLayout, который состоит из вашего настраиваемого элемента строки.

Ответ 4

Честно говоря, это будет большая работа и довольно математически интенсивная, но я бы подумал, что вы можете сделать макеты элементов списка, заполняя верхнюю и нижнюю части, и что вы можете настроить это дополнение для каждого элемента, чтобы отдельные элементы стали более или менее разнесенных. Как вы будете отслеживать, насколько и как вы узнаете скорость, с которой прокручиваются элементы, и это будет тяжелая часть.

Ответ 5

Поскольку мы хотим, чтобы элементы появлялись каждый раз, когда они отображались в верхней или нижней части нашего списка, лучшим местом для этого является метод getView() адаптера:

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

Ответ 6

Из того, что я понимаю, то, что вы ищете, является эффектом параллакса.

Этот ответ действительно завершен, и я думаю, что это может вам помочь.

Ответ 7

Используйте эту библиотеку: http://nhaarman.github.io/ListViewAnimations

Demo

Это очень здорово. Лучше, чем iOS, по крайней мере, это с открытым исходным кодом:)