it-swarm.com.ru

Анимация прокрутки элемента ListView (в стиле "UIKit Dynamics")

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

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

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


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

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

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

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

62
b_yng

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

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

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

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

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

16
Sagar Shah

Я потратил всего несколько минут, чтобы изучить это, и похоже, что это можно сделать довольно легко с 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 (float scale)) и цепочку convertView.animate () (например, .scaleX (float)).

enter image description here

16
Justin Pollard

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

1
SDJMcHattie

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

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