Updated Sunshine to start using RecyclerView instead of ListView.

This commit is contained in:
Dan Galpin
2015-05-25 03:02:34 -07:00
parent dbff5f9982
commit c4d1dffc5e
5 changed files with 114 additions and 90 deletions

View File

@@ -26,5 +26,6 @@ dependencies {
compile 'com.android.support:support-annotations:22.1.0' compile 'com.android.support:support-annotations:22.1.0'
compile 'com.android.support:gridlayout-v7:22.1.0' compile 'com.android.support:gridlayout-v7:22.1.0'
compile 'com.android.support:cardview-v7:22.1.1' compile 'com.android.support:cardview-v7:22.1.1'
compile 'com.android.support:recyclerview-v7:22.1.1'
compile 'com.google.android.gms:play-services-gcm:7.0.0' compile 'com.google.android.gms:play-services-gcm:7.0.0'
} }

View File

@@ -17,7 +17,8 @@ package com.example.android.sunshine.app;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.support.v4.widget.CursorAdapter; import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -28,28 +29,31 @@ import com.bumptech.glide.Glide;
/** /**
* {@link ForecastAdapter} exposes a list of weather forecasts * {@link ForecastAdapter} exposes a list of weather forecasts
* from a {@link Cursor} to a {@link android.widget.ListView}. * from a {@link android.database.Cursor} to a {@link android.support.v7.widget.RecyclerView}.
*/ */
public class ForecastAdapter extends CursorAdapter { public class ForecastAdapter extends RecyclerView.Adapter<ForecastAdapter.ForecastAdapterViewHolder> {
private static final int VIEW_TYPE_COUNT = 2;
private static final int VIEW_TYPE_TODAY = 0; private static final int VIEW_TYPE_TODAY = 0;
private static final int VIEW_TYPE_FUTURE_DAY = 1; private static final int VIEW_TYPE_FUTURE_DAY = 1;
// Flag to determine if we want to use a separate view for "today". // Flag to determine if we want to use a separate view for "today".
private boolean mUseTodayLayout = true; private boolean mUseTodayLayout = true;
private Cursor mCursor;
final private Context mContext;
/** /**
* Cache of the children views for a forecast list item. * Cache of the children views for a forecast list item.
*/ */
public static class ViewHolder { public class ForecastAdapterViewHolder extends RecyclerView.ViewHolder {
public final ImageView mIconView; public final ImageView mIconView;
public final TextView mDateView; public final TextView mDateView;
public final TextView mDescriptionView; public final TextView mDescriptionView;
public final TextView mHighTempView; public final TextView mHighTempView;
public final TextView mLowTempView; public final TextView mLowTempView;
public ViewHolder(View view) { public ForecastAdapterViewHolder(View view) {
super(view);
mIconView = (ImageView) view.findViewById(R.id.list_item_icon); mIconView = (ImageView) view.findViewById(R.id.list_item_icon);
mDateView = (TextView) view.findViewById(R.id.list_item_date_textview); mDateView = (TextView) view.findViewById(R.id.list_item_date_textview);
mDescriptionView = (TextView) view.findViewById(R.id.list_item_forecast_textview); mDescriptionView = (TextView) view.findViewById(R.id.list_item_forecast_textview);
@@ -58,42 +62,47 @@ public class ForecastAdapter extends CursorAdapter {
} }
} }
public ForecastAdapter(Context context, Cursor c, int flags) { public ForecastAdapter(Context context) {
super(context, c, flags); mContext = context;
} }
/*
This takes advantage of the fact that the viewGroup passed to onCreateViewHolder is the
RecyclerView that will be used to contain the view, so that it can get the current
ItemSelectionManager from the view.
One could implement this pattern without modifying RecyclerView by taking advantage
of the view tag to store the ItemChoiceManager.
*/
@Override @Override
public View newView(Context context, Cursor cursor, ViewGroup parent) { public ForecastAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Choose the layout type if ( viewGroup instanceof RecyclerView ) {
int viewType = getItemViewType(cursor.getPosition()); int layoutId = -1;
int layoutId = -1; switch (viewType) {
switch (viewType) { case VIEW_TYPE_TODAY: {
case VIEW_TYPE_TODAY: { layoutId = R.layout.list_item_forecast_today;
layoutId = R.layout.list_item_forecast_today; break;
break; }
} case VIEW_TYPE_FUTURE_DAY: {
case VIEW_TYPE_FUTURE_DAY: { layoutId = R.layout.list_item_forecast;
layoutId = R.layout.list_item_forecast; break;
break; }
} }
View view = LayoutInflater.from(viewGroup.getContext()).inflate(layoutId, viewGroup, false);
view.setFocusable(true);
return new ForecastAdapterViewHolder(view);
} else {
throw new RuntimeException("Not bound to RecyclerViewSelection");
} }
View view = LayoutInflater.from(context).inflate(layoutId, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
view.setTag(viewHolder);
return view;
} }
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void onBindViewHolder(ForecastAdapterViewHolder forecastAdapterViewHolder, int position) {
mCursor.moveToPosition(position);
ViewHolder viewHolder = (ViewHolder) view.getTag(); int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
int weatherId = cursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
int defaultImage; int defaultImage;
switch (getItemViewType(cursor.getPosition())) { switch (getItemViewType(position)) {
case VIEW_TYPE_TODAY: case VIEW_TYPE_TODAY:
defaultImage = Utility.getArtResourceForWeatherCondition(weatherId); defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
break; break;
@@ -101,27 +110,28 @@ public class ForecastAdapter extends CursorAdapter {
defaultImage = Utility.getIconResourceForWeatherCondition(weatherId); defaultImage = Utility.getIconResourceForWeatherCondition(weatherId);
} }
if (Utility.usingLocalGraphics(mContext)) { if ( Utility.usingLocalGraphics(mContext) ) {
viewHolder.mIconView. forecastAdapterViewHolder.mIconView.setImageResource(defaultImage);
setImageResource(defaultImage);
} else { } else {
Glide.with(mContext) Glide.with(mContext)
.load(Utility.getArtUrlForWeatherCondition(mContext, weatherId)) .load(Utility.getArtUrlForWeatherCondition(mContext, weatherId))
.error(defaultImage) .error(defaultImage)
.crossFade() .crossFade()
.into(viewHolder.mIconView); .into(forecastAdapterViewHolder.mIconView);
} }
// Read date from cursor // Read date from cursor
long dateInMillis = cursor.getLong(ForecastFragment.COL_WEATHER_DATE); long dateInMillis = mCursor.getLong(ForecastFragment.COL_WEATHER_DATE);
// Find TextView and set formatted date on it // Find TextView and set formatted date on it
viewHolder.mDateView.setText(Utility.getFriendlyDayString(context, dateInMillis)); forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis));
// Read weather forecast from cursor // Read weather forecast from cursor
String description = Utility.getStringForWeatherCondition(mContext, weatherId); String description = Utility.getStringForWeatherCondition(mContext, weatherId);
// Find TextView and set weather forecast on it // Find TextView and set weather forecast on it
viewHolder.mDescriptionView.setText(description); forecastAdapterViewHolder.mDescriptionView.setText(description);
viewHolder.mDescriptionView.setContentDescription(mContext.getString(R.string.a11y_forecast, description)); forecastAdapterViewHolder.mDescriptionView.setContentDescription(mContext.getString(R.string.a11y_forecast, description));
// For accessibility, we don't want a content description for the icon field // For accessibility, we don't want a content description for the icon field
// because the information is repeated in the description view and the icon // because the information is repeated in the description view and the icon
@@ -130,27 +140,39 @@ public class ForecastAdapter extends CursorAdapter {
// Read high temperature from cursor // Read high temperature from cursor
double high = mCursor.getDouble(ForecastFragment.COL_WEATHER_MAX_TEMP); double high = mCursor.getDouble(ForecastFragment.COL_WEATHER_MAX_TEMP);
String highString = Utility.formatTemperature(mContext, high); String highString = Utility.formatTemperature(mContext, high);
viewHolder.mHighTempView.setText(highString); forecastAdapterViewHolder.mHighTempView.setText(highString);
viewHolder.mHighTempView.setContentDescription(mContext.getString(R.string.a11y_high_temp, highString)); forecastAdapterViewHolder.mHighTempView.setContentDescription(mContext.getString(R.string.a11y_high_temp, highString));
// Read low temperature from cursor // Read low temperature from cursor
double low = mCursor.getDouble(ForecastFragment.COL_WEATHER_MIN_TEMP); double low = mCursor.getDouble(ForecastFragment.COL_WEATHER_MIN_TEMP);
String lowString = Utility.formatTemperature(mContext, low); String lowString = Utility.formatTemperature(mContext, low);
viewHolder.mLowTempView.setText(lowString); forecastAdapterViewHolder.mLowTempView.setText(lowString);
viewHolder.mLowTempView.setContentDescription(mContext.getString(R.string.a11y_low_temp, lowString)); forecastAdapterViewHolder.mLowTempView.setContentDescription(mContext.getString(R.string.a11y_low_temp, lowString));
} }
public void setUseTodayLayout(boolean useTodayLayout) { public void setUseTodayLayout(boolean useTodayLayout) {
mUseTodayLayout = useTodayLayout; mUseTodayLayout = useTodayLayout;
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
return (position == 0 && mUseTodayLayout) ? VIEW_TYPE_TODAY : VIEW_TYPE_FUTURE_DAY; return (position == 0 && mUseTodayLayout) ? VIEW_TYPE_TODAY : VIEW_TYPE_FUTURE_DAY;
} }
@Override @Override
public int getViewTypeCount() { public int getItemCount() {
return VIEW_TYPE_COUNT; if ( null == mCursor ) return 0;
return mCursor.getCount();
}
public void swapCursor(Cursor newCursor) {
mCursor = newCursor;
notifyDataSetChanged();
}
public Cursor getCursor() {
return mCursor;
} }
} }

View File

@@ -25,6 +25,8 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@@ -32,22 +34,20 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import com.example.android.sunshine.app.data.WeatherContract; import com.example.android.sunshine.app.data.WeatherContract;
import com.example.android.sunshine.app.sync.SunshineSyncAdapter; import com.example.android.sunshine.app.sync.SunshineSyncAdapter;
/** /**
* Encapsulates fetching the forecast and displaying it as a {@link ListView} layout. * Encapsulates fetching the forecast and displaying it as a {@link RecyclerView} layout.
*/ */
public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, SharedPreferences.OnSharedPreferenceChangeListener { public class ForecastFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, SharedPreferences.OnSharedPreferenceChangeListener {
public static final String LOG_TAG = ForecastFragment.class.getSimpleName(); public static final String LOG_TAG = ForecastFragment.class.getSimpleName();
private ForecastAdapter mForecastAdapter; private ForecastAdapter mForecastAdapter;
private ListView mListView; private RecyclerView mRecyclerView;
private int mPosition = ListView.INVALID_POSITION; private int mPosition = RecyclerView.NO_POSITION;
private boolean mUseTodayLayout; private boolean mUseTodayLayout;
private static final String SELECTED_KEY = "selected_position"; private static final String SELECTED_KEY = "selected_position";
@@ -149,34 +149,36 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
Bundle savedInstanceState) { Bundle savedInstanceState) {
// The ForecastAdapter will take data from a source and // The ForecastAdapter will take data from a source and
// use it to populate the ListView it's attached to. // use it to populate the RecyclerView it's attached to.
mForecastAdapter = new ForecastAdapter(getActivity(), null, 0); mForecastAdapter = new ForecastAdapter(getActivity());
View rootView = inflater.inflate(R.layout.fragment_main, container, false); View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// Get a reference to the ListView, and attach this adapter to it. // Get a reference to the RecyclerView, and attach this adapter to it.
mListView = (ListView) rootView.findViewById(R.id.listview_forecast); mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_forecast);
View emptyView = rootView.findViewById(R.id.listview_forecast_empty);
mListView.setEmptyView(emptyView);
mListView.setAdapter(mForecastAdapter);
// We'll call our MainActivity
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override // Set the layout manager
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) { mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
// CursorAdapter returns a cursor at the correct position for getItem(), or null // View emptyView = rootView.findViewById(R.id.recyclerview_forecast_empty);
// if it cannot seek to that position. mRecyclerView.setAdapter(mForecastAdapter);
Cursor cursor = (Cursor) adapterView.getItemAtPosition(position); // We'll call our MainActivity
if (cursor != null) { // mRecyclerView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
String locationSetting = Utility.getPreferredLocation(getActivity()); //
((Callback) getActivity()) // @Override
.onItemSelected(WeatherContract.WeatherEntry.buildWeatherLocationWithDate( // public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
locationSetting, cursor.getLong(COL_WEATHER_DATE) // // CursorAdapter returns a cursor at the correct position for getItem(), or null
)); // // if it cannot seek to that position.
} // Cursor cursor = (Cursor) adapterView.getItemAtPosition(position);
mPosition = position; // if (cursor != null) {
} // String locationSetting = Utility.getPreferredLocation(getActivity());
}); // ((Callback) getActivity())
// .onItemSelected(WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
// locationSetting, cursor.getLong(COL_WEATHER_DATE)
// ));
// }
// mPosition = position;
// }
// });
// If there's instance state, mine it for useful information. // If there's instance state, mine it for useful information.
// The end-goal here is that the user never knows that turning their device sideways // The end-goal here is that the user never knows that turning their device sideways
@@ -184,7 +186,7 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
// or magically appeared to take advantage of room, but data or place in the app was never // or magically appeared to take advantage of room, but data or place in the app was never
// actually *lost*. // actually *lost*.
if (savedInstanceState != null && savedInstanceState.containsKey(SELECTED_KEY)) { if (savedInstanceState != null && savedInstanceState.containsKey(SELECTED_KEY)) {
// The listview probably hasn't even been populated yet. Actually perform the // The RecyclerView probably hasn't even been populated yet. Actually perform the
// swapout in onLoadFinished. // swapout in onLoadFinished.
mPosition = savedInstanceState.getInt(SELECTED_KEY); mPosition = savedInstanceState.getInt(SELECTED_KEY);
} }
@@ -233,9 +235,9 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
// When tablets rotate, the currently selected list item needs to be saved. // When tablets rotate, the currently selected list item needs to be saved.
// When no item is selected, mPosition will be set to Listview.INVALID_POSITION, // When no item is selected, mPosition will be set to RecyclerView.INVALID_POSITION,
// so check for that before storing. // so check for that before storing.
if (mPosition != ListView.INVALID_POSITION) { if (mPosition != RecyclerView.NO_POSITION) {
outState.putInt(SELECTED_KEY, mPosition); outState.putInt(SELECTED_KEY, mPosition);
} }
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
@@ -267,10 +269,10 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) { public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mForecastAdapter.swapCursor(data); mForecastAdapter.swapCursor(data);
if (mPosition != ListView.INVALID_POSITION) { if (mPosition != RecyclerView.NO_POSITION) {
// If we don't need to restart the loader, and there's a desired position to restore // If we don't need to restart the loader, and there's a desired position to restore
// to, do so now. // to, do so now.
mListView.smoothScrollToPosition(mPosition); mRecyclerView.smoothScrollToPosition(mPosition);
} }
updateEmptyView(); updateEmptyView();
} }
@@ -292,8 +294,8 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
use to determine why they aren't seeing weather. use to determine why they aren't seeing weather.
*/ */
private void updateEmptyView() { private void updateEmptyView() {
if ( mForecastAdapter.getCount() == 0 ) { if ( mForecastAdapter.getItemCount() == 0 ) {
TextView tv = (TextView) getView().findViewById(R.id.listview_forecast_empty); TextView tv = (TextView) getView().findViewById(R.id.recyclerview_forecast_empty);
if ( null != tv ) { if ( null != tv ) {
// if cursor is empty, why? do we have an invalid location // if cursor is empty, why? do we have an invalid location
int message = R.string.empty_forecast_list; int message = R.string.empty_forecast_list;

View File

@@ -18,15 +18,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.example.android.sunshine.app.ForecastFragment"> tools:context="com.example.android.sunshine.app.ForecastFragment">
<ListView <android.support.v7.widget.RecyclerView
style="@style/ForecastListStyle" style="@style/ForecastListStyle"
android:id="@+id/listview_forecast" android:id="@+id/recyclerview_forecast"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"/>
android:divider="@null" />
<!-- empty list --> <!-- empty list -->
<TextView <TextView
android:id="@+id/listview_forecast_empty" android:id="@+id/recyclerview_forecast_empty"
android:text="@string/empty_forecast_list" android:text="@string/empty_forecast_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -19,15 +19,15 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:elevation="@dimen/appbar_elevation" android:elevation="@dimen/appbar_elevation"
tools:context="com.example.android.sunshine.app.ForecastFragment"> tools:context="com.example.android.sunshine.app.ForecastFragment">
<ListView <android.support.v7.widget.RecyclerView
style="@style/ForecastListStyle" style="@style/ForecastListStyle"
android:id="@+id/listview_forecast" android:id="@+id/recyclerview_forecast"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:divider="@null" /> android:divider="@null" />
<!-- empty list --> <!-- empty list -->
<TextView <TextView
android:id="@+id/listview_forecast_empty" android:id="@+id/recyclerview_forecast_empty"
android:text="@string/empty_forecast_list" android:text="@string/empty_forecast_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"