diff --git a/app/build.gradle b/app/build.gradle index 29232c5..735d5a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java b/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java index 4df5d55..ed69bcb 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastAdapter.java @@ -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 { - 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,42 +62,47 @@ 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()); - int layoutId = -1; - switch (viewType) { - case VIEW_TYPE_TODAY: { - layoutId = R.layout.list_item_forecast_today; - break; - } - case VIEW_TYPE_FUTURE_DAY: { - layoutId = R.layout.list_item_forecast; - break; + public ForecastAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + if ( viewGroup instanceof RecyclerView ) { + int layoutId = -1; + switch (viewType) { + case VIEW_TYPE_TODAY: { + layoutId = R.layout.list_item_forecast_today; + break; + } + case VIEW_TYPE_FUTURE_DAY: { + layoutId = R.layout.list_item_forecast; + 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 - 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; @@ -101,27 +110,28 @@ public class ForecastAdapter extends CursorAdapter { defaultImage = Utility.getIconResourceForWeatherCondition(weatherId); } - if (Utility.usingLocalGraphics(mContext)) { - viewHolder.mIconView. - setImageResource(defaultImage); + if ( Utility.usingLocalGraphics(mContext) ) { + 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; } } \ No newline at end of file diff --git a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java index 341773e..0f5115a 100644 --- a/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java +++ b/app/src/main/java/com/example/android/sunshine/app/ForecastFragment.java @@ -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, 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 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; diff --git a/app/src/main/res/layout-sw600dp/fragment_main.xml b/app/src/main/res/layout-sw600dp/fragment_main.xml index c77f393..6b7e76c 100644 --- a/app/src/main/res/layout-sw600dp/fragment_main.xml +++ b/app/src/main/res/layout-sw600dp/fragment_main.xml @@ -18,15 +18,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.android.sunshine.app.ForecastFragment"> - + android:layout_height="match_parent"/> -