From 492cd7371c6d8fddc4de85887a4ed9a89d602767 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 8 Apr 2015 14:58:45 -0700 Subject: Replace ListView with RecyclerView in call log. Yay, finally! + Replace ListView with RecyclerView in layout and fragment files. + Change GroupingListAdapter to extend RecyclerView.Adapter instead of BaseAdapter. + Change CallLogListItemViews to extend RecyclerView.ViewHolder. + Adapt onBindViewHolder and onCreateViewHolder methods in the CallLogAdapter. + Update/rework tests for related classes. + Fix a bug in the GroupingListAdapter, where childCount was not updated for standalone views, and the previously cached group size was used instead. Set childCount to 1 for standalone views. - Removed the idea of creating different views for standalone vs group vs group headers from the adapters. This logic has not been used for quite some time and all these functions funneled into createView/bindView methods anyways, so there is no logical difference. If we need to create custom views in the future, we can leverage onCreateViewHolder's viewType parameter. Bug: 19372817 Change-Id: I1b7289340600609669db22d8bc89265240d0b561 --- res/layout/call_log_fragment.xml | 13 +-- src/com/android/dialer/calllog/CallLogAdapter.java | 56 +++-------- .../android/dialer/calllog/CallLogFragment.java | 34 ++++--- .../dialer/calllog/CallLogListItemViews.java | 4 +- .../dialer/calllog/GroupingListAdapter.java | 79 ++++----------- .../android/dialer/calllog/CallLogAdapterTest.java | 16 ++- .../dialer/calllog/CallLogFragmentTest.java | 111 +++++++++++---------- .../dialer/calllog/GroupingListAdapterTests.java | 53 ++++------ 8 files changed, 154 insertions(+), 212 deletions(-) diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml index 74c630959..c126b778b 100644 --- a/res/layout/call_log_fragment.xml +++ b/res/layout/call_log_fragment.xml @@ -61,18 +61,11 @@ - - + android:background="@color/background_dialer_list_items" /> 0; - listView.setVisibility(showListView ? View.VISIBLE : View.GONE); + mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE); mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE); if (mScrollToTop) { @@ -213,8 +212,9 @@ public class CallLogFragment extends ListFragment // will not experience the illusion of downward motion. Instead, // if we're not already near the top of the list, we instantly jump // near the top, and animate from there. - if (listView.getFirstVisiblePosition() > 5) { - listView.setSelection(5); + if (mLayoutManager.findFirstVisibleItemPosition() > 5) { + // TODO: Jump to near the top, then begin smooth scroll. + mRecyclerView.smoothScrollToPosition(0); } // Workaround for framework issue: the smooth-scroll doesn't // occur if setSelection() is called immediately before. @@ -224,7 +224,7 @@ public class CallLogFragment extends ListFragment if (getActivity() == null || getActivity().isFinishing()) { return; } - listView.smoothScrollToPosition(0); + mRecyclerView.smoothScrollToPosition(0); } }); @@ -269,6 +269,17 @@ public class CallLogFragment extends ListFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { View view = inflater.inflate(R.layout.call_log_fragment, container, false); + + mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + mRecyclerView.setHasFixedSize(true); + mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + + String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); + mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, + new ContactInfoHelper(getActivity(), currentCountryIso), this); + mRecyclerView.setAdapter(mAdapter); + mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); mStatusMessageView = view.findViewById(R.id.voicemail_status); mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); @@ -280,7 +291,6 @@ public class CallLogFragment extends ListFragment public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mEmptyListView = view.findViewById(R.id.empty_list_view); - getListView().setItemsCanFocus(true); updateEmptyMessage(mCallTypeFilter); } diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java index 9d11a3ab6..f2bed531f 100644 --- a/src/com/android/dialer/calllog/CallLogListItemViews.java +++ b/src/com/android/dialer/calllog/CallLogListItemViews.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.Resources; import android.net.Uri; import android.provider.CallLog.Calls; +import android.support.v7.widget.RecyclerView; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.view.View; @@ -44,7 +45,7 @@ import com.android.dialer.R; * is a way of isolating view logic from the CallLogAdapter. We should consider moving that logic * if the call log list item is eventually represented as a UI component. */ -public final class CallLogListItemViews { +public final class CallLogListItemViews extends RecyclerView.ViewHolder { /** The root view of the call log list item */ public final View rootView; /** The quick contact badge for the contact. */ @@ -147,6 +148,7 @@ public final class CallLogListItemViews { PhoneCallDetailsViews phoneCallDetailsViews, View callLogEntryView, TextView dayGroupHeader) { + super(rootView); mContext = context; this.rootView = rootView; diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java index 78955492e..501e88df0 100644 --- a/src/com/android/dialer/calllog/GroupingListAdapter.java +++ b/src/com/android/dialer/calllog/GroupingListAdapter.java @@ -21,6 +21,8 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; +import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; @@ -34,7 +36,7 @@ import com.android.contacts.common.testing.NeededForTesting; * The list has three types of elements: stand-alone, group header and group child. Groups are * collapsible and collapsed by default. This is used by the call log to group related entries. */ -abstract class GroupingListAdapter extends BaseAdapter { +abstract class GroupingListAdapter extends RecyclerView.Adapter { private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16; private static final int GROUP_METADATA_ARRAY_INCREMENT = 128; @@ -109,11 +111,6 @@ abstract class GroupingListAdapter extends BaseAdapter { public void onChanged() { notifyDataSetChanged(); } - - @Override - public void onInvalidated() { - notifyDataSetInvalidated(); - } }; public GroupingListAdapter(Context context) { @@ -127,15 +124,7 @@ abstract class GroupingListAdapter extends BaseAdapter { */ protected abstract void addGroups(Cursor cursor); - protected abstract View newStandAloneView(Context context, ViewGroup parent); - protected abstract void bindStandAloneView(View view, Context context, Cursor cursor); - - protected abstract View newGroupView(Context context, ViewGroup parent); - protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize, - boolean expanded); - - protected abstract View newChildView(Context context, ViewGroup parent); - protected abstract void bindChildView(View view, Context context, Cursor cursor); + protected abstract void onContentChanged(); /** * Cache should be reset whenever the cursor changes or groups are expanded or collapsed. @@ -149,9 +138,6 @@ abstract class GroupingListAdapter extends BaseAdapter { mPositionCache.clear(); } - protected void onContentChanged() { - } - public void changeCursor(Cursor cursor) { if (cursor == mCursor) { return; @@ -171,13 +157,10 @@ abstract class GroupingListAdapter extends BaseAdapter { cursor.registerDataSetObserver(mDataSetObserver); mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id"); notifyDataSetChanged(); - } else { - // notify the observers about the lack of a data set - notifyDataSetInvalidated(); } - } + @NeededForTesting public Cursor getCursor() { return mCursor; } @@ -231,7 +214,8 @@ abstract class GroupingListAdapter extends BaseAdapter { return need; } - public int getCount() { + @Override + public int getItemCount() { if (mCursor == null) { return 0; } @@ -343,6 +327,7 @@ abstract class GroupingListAdapter extends BaseAdapter { if (position < listPosition) { metadata.itemType = ITEM_TYPE_STANDALONE; metadata.cursorPosition = cursorPosition - (listPosition - position); + metadata.childCount = 1; return; } @@ -382,6 +367,7 @@ abstract class GroupingListAdapter extends BaseAdapter { // The required item is past the last group metadata.itemType = ITEM_TYPE_STANDALONE; metadata.cursorPosition = cursorPosition + (position - listPosition); + metadata.childCount = 1; } /** @@ -421,12 +407,6 @@ abstract class GroupingListAdapter extends BaseAdapter { notifyDataSetChanged(); } - @Override - public int getViewTypeCount() { - return 3; - } - - @Override public int getItemViewType(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.itemType; @@ -454,37 +434,16 @@ abstract class GroupingListAdapter extends BaseAdapter { } } - public View getView(int position, View convertView, ViewGroup parent) { - obtainPositionMetadata(mPositionMetadata, position); - View view = convertView; - if (view == null) { - switch (mPositionMetadata.itemType) { - case ITEM_TYPE_STANDALONE: - view = newStandAloneView(mContext, parent); - break; - case ITEM_TYPE_GROUP_HEADER: - view = newGroupView(mContext, parent); - break; - case ITEM_TYPE_IN_GROUP: - view = newChildView(mContext, parent); - break; - } - } - - mCursor.moveToPosition(mPositionMetadata.cursorPosition); - switch (mPositionMetadata.itemType) { - case ITEM_TYPE_STANDALONE: - bindStandAloneView(view, mContext, mCursor); - break; - case ITEM_TYPE_GROUP_HEADER: - bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount, - mPositionMetadata.isExpanded); - break; - case ITEM_TYPE_IN_GROUP: - bindChildView(view, mContext, mCursor); - break; - + /** + * Used for setting the cursor without triggering a UI thread update. + */ + @NeededForTesting + public void setCursorForTesting(Cursor cursor) { + if (cursor != null) { + mCursor = cursor; + cursor.registerContentObserver(mChangeObserver); + cursor.registerDataSetObserver(mDataSetObserver); + mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id"); } - return view; } } diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java index 845e279c9..bffbe5cf5 100644 --- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java +++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java @@ -87,7 +87,8 @@ public class CallLogAdapterTest extends AndroidTestCase { mCursor.addRow(createCallLogEntry()); // Bind the views of a single row. - mAdapter.bindStandAloneView(mView, getContext(), mCursor); + mAdapter.changeCursor(mCursor); + mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); @@ -105,7 +106,8 @@ public class CallLogAdapterTest extends AndroidTestCase { mCursor.addRow(createCallLogEntryWithCachedValues()); // Bind the views of a single row. - mAdapter.bindStandAloneView(mView, getContext(), mCursor); + mAdapter.changeCursor(mCursor); + mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); @@ -123,7 +125,9 @@ public class CallLogAdapterTest extends AndroidTestCase { mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo()); // Bind the views of a single row. - mAdapter.bindStandAloneView(mView, getContext(), mCursor); + mAdapter.changeCursor(mCursor); + mAdapter.onBindViewHolder( + CallLogListItemViews.fromView(getContext(), mView), 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); @@ -138,7 +142,8 @@ public class CallLogAdapterTest extends AndroidTestCase { mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo()); // Bind the views of a single row. - mAdapter.bindStandAloneView(mView, getContext(), mCursor); + mAdapter.changeCursor(mCursor); + mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0); // Cache and call log are up-to-date: no need to request update. assertEquals(0, mAdapter.getContactInfoCache().requests.size()); @@ -153,7 +158,8 @@ public class CallLogAdapterTest extends AndroidTestCase { mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, info); // Bind the views of a single row. - mAdapter.bindStandAloneView(mView, getContext(), mCursor); + mAdapter.changeCursor(mCursor); + mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java index b57489d55..fe14f8709 100644 --- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java +++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java @@ -30,6 +30,7 @@ import android.net.Uri; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.VoicemailContract; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.test.ActivityInstrumentationTestCase2; @@ -94,9 +95,9 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2