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:gridlayout-v7:22.1.0'
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'
}

View File

@@ -17,7 +17,8 @@ package com.example.android.sunshine.app;
import android.content.Context;
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.View;
import android.view.ViewGroup;
@@ -28,28 +29,31 @@ import com.bumptech.glide.Glide;
/**
* {@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_FUTURE_DAY = 1;
// Flag to determine if we want to use a separate view for "today".
private boolean mUseTodayLayout = true;
private Cursor mCursor;
final private Context mContext;
/**
* 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 TextView mDateView;
public final TextView mDescriptionView;
public final TextView mHighTempView;
public final TextView mLowTempView;
public ViewHolder(View view) {
public ForecastAdapterViewHolder(View view) {
super(view);
mIconView = (ImageView) view.findViewById(R.id.list_item_icon);
mDateView = (TextView) view.findViewById(R.id.list_item_date_textview);
mDescriptionView = (TextView) view.findViewById(R.id.list_item_forecast_textview);
@@ -58,14 +62,21 @@ public class ForecastAdapter extends CursorAdapter {
}
}
public ForecastAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
public ForecastAdapter(Context context) {
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
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// Choose the layout type
int viewType = getItemViewType(cursor.getPosition());
public ForecastAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
if ( viewGroup instanceof RecyclerView ) {
int layoutId = -1;
switch (viewType) {
case VIEW_TYPE_TODAY: {
@@ -77,23 +88,21 @@ public class ForecastAdapter extends CursorAdapter {
break;
}
}
View view = LayoutInflater.from(context).inflate(layoutId, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
view.setTag(viewHolder);
return view;
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");
}
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder viewHolder = (ViewHolder) view.getTag();
int weatherId = cursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
public void onBindViewHolder(ForecastAdapterViewHolder forecastAdapterViewHolder, int position) {
mCursor.moveToPosition(position);
int weatherId = mCursor.getInt(ForecastFragment.COL_WEATHER_CONDITION_ID);
int defaultImage;
switch (getItemViewType(cursor.getPosition())) {
switch (getItemViewType(position)) {
case VIEW_TYPE_TODAY:
defaultImage = Utility.getArtResourceForWeatherCondition(weatherId);
break;
@@ -102,26 +111,27 @@ public class ForecastAdapter extends CursorAdapter {
}
if ( Utility.usingLocalGraphics(mContext) ) {
viewHolder.mIconView.
setImageResource(defaultImage);
forecastAdapterViewHolder.mIconView.setImageResource(defaultImage);
} else {
Glide.with(mContext)
.load(Utility.getArtUrlForWeatherCondition(mContext, weatherId))
.error(defaultImage)
.crossFade()
.into(viewHolder.mIconView);
.into(forecastAdapterViewHolder.mIconView);
}
// 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
viewHolder.mDateView.setText(Utility.getFriendlyDayString(context, dateInMillis));
forecastAdapterViewHolder.mDateView.setText(Utility.getFriendlyDayString(mContext, dateInMillis));
// Read weather forecast from cursor
String description = Utility.getStringForWeatherCondition(mContext, weatherId);
// Find TextView and set weather forecast on it
viewHolder.mDescriptionView.setText(description);
viewHolder.mDescriptionView.setContentDescription(mContext.getString(R.string.a11y_forecast, description));
forecastAdapterViewHolder.mDescriptionView.setText(description);
forecastAdapterViewHolder.mDescriptionView.setContentDescription(mContext.getString(R.string.a11y_forecast, description));
// 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
@@ -130,27 +140,39 @@ public class ForecastAdapter extends CursorAdapter {
// Read high temperature from cursor
double high = mCursor.getDouble(ForecastFragment.COL_WEATHER_MAX_TEMP);
String highString = Utility.formatTemperature(mContext, high);
viewHolder.mHighTempView.setText(highString);
viewHolder.mHighTempView.setContentDescription(mContext.getString(R.string.a11y_high_temp, highString));
forecastAdapterViewHolder.mHighTempView.setText(highString);
forecastAdapterViewHolder.mHighTempView.setContentDescription(mContext.getString(R.string.a11y_high_temp, highString));
// Read low temperature from cursor
double low = mCursor.getDouble(ForecastFragment.COL_WEATHER_MIN_TEMP);
String lowString = Utility.formatTemperature(mContext, low);
viewHolder.mLowTempView.setText(lowString);
viewHolder.mLowTempView.setContentDescription(mContext.getString(R.string.a11y_low_temp, lowString));
forecastAdapterViewHolder.mLowTempView.setText(lowString);
forecastAdapterViewHolder.mLowTempView.setContentDescription(mContext.getString(R.string.a11y_low_temp, lowString));
}
public void setUseTodayLayout(boolean useTodayLayout) {
mUseTodayLayout = useTodayLayout;
}
@Override
public int getItemViewType(int position) {
return (position == 0 && mUseTodayLayout) ? VIEW_TYPE_TODAY : VIEW_TYPE_FUTURE_DAY;
}
@Override
public int getViewTypeCount() {
return VIEW_TYPE_COUNT;
public int getItemCount() {
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.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -32,22 +34,20 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import com.example.android.sunshine.app.data.WeatherContract;
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 static final String LOG_TAG = ForecastFragment.class.getSimpleName();
private ForecastAdapter mForecastAdapter;
private ListView mListView;
private int mPosition = ListView.INVALID_POSITION;
private RecyclerView mRecyclerView;
private int mPosition = RecyclerView.NO_POSITION;
private boolean mUseTodayLayout;
private static final String SELECTED_KEY = "selected_position";
@@ -149,34 +149,36 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
Bundle savedInstanceState) {
// The ForecastAdapter will take data from a source and
// use it to populate the ListView it's attached to.
mForecastAdapter = new ForecastAdapter(getActivity(), null, 0);
// use it to populate the RecyclerView it's attached to.
mForecastAdapter = new ForecastAdapter(getActivity());
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
// Get a reference to the ListView, and attach this adapter to it.
mListView = (ListView) rootView.findViewById(R.id.listview_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() {
// Get a reference to the RecyclerView, and attach this adapter to it.
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_forecast);
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
// 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);
if (cursor != null) {
String locationSetting = Utility.getPreferredLocation(getActivity());
((Callback) getActivity())
.onItemSelected(WeatherContract.WeatherEntry.buildWeatherLocationWithDate(
locationSetting, cursor.getLong(COL_WEATHER_DATE)
));
}
mPosition = position;
}
});
// Set the layout manager
mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
// View emptyView = rootView.findViewById(R.id.recyclerview_forecast_empty);
mRecyclerView.setAdapter(mForecastAdapter);
// We'll call our MainActivity
// mRecyclerView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
//
// @Override
// public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
// // 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);
// 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.
// 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
// actually *lost*.
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.
mPosition = savedInstanceState.getInt(SELECTED_KEY);
}
@@ -233,9 +235,9 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
@Override
public void onSaveInstanceState(Bundle outState) {
// 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.
if (mPosition != ListView.INVALID_POSITION) {
if (mPosition != RecyclerView.NO_POSITION) {
outState.putInt(SELECTED_KEY, mPosition);
}
super.onSaveInstanceState(outState);
@@ -267,10 +269,10 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor 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
// to, do so now.
mListView.smoothScrollToPosition(mPosition);
mRecyclerView.smoothScrollToPosition(mPosition);
}
updateEmptyView();
}
@@ -292,8 +294,8 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa
use to determine why they aren't seeing weather.
*/
private void updateEmptyView() {
if ( mForecastAdapter.getCount() == 0 ) {
TextView tv = (TextView) getView().findViewById(R.id.listview_forecast_empty);
if ( mForecastAdapter.getItemCount() == 0 ) {
TextView tv = (TextView) getView().findViewById(R.id.recyclerview_forecast_empty);
if ( null != tv ) {
// if cursor is empty, why? do we have an invalid location
int message = R.string.empty_forecast_list;

View File

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

View File

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