CardViewにRecyclerViewを埋め込む





新しいウィジェット「RecyclerViewおよびCardView」に関するハブの投稿を読んだこと Android Lの新しいウィジェット」 、私はそれを試してみることにしました。 CardViewがRecyclerViewに埋め込まれているネットには多くの例があります。 CardViewにRecyclerViewを埋め込むという反対に興味があります。 このデザインを持つことは断片でした。



この記事から例をダウンロードしました。 複数のアイテムを削除するとすぐに問題が発生しました。 コードを確認した後、チェックを設定しました。



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は役に立ちません。 多くの人がすでに遭遇し、解決策があることが判明しました: 「ネストされたリサイクルビューの高さはコンテンツをラップしません



最初は、 AndroidのRecyclerViewの概要を見て、layoutManager.getDecoratedMeasuredHeight()...メソッドなどを使用することを考えましたが、それは役に立ちませんでした。 サイズが0を返しました。

LinearLayoutManagerでonMeasureを書き換える必要がありました。 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の無効なアイテムの位置XX(XX)の人々と同様の問題に遭遇しました アイテム数:XX#134 私が読んだすべてのトピックを見た:



実際、RecyclerViewのバグであり、まだ修正されていません。



詳細については、次を確認してください。

code.google.com/p/android/issues/detail?id=77846

code.google.com/p/android/issues/detail?id=77232



そして、上記の行は単なる解決策でした:



今、私はいくつかの汚い回避策をしています

if(index == 0){notifydatasetchange();} else {notifyItemRemoved(index)}



より正確には、要素の削除方法を見て、notifyItemRemoved(インデックス)をnotifydatasetchange()に置き換える必要があることに気付きました。 追加しても同様です。 ソリューションは最適ではありませんが、おそらく唯一のウィジェットの現在の実装で動作します。



このような決定はアニメーションを完全に殺しました。 研究を終了することがあります...



その結果、オーバーライドされたonMeasure()に落下の場所が見つかりました

 IndexOutOfBoundsException = java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:7
      
      





何らかの形で状況を傍受したり、事前のオフセットを要求したりするためのRecyclerViewコードのさらなる調査は成功しませんでした。 ハードハックを作りました! 厳密に判断しないでください)

 View view = null; try { view = recycler.getViewForPosition(position); }catch (IndexOutOfBoundsException ex){ Log.i(">", "IndexOutOfBoundsException = " + ex + "position : " + position); }
      
      







これでアニメーションが表示されましたが、最初の追加(初期化)後にリストは表示されませんでした。 次の操作の後、要素が追加され、すべてが表示されましたが。 要素を追加する方法に別のハックを作成しました。 彼が何をしているのかはっきりしていることを願っています

 if ( adapter.getItemCount() == 1 ) { adapter.notifyDataSetChanged(); }
      
      





記事「 RecyclerView LayoutManagerの構築-パート1」には、一見全体的な問題に対する解決策がありますが、私にはうまくいきませんでした。 サポートライブラリのバージョンを更新する必要があるか、SDKが必要な場合があります。 知りません

これは、実際にLayoutManagerをコンパイルするために必要な唯一のオーバーライドです。 実装は非常に簡単で、Recyclerから返されるすべての子ビューにデフォルトで適用するRecyclerView.LayoutParamsの新しいインスタンスを返すだけです。 これらのパラメーターは、getViewForPosition()から戻る前に各子に適用されます



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







結果として、我々は「幽霊」アプローチを採用しています。これは適用する価値があります...



そのようなPopUpウィジェットは、プログラムメッセージの表示に役立ちますが。 進行状況のウィンドウの代わりに。 タイマーを使用すると、一定の時間後に、またはすぐにクリックして、トップメッセージを削除できます



結果は、影とアニメーションを含む断片化されたリストです。 画面の回転を正しく処理しました。 アプリケーションに簡単に統合できます。 唯一の些細なこと。 フラグメントのスタックを並べ替えた後、ユーザーがアプリケーションを操作したときに、ウィンドウが常に表示されるとは限りませんでした。 おそらく、一部のコールバックはUI Thread ... Solutionにありません。ハンドラーを介してフラグメントにアクセスします。



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







UIスレッドとの通信
すべてのアプリには、ViewオブジェクトなどのUIオブジェクトを実行する独自の特別なスレッドがあります。 このスレッドはUIスレッドと呼ばれます。 UIスレッドで実行されているオブジェクトのみが、そのスレッド上の他のオブジェクトにアクセスできます。 スレッドプールのスレッドで実行するタスクはUIスレッドでは実行されないため、UIオブジェクトにアクセスできません。 バックグラウンドスレッドからUIスレッドにデータを移動するには、UIスレッドで実行されているハンドラーを使用します。

developer.android.com/training/multiple-threads/communicate-ui.html





他のフラグメントとの通信
多くの場合、たとえばユーザーイベントに基づいてコンテンツを変更するために、あるフラグメントが別のフラグメントと通信する必要があります。 フラグメント間の通信はすべて、関連付けられたアクティビティを通じて行われます。 2つのフラグメントが直接通信することはありません。

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









レイアウト
 <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



ハンドラーとタイマーの実装: github.com/app-z/PopUpWidget



Google Playからその仕組みを確認できます: play.google.com/store/apps/details?id=net.appz.iconfounder



All Articles