Прочитав пост на хабре о новых виджетах «RecyclerView и CardView. Новые виджеты в Android L», решил попробовать использовать. В сети много примеров, где CardView встраивается в RecyclerView. Интересовало наоборот встроить RecyclerView в CardView. Чтобы еще эта конструкция была фрагментом.

Скачал пример из статьи. Сразу возникла проблема при удалении нескольких элементов. Посмотрев код, поставил проверку:

    private void delete(Record record) {
        int position = records.indexOf(record);
        Log.i(">" , "position=" + position);
        if( position != -1 ) {
            records.remove(position);
            notifyItemRemoved(position);
        }
    }

Это было только начало… После добавления фрагмента вылезла другая проблема. CardView не может корректно «обернуть» список из RecyclerView по размеру по высоте. wrap_content не помогает. Оказалось, многие уже сталкивались и есть решения: «Nested Recycler view height doesn't wrap its content».

Сначала посмотрев A First Glance at Android’s RecyclerView думал использовать методы layoutManager.getDecoratedMeasuredHeight()… и подобные, но это не помогло. Размеры возращались 0.
Пришлось переписать onMeasure в LinearLayoutManager. Взято со stackoverflow:

MyLinearLayoutManager
    public class MyLinearLayoutManager extends LinearLayoutManager {

        public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
            super(context, orientation, reverseLayout);
        }

        public MyLinearLayoutManager(Context context) {
            super(context);
        }

        private int[] mMeasuredDimension = new int[2];
        @Override
        public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
                              int widthSpec, int heightSpec) {
            final int widthMode = View.MeasureSpec.getMode(widthSpec);
            final int heightMode = View.MeasureSpec.getMode(heightSpec);
            final int widthSize = View.MeasureSpec.getSize(widthSpec);
            final int heightSize = View.MeasureSpec.getSize(heightSpec);
            int width = 0;
            int height = 0;
            for (int i = 0; i < getItemCount(); i++) {

                if (getOrientation() == HORIZONTAL) {

                    measureScrapChild(recycler, i,
                            View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                            heightSpec,
                            mMeasuredDimension);

                    width = width + mMeasuredDimension[0];
                    if (i == 0) {
                        height = mMeasuredDimension[1];
                    }
                } else {
                    measureScrapChild(recycler, i,
                            widthSpec,
                            View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                            mMeasuredDimension);
                    height = height + mMeasuredDimension[1];
                    if (i == 0) {
                        width = mMeasuredDimension[0];
                    }
                }
            }
            switch (widthMode) {
                case View.MeasureSpec.EXACTLY:
                    width = widthSize;
                case View.MeasureSpec.AT_MOST:
                case View.MeasureSpec.UNSPECIFIED:
            }

            switch (heightMode) {
                case View.MeasureSpec.EXACTLY:
                    height = heightSize;
                case View.MeasureSpec.AT_MOST:
                case View.MeasureSpec.UNSPECIFIED:
            }

            setMeasuredDimension(width, height);
        }

        private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
                                       int heightSpec, int[] measuredDimension) {
            View view = recycler.getViewForPosition(position);
            recycler.bindViewToPosition(view, position);
            if (view != null) {
                RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
                int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
                        getPaddingLeft() + getPaddingRight(), p.width);
                int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
                        getPaddingTop() + getPaddingBottom(), p.height);
                view.measure(childWidthSpec, childHeightSpec);
                measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
                measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
                recycler.recycleView(view);
            }
        }
    }



Заработало. Удаление стало возможно только с конца. Удалении из середины или с начала списка приводило к исключению:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 3(offset:-1).state:5

Странно. Погуглив еще немного, наткнулся на аналогичные проблемы у народа IndexOutOfBoundsException Invalid item position XX(XX). Item count:XX #134. Посмотрев весь топик прочитал:

It is indeed a RecyclerView bug and is yet to be fixed.

For more information check:
code.google.com/p/android/issues/detail?id=77846
code.google.com/p/android/issues/detail?id=77232

А строчкой выше как раз было решение:

Now I am doing some dirty workaround like
if (index == 0) {notifydatasetchange();} else {notifyItemRemoved(index)}


Точнее посмотрев, как удаляются элементы, я понял, что надо заменить notifyItemRemoved(index) на notifydatasetchange(). С добавлением аналогично. Решение не оптимальное, но рабочее и в текущей реализации виджета, наверное, единственное.

Такое решение напрочь убило анимацию. Тут бы и закончить исследования…

В результате выяснилось место падения в переопределенном onMeasure()
IndexOutOfBoundsException = java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:7

Дальнейшее исследование кода RecyclerView на предмет как-то перехватить ситуацию или запросить заранее offset не увенчались успехом. Сделал жесткий хак! Не судите строго)
View view = null;
try {
      view = recycler.getViewForPosition(position);
     }catch (IndexOutOfBoundsException ex){
        Log.i(">", "IndexOutOfBoundsException = " + ex + "position : " + position);
}


Теперь анимация появилась, но после первого добавления (инициализации) список не появлялся. Хотя элементы добавлялись и все появлялось, после следующей операции. Сделал еще один хак в метод добавления элементов. Надеюсь понятно что он делает
if ( adapter.getItemCount() == 1 ) {
     adapter.notifyDataSetChanged();
}

В статье Building a RecyclerView LayoutManager – Part 1 есть решение казалось бы всей проблемы, но у меня не заработало. Может версия support library надо было обновить или SDK. Не знаю.
This is actually the only required override to get your LayoutManager to compile. The implementation is pretty straightforward, just return a new instance of the RecyclerView.LayoutParams that you want applied by default to all the child views returned from the Recycler. These parameters will be applied to each child before they return from getViewForPosition()

@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(
        RecyclerView.LayoutParams.WRAP_CONTENT,
        RecyclerView.LayoutParams.WRAP_CONTENT);
} 


В результате имеем «хаченый» подход, который стоит ли применять…

Хотя такой PopUp виджет может быть полезен для отображения сообщений программы. Вместо окон прогресса. По таймеру можно удалять верхнее сообщение через определенное время или при клике на него сразу

Получился округленный с тенью и анимацией список-фрагмент. Корректно обрабатывается поворот экрана. Легко встраивается в приложение. Единственная мелочь. После переупорядочивания стека фрагментов, когда пользователь поработал с приложением, окно не всегда появлялось. Возможно какой-то callback не в UI Thread… Решение, обращаться к фрагменту через Handler.

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
               mOverlayMessageFragment.addMessage(text);
            }
        });


Communicating with the UI Thread
Every app has its own special thread that runs UI objects such as View objects; this thread is called the UI thread. Only objects running on the UI thread have access to other objects on that thread. Because tasks that you run on a thread from a thread pool aren't running on your UI thread, they don't have access to UI objects. To move data from a background thread to the UI thread, use a Handler that's running on the UI thread.
developer.android.com/training/multiple-threads/communicate-ui.html


Communicating with Other Fragments
Often you will want one Fragment to communicate with another, for example to change the content based on a user event. All Fragment-to-Fragment communication is done through the associated Activity. Two Fragments should never communicate directly.
developer.android.com/training/basics/fragments/communicating.html


И еще одна полезная фишка для фрагмента:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // keep the fragment and all its data across screen rotation
    setRetainInstance(true);
}

Изменения кода приводятся ниже:

PopUpFragment.java
package net.appz.iconfounder.popupwidget.fragment;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.support.v7.widget.CardView;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import net.appz.iconfounder.R;
import net.appz.iconfounder.popupwidget.adapter.RecyclerViewAdapter;
import net.appz.iconfounder.popupwidget.model.Record;

import java.util.ArrayList;
import java.util.List;


public class PopUpFragment extends Fragment{

    private static final String ARG_TIMER_INTERVAL = "timer_interval";
    private OnFragmentInteractionListener mListener;
    private HandlerPopUpMessages messageHandler;

    private int TIMER_INTERVAL_DEFAULT = 2000;

    private int timer_interval;


    private RecyclerViewAdapter adapter;
    private CardView cardView;
    private RecyclerView recyclerView;
    private List<Record> records = new ArrayList<Record>();

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @param timer_interval .
     * @return A new instance of fragment PopUpFragment.
     */
    public static PopUpFragment newInstance(int timer_interval) {
        PopUpFragment fragment = new PopUpFragment();
        Bundle args = new Bundle();
        args.putInt(ARG_TIMER_INTERVAL, timer_interval);
        fragment.setArguments(args);
        return fragment;
    }

    public static PopUpFragment newInstance() {
        PopUpFragment fragment = new PopUpFragment();
        return fragment;
    }

    public PopUpFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            timer_interval = getArguments().getInt(ARG_TIMER_INTERVAL);
        } else {
            timer_interval = TIMER_INTERVAL_DEFAULT;
        }

        // keep the fragment and all its data across screen rotation
        //setRetainInstance(true);

        messageHandler = new HandlerPopUpMessages(this);

        if (savedInstanceState != null) {
            records = savedInstanceState.getParcelableArrayList(PopUpFragment.class.getSimpleName());
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
         outState.putParcelableArrayList(
                    PopUpFragment.class.getSimpleName(),
                    (java.util.ArrayList<? extends android.os.Parcelable>) records);
        super.onSaveInstanceState(outState);
    }

    Handler timerHandler = new Handler();
    Runnable timerRunnable = new Runnable() {
        @Override
        public void run() {
            if (adapter.getItemCount() > 0) {
                Record record = adapter.getRecords().get(0);
                long ts = record.getTimestamp();
                if (ts < System.currentTimeMillis() - timer_interval){
                    if (adapter.getItemCount() > 1){
                        record = adapter.getRecords().get(1);
                        record.setTimestamp(System.currentTimeMillis());
                    }
                    removeMessagePopUp();
                }
            }
            timerHandler.postDelayed(this, timer_interval);
        }
    };

    @Override
    public void onPause() {
        super.onPause();
        timerHandler.removeCallbacks(timerRunnable);
    }

    @Override
    public void onResume() {
        super.onResume();
        timerHandler.postDelayed(timerRunnable, timer_interval);
        if ( adapter.getItemCount() == 0 ) {
            cardView.setVisibility(View.GONE);
            mListener.onHidePopUpFrugment();
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        mListener.onPopUpFragmentStart();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_popup, container, false);
        recyclerView = (RecyclerView)view.findViewById(R.id.recyclerView);
        // recyclerView.setHasFixedSize(true);
        adapter = new RecyclerViewAdapter(records);
        LinearLayoutManager layoutManager = new MyLinearLayoutManager(getActivity());
        RecyclerView.ItemAnimator itemAnimator = new DefaultItemAnimator();

        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setItemAnimator(itemAnimator);

        cardView = (CardView)view.findViewById(R.id.cardView);
        return view;
    }


    public void addMessage0ToPopUp(int type, String text){
        Bundle msgBundle = new Bundle();
        msgBundle.putInt(HandlerPopUpMessages.ICON_ARG, type);
        msgBundle.putString(HandlerPopUpMessages.TEXT_ARG, text);
        Message msg = new Message();
        msg.what = HandlerPopUpMessages.ADD_MESSAGE;
        msg.setData(msgBundle);
        messageHandler.sendMessage(msg);
    }

    public void removeMessagePopUp() {
        Bundle msgBundle = new Bundle();
        Message msg = new Message();
        msg.what = HandlerPopUpMessages.REMOVE_MESSAGE_0;
        msg.setData(msgBundle);
        messageHandler.sendMessage(msg);
    }


    private void addMessageInternal(int type, String text) {
        Record record = new Record();
        record.setName(text);
        record.setType(Record.Type.values()[type]);
        record.setTimestamp(System.currentTimeMillis());
        adapter.getRecords().add(record);
        adapter.notifyItemInserted(adapter.getItemCount()-1);
        //adapter.notifyDataSetChanged();

        // Bellow there is hack. First show RecyclerView
        if ( adapter.getItemCount() == 1 ) {
            adapter.notifyDataSetChanged();
        }

        if ( adapter.getItemCount() > 0 ) {
            cardView.setVisibility(View.VISIBLE);
            mListener.onShowPopUpFrugment();
        }
    }


    private void removeMessage0Internal(){
        if ( adapter.getItemCount() > 0 ) {
            adapter.getRecords().remove(0);
            adapter.notifyItemRemoved(0);
            //adapter.notifyDataSetChanged();
        }
        if ( adapter.getItemCount() == 0 ) {
            cardView.setVisibility(View.GONE);
            mListener.onHidePopUpFrugment();
        }
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnFragmentInteractionListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
        messageHandler.removeCallbacksAndMessages(null);
    }


    /**
     * This interface must be implemented by activities that contain this
     * fragment to allow an interaction in this fragment to be communicated
     * to the activity and potentially other fragments contained in that
     * activity.
     * <p/>
     * See the Android Training lesson <a href=
     * "http://developer.android.com/training/basics/fragments/communicating.html"
     * >Communicating with Other Fragments</a> for more information.
     */
    public interface OnFragmentInteractionListener {
        void onPopUpFragmentStart();
        void onHidePopUpFrugment();
        void onShowPopUpFrugment();
    }


    public class MyLinearLayoutManager extends LinearLayoutManager {

        public MyLinearLayoutManager(Context context)    {
            super(context);
        }

        // Not worked
        @Override
        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
            return new RecyclerView.LayoutParams(
                    RecyclerView.LayoutParams.WRAP_CONTENT,
                    RecyclerView.LayoutParams.WRAP_CONTENT);
        }

        // Not worked
        @Override
        public boolean canScrollVertically() {
            //We do allow scrolling
            return true;
        }

        private int[] mMeasuredDimension = new int[2];

        @Override
        public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
                              int widthSpec, int heightSpec) {

            Log.i(">", "state " + state.toString());

            //if ( state.isPreLayout() ) {
            //    super.onMeasure(recycler, state, widthSpec, heightSpec);
            //} else
            {

                final int widthMode = View.MeasureSpec.getMode(widthSpec);
                final int heightMode = View.MeasureSpec.getMode(heightSpec);
                final int widthSize = View.MeasureSpec.getSize(widthSpec);
                final int heightSize = View.MeasureSpec.getSize(heightSpec);
                int width = 0;
                int height = 0;
                for (int i = 0; i < getItemCount(); i++) {
                    if (getOrientation() == HORIZONTAL) {
                        measureScrapChild(recycler, i,
                                View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                                heightSpec,
                                mMeasuredDimension);

                        width = width + mMeasuredDimension[0];
                        if (i == 0) {
                            height = mMeasuredDimension[1];
                        }
                    } else {
                        measureScrapChild(recycler, i,
                                widthSpec,
                                View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                                mMeasuredDimension);
                        height = height + mMeasuredDimension[1];
                        if (i == 0) {
                            width = mMeasuredDimension[0];
                        }
                    }
                }
                switch (widthMode) {
                    case View.MeasureSpec.EXACTLY:
                        width = widthSize;
                    case View.MeasureSpec.AT_MOST:
                    case View.MeasureSpec.UNSPECIFIED:
                }

                switch (heightMode) {
                    case View.MeasureSpec.EXACTLY:
                        height = heightSize;
                    case View.MeasureSpec.AT_MOST:
                    case View.MeasureSpec.UNSPECIFIED:
                }

                setMeasuredDimension(width, height);
            }
        }

        private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
                                       int heightSpec, int[] measuredDimension) {

            View view = null;
            // Bellow there is strong hack!
            try {
                view = recycler.getViewForPosition(position);
            }catch (IndexOutOfBoundsException ex){
                Log.i(">", "IndexOutOfBoundsException = " + ex + "position : " + position);
            }


            if (view != null) {

                // For adding Item Decor Insets to view
                //super.measureChildWithMargins(view, 0, 0);

                //recycler.bindViewToPosition(view, position);

                RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
                int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
                        getPaddingLeft() + getPaddingRight(), p.width);
                int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
                        getPaddingTop() + getPaddingBottom(), p.height);
                view.measure(childWidthSpec, childHeightSpec);
                measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
                measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
                recycler.recycleView(view);
            }
        }
    }

    private class HandlerPopUpMessages <T> extends Handler {

        public static final int ADD_MESSAGE = 100;
        public static final int REMOVE_MESSAGE_0 = 101;
        public static final String TEXT_ARG = "text";
        public static final String ICON_ARG = "icon";

        private final T fragment;

        public HandlerPopUpMessages(T fragment ){
            this.fragment = fragment;
        }

        @Override
        public void handleMessage(Message message){
            if (this.fragment != null){

                Bundle b = message.getData();

                switch (message.what){
                    case ADD_MESSAGE:
                        if(b == null)
                            new IllegalArgumentException("Message should be have params !");

                        String text = b.getString(TEXT_ARG);
                        int type = b.getInt(ICON_ARG);
                        ((PopUpFragment)fragment).addMessageInternal(type, text);
                        break;
                    case REMOVE_MESSAGE_0:
                        ((PopUpFragment)fragment).removeMessage0Internal();
                        break;
                }
            }
        }
    }
}



Layout
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".PopUpFragment">

    <android.support.v7.widget.CardView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/cardView"
        card_view:cardCornerRadius="6dp"
        card_view:cardBackgroundColor="#84ffff">

            <android.support.v7.widget.RecyclerView
                android:layout_width="400dp"
                android:layout_height="wrap_content"
                android:id="@+id/recyclerView" />
    </android.support.v7.widget.CardView>
</FrameLayout>



MainActivity.java
...
            mPopupWidget = (PopUpFragment)
                    getSupportFragmentManager().findFragmentById(R.id.popup);
            if (DEBUG) Log.d(TAG, "onCreate() : mPopupWidget = " + mPopupWidget);
            if( mPopupWidget == null ){
                getSupportFragmentManager().beginTransaction()
                        .replace(R.id.popup,
                                PopUpFragment.newInstance(),
                                PopUpFragment.class.getSimpleName())
                        .commit();
            }
...



RecyclerViewAdapter
package com.renal128.demo.recyclerviewdemo.adapter;

import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.renal128.demo.recyclerviewdemo.R;
import com.renal128.demo.recyclerviewdemo.model.Record;

import java.util.List;

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {

    private List<Record> records;

    public RecyclerViewAdapter(List<Record> records) {
        this.records = records;
    }

    public List<Record> getRecords() {
        return records;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recyclerview_item, viewGroup, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        Record record = records.get(i);
        int iconResourceId = 0;
        switch (record.getType()) {
            case GREEN:
                iconResourceId = R.drawable.green_circle;
                break;
            case RED:
                iconResourceId = R.drawable.red_circle;
                break;
            case YELLOW:
                iconResourceId = R.drawable.yellow_circle;
                break;
        }
        viewHolder.icon.setImageResource(iconResourceId);
        viewHolder.name.setText(record.getName());
        viewHolder.deleteButtonListener.setRecord(record);
        viewHolder.copyButtonListener.setRecord(record);
    }

    @Override
    public int getItemCount() {
        return records.size();
    }

    private void copy(Record record) {
        int position = records.indexOf(record);
        Record copy = record.copy();
        records.add(position + 1, copy);
        //notifyItemInserted(position + 1);
        notifyDataSetChanged();
    }

    private void delete(Record record) {
        int position = records.indexOf(record);
        Log.i(">" , "position=" + position);
        if( position != -1 ) {
            records.remove(position);
            //notifyItemRemoved(position);
            notifyDataSetChanged();
        }
    }

    class ViewHolder extends RecyclerView.ViewHolder {

        private TextView name;
        private ImageView icon;
        private Button deleteButton;
        private Button copyButton;
        private DeleteButtonListener deleteButtonListener;
        private CopyButtonListener copyButtonListener;

        public ViewHolder(View itemView) {
            super(itemView);
            name = (TextView) itemView.findViewById(R.id.recyclerViewItemName);
            icon = (ImageView) itemView.findViewById(R.id.recyclerViewItemIcon);
            deleteButton = (Button) itemView.findViewById(R.id.recyclerViewItemDeleteButton);
            copyButton = (Button) itemView.findViewById(R.id.recyclerViewItemCopyButton);
            deleteButtonListener = new DeleteButtonListener();
            copyButtonListener = new CopyButtonListener();
            deleteButton.setOnClickListener(deleteButtonListener);
            copyButton.setOnClickListener(copyButtonListener);
        }
    }

    private class CopyButtonListener implements View.OnClickListener {
        private Record record;

        @Override
        public void onClick(View v) {
            copy(record);
        }

        public void setRecord(Record record) {
            this.record = record;
        }
    }

    private class DeleteButtonListener implements View.OnClickListener {
        private Record record;

        @Override
        public void onClick(View v) {
            delete(record);
        }

        public void setRecord(Record record) {
            this.record = record;
        }
    }
}


Изначальный код был взят здесь: github.com/renal128/RecyclerViewDemo

Реализация с Handler's и Timer's: github.com/app-z/PopUpWidget

Посмотреть как это работает можно из Google Play: play.google.com/store/apps/details?id=net.appz.iconfounder

Комментарии (3)


  1. andkulikov
    12.04.2015 21:08
    +1

    чтобы анимация была и после вызова простого notifyDataSetChange(), а не только после явных сообщений типа notifyItemRemoved(index) можете попробовать написать так:
    добавить в RecyclerViewAdapter, например, в конструкторе

    setHasStableIds(true);

    и в нем же переопределить метод

    public long getItemId(int position)

    в котором возвращать некий уникальный айди типа long для элемента в этой позиции.
    тогда он сам поймет соответствие элементов до и после вызова notifyDataSetChange, какой удалился, добавился или переместился


  1. app-z Автор
    13.04.2015 10:19

    Спасибо!
    Попробовал. Теперь notifyDataSetChange. В onMeasure без падения и хака.
    Однако с прокруткой проблемы остались. Ее не стало после переопределения onMeasure. Если список превышает размер экрана, записи просто гдето внизу и недоступны.
    Опять же из статей wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1

    @Override
         public boolean canScrollVertically() {
             //We do allow scrolling
               return true;
         }
    

    И надо переопределить

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
    ...
    

    Не пробовал это


  1. serso
    28.04.2015 13:19

    В MyLinearLayoutManager есть ряд проблем, которые я решил при написании одного из своих приложений Say it right!. Я решил поделиться наработкой с миром и теперь LinearLayoutManager с поддержкой WRAP_CONTENT доступен в виде библиотеки. Подробности на странице проекта.

    Код класса:
    package org.solovyev.android.views.llm;
    
    import android.content.Context;
    import android.support.v7.widget.RecyclerView;
    import android.util.Log;
    import android.view.View;
    
    /**
     * {@link android.support.v7.widget.LinearLayoutManager} which wraps its content. Note that this class will always
     * wrap the content regardless of {@link android.support.v7.widget.RecyclerView} layout parameters.
     *
     * Now it's impossible to run add/remove animations with child views which have arbitrary dimensions (height for
     * VERTICAL orientation and width for HORIZONTAL). However if child views have fixed dimensions
     * {@link #setChildSize(int)} method might be used to let the layout manager know how big they are going to be.
     * If animations are not used at all then a normal measuring procedure will run and child views will be measured during
     * the measure pass.
     */
    public class LinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {
    
    	private static final int CHILD_WIDTH = 0;
    	private static final int CHILD_HEIGHT = 1;
    	private static final int DEFAULT_CHILD_SIZE = 100;
    
    	private final int[] childDimensions = new int[2];
    
    	private int childSize = DEFAULT_CHILD_SIZE;
    	private boolean hasChildSize;
    
    	@SuppressWarnings("UnusedDeclaration")
    	public LinearLayoutManager(Context context) {
    		super(context);
    	}
    
    	@SuppressWarnings("UnusedDeclaration")
    	public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
    		super(context, orientation, reverseLayout);
    	}
    
    	public static int makeUnspecifiedSpec() {
    		return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    	}
    
    	@Override
    	public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
    		final int widthMode = View.MeasureSpec.getMode(widthSpec);
    		final int heightMode = View.MeasureSpec.getMode(heightSpec);
    
    		final int widthSize = View.MeasureSpec.getSize(widthSpec);
    		final int heightSize = View.MeasureSpec.getSize(heightSpec);
    
    		final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
    		final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;
    
    		final int unspecified = makeUnspecifiedSpec();
    
    		if (exactWidth && exactHeight) {
    			// in case of exact calculations for both dimensions let's use default "onMeasure" implementation
    			super.onMeasure(recycler, state, widthSpec, heightSpec);
    			return;
    		}
    
    		final boolean vertical = getOrientation() == VERTICAL;
    
    		initChildDimensions(widthSize, heightSize, vertical);
    
    		int width = 0;
    		int height = 0;
    
    		// it's possible to get scrap views in recycler which are bound to old (invalid) adapter entities. This
    		// happens because their invalidation happens after "onMeasure" method. As a workaround let's clear the
    		// recycler now (it should not cause any performance issues while scrolling as "onMeasure" is never
    		// called whiles scrolling)
    		recycler.clear();
    
    		final int stateItemCount = state.getItemCount();
    		final int adapterItemCount = getItemCount();
    		// adapter always contains actual data while state might contain old data (f.e. data before the animation is
    		// done). As we want to measure the view with actual data we must use data from the adapter and not from  the
    		// state
    		for (int i = 0; i < adapterItemCount; i++) {
    			if (vertical) {
    				if (!hasChildSize) {
    					if (i < stateItemCount) {
    						// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
    						// we will use previously calculated dimensions
    						measureChild(recycler, i, widthSpec, unspecified, childDimensions);
    					} else {
    						logMeasureWarning(i);
    					}
    				}
    				height += childDimensions[CHILD_HEIGHT];
    				if (i == 0) {
    					width = childDimensions[CHILD_WIDTH];
    				}
    				if (height >= heightSize) {
    					break;
    				}
    			} else {
    				if (!hasChildSize) {
    					if (i < stateItemCount) {
    						// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
    						// we will use previously calculated dimensions
    						measureChild(recycler, i, unspecified, heightSpec, childDimensions);
    					} else {
    						logMeasureWarning(i);
    					}
    				}
    				width += childDimensions[CHILD_WIDTH];
    				if (i == 0) {
    					height = childDimensions[CHILD_HEIGHT];
    				}
    				if (width >= widthSize) {
    					break;
    				}
    			}
    		}
    
    		if ((vertical && height < heightSize) || (!vertical && width < widthSize)) {
    			// we really should wrap the contents of the view, let's do it
    
    			if (exactWidth) {
    				width = widthSize;
    			} else {
    				width += getPaddingLeft() + getPaddingRight();
    			}
    
    			if (exactHeight) {
    				height = heightSize;
    			} else {
    				height += getPaddingTop() + getPaddingBottom();
    			}
    
    			setMeasuredDimension(width, height);
    		} else {
    			// if calculated height/width exceeds requested height/width let's use default "onMeasure" implementation
    			super.onMeasure(recycler, state, widthSpec, heightSpec);
    		}
    	}
    
    	private void logMeasureWarning(int child) {
    		if (BuildConfig.DEBUG) {
    			Log.w("LinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
    					"To remove this message either use #setChildSize() method or don't run RecyclerView animations");
    		}
    	}
    
    	private void initChildDimensions(int width, int height, boolean vertical) {
    		if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
    			// already initialized, skipping
    			return;
    		}
    		if (vertical) {
    			childDimensions[CHILD_WIDTH] = width;
    			childDimensions[CHILD_HEIGHT] = childSize;
    		} else {
    			childDimensions[CHILD_WIDTH] = childSize;
    			childDimensions[CHILD_HEIGHT] = height;
    		}
    	}
    
    	@Override
    	public void setOrientation(int orientation) {
    		// might be called before the constructor of this class is called
    		//noinspection ConstantConditions
    		if (childDimensions != null) {
    			if (getOrientation() != orientation) {
    				childDimensions[CHILD_WIDTH] = 0;
    				childDimensions[CHILD_HEIGHT] = 0;
    			}
    		}
    		super.setOrientation(orientation);
    	}
    
    	public void clearChildSize() {
    		hasChildSize = false;
    		setChildSize(DEFAULT_CHILD_SIZE);
    	}
    
    	public void setChildSize(int childSize) {
    		hasChildSize = true;
    		if (this.childSize != childSize) {
    			this.childSize = childSize;
    			requestLayout();
    		}
    	}
    
    	private void measureChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] dimensions) {
    		final View child = recycler.getViewForPosition(position);
    
    		final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();
    
    		final int hPadding = getPaddingLeft() + getPaddingRight();
    		final int vPadding = getPaddingTop() + getPaddingBottom();
    
    		final int hMargin = p.leftMargin + p.rightMargin;
    		final int vMargin = p.topMargin + p.bottomMargin;
    
    		final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
    		final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
    
    		final int childWidthSpec = getChildMeasureSpec(widthSpec, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
    		final int c