From cf8b67e28b3a2cfbef473b36b4ec9636a5ae93c3 Mon Sep 17 00:00:00 2001 From: Dan Galpin Date: Mon, 25 May 2015 03:09:28 -0700 Subject: [PATCH] Added selection into forecast adapter. --- .../android/sunshine/app/ForecastAdapter.java | 26 +- .../sunshine/app/ForecastFragment.java | 54 +++- .../sunshine/app/ItemChoiceManager.java | 237 ++++++++++++++++++ .../layout-land/list_item_forecast_today.xml | 29 +++ .../res/layout-sw600dp-port/activity_main.xml | 8 +- .../main/res/layout-sw600dp/activity_main.xml | 5 +- app/src/main/res/layout/activity_main.xml | 3 +- .../main/res/layout/fragment_main_base.xml | 7 +- .../layout/list_item_base_forecast_today.xml | 80 ++++++ .../res/layout/list_item_forecast_today.xml | 65 +---- app/src/main/res/values-sw600dp/styles.xml | 2 +- app/src/main/res/values/attrs.xml | 4 + 12 files changed, 443 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java create mode 100644 app/src/main/res/layout-land/list_item_forecast_today.xml create mode 100644 app/src/main/res/layout/list_item_base_forecast_today.xml 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 ba12a38..95cb45d 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 @@ -18,6 +18,7 @@ package com.example.android.sunshine.app; import android.content.Context; import android.database.Cursor; import android.os.Build; +import android.os.Bundle; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -46,6 +47,7 @@ public class ForecastAdapter extends RecyclerView.Adapter onCreateLoader(int i, Bundle bundle) { // This is called when a new Loader needs to be created. This @@ -274,6 +295,27 @@ public class ForecastFragment extends Fragment implements LoaderManager.LoaderCa mRecyclerView.smoothScrollToPosition(mPosition); } updateEmptyView(); + if ( data.getCount() > 0 ) { + mRecyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + // Since we know we're going to get items, we keep the listener around until + // we see Children. + if (mRecyclerView.getChildCount() > 0) { + mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this); + int itemPosition = mForecastAdapter.getSelectedItemPosition(); + if ( RecyclerView.NO_POSITION == itemPosition ) itemPosition = 0; + RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(itemPosition); + if ( null != vh && mAutoSelectView ) { + mForecastAdapter.selectView( vh ); + } + return true; + } + return false; + } + }); + } + } @Override diff --git a/app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java b/app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java new file mode 100644 index 0000000..f046fb0 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/ItemChoiceManager.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.sunshine.app; + +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.LongSparseArray; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.widget.AbsListView; +import android.widget.Checkable; + +/** + * The ItemChoiceManager class keeps track of which positions have been selected. Note that it + * doesn't take advantage of new adapter features to track changes in the underlying data. + */ +public class ItemChoiceManager { + private final String LOG_TAG = MainActivity.class.getSimpleName(); + private final String SELECTED_ITEMS_KEY = "SIK"; + private int mChoiceMode; + + private RecyclerView.Adapter mAdapter; + private RecyclerView.AdapterDataObserver mAdapterDataObserver = new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + if (mAdapter != null && mAdapter.hasStableIds()) + confirmCheckedPositionsById(mAdapter.getItemCount()); + } + }; + + private ItemChoiceManager() { + } + + ; + + public ItemChoiceManager(RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + /** + * How many positions in either direction we will search to try to + * find a checked item with a stable ID that moved position across + * a data set change. If the item isn't found it will be unselected. + */ + private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; + + /** + * Running state of which positions are currently checked + */ + SparseBooleanArray mCheckStates = new SparseBooleanArray(); + + /** + * Running state of which IDs are currently checked. + * If there is a value for a given key, the checked state for that ID is true + * and the value holds the last known position in the adapter for that id. + */ + LongSparseArray mCheckedIdStates = new LongSparseArray(); + + public void onClick(RecyclerView.ViewHolder vh) { + if (mChoiceMode == AbsListView.CHOICE_MODE_NONE) + return; + + int checkedItemCount = mCheckStates.size(); + int position = vh.getAdapterPosition(); + + if (position == RecyclerView.NO_POSITION) { + Log.d(LOG_TAG, "Unable to Set Item State"); + return; + } + + switch (mChoiceMode) { + case AbsListView.CHOICE_MODE_NONE: + break; + case AbsListView.CHOICE_MODE_SINGLE: { + boolean checked = mCheckStates.get(position, false); + if (!checked) { + for (int i = 0; i < checkedItemCount; i++) { + mAdapter.notifyItemChanged(mCheckStates.keyAt(i)); + } + mCheckStates.clear(); + mCheckStates.put(position, true); + mCheckedIdStates.clear(); + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } + // We directly call onBindViewHolder here because notifying that an item has + // changed on an item that has the focus causes it to lose focus, which makes + // keyboard navigation a bit annoying + mAdapter.onBindViewHolder(vh, position); + break; + } + case AbsListView.CHOICE_MODE_MULTIPLE: { + boolean checked = mCheckStates.get(position, false); + mCheckStates.put(position, !checked); + // We directly call onBindViewHolder here because notifying that an item has + // changed on an item that has the focus causes it to lose focus, which makes + // keyboard navigation a bit annoying + mAdapter.onBindViewHolder(vh, position); + break; + } + case AbsListView.CHOICE_MODE_MULTIPLE_MODAL: { + throw new RuntimeException("Multiple Modal not implemented in ItemChoiceManager."); + } + } + } + + /** + * Defines the choice behavior for the RecyclerView. By default, RecyclerViewChoiceMode does + * not have any choice behavior (AbsListView.CHOICE_MODE_NONE). By setting the choiceMode to + * AbsListView.CHOICE_MODE_SINGLE, the RecyclerView allows up to one item to be in a + * chosen state. + * + * @param choiceMode One of AbsListView.CHOICE_MODE_NONE, AbsListView.CHOICE_MODE_SINGLE + */ + public void setChoiceMode(int choiceMode) { + if (mChoiceMode != choiceMode) { + mChoiceMode = choiceMode; + clearSelections(); + } + } + + /** + * Returns the checked state of the specified position. The result is only + * valid if the choice mode has been set to AbsListView.CHOICE_MODE_SINGLE, + * but the code does not check this. + * + * @param position The item whose checked state to return + * @return The item's checked state + * @see #setChoiceMode(int) + */ + public boolean isItemChecked(int position) { + return mCheckStates.get(position); + } + + void clearSelections() { + mCheckStates.clear(); + mCheckedIdStates.clear(); + } + + void confirmCheckedPositionsById(int oldItemCount) { + // Clear out the positional check states, we'll rebuild it below from IDs. + mCheckStates.clear(); + + for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { + final long id = mCheckedIdStates.keyAt(checkedIndex); + final int lastPos = mCheckedIdStates.valueAt(checkedIndex); + + final long lastPosId = mAdapter.getItemId(lastPos); + if (id != lastPosId) { + // Look around to see if the ID is nearby. If not, uncheck it. + final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); + final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, oldItemCount); + boolean found = false; + for (int searchPos = start; searchPos < end; searchPos++) { + final long searchId = mAdapter.getItemId(searchPos); + if (id == searchId) { + found = true; + mCheckStates.put(searchPos, true); + mCheckedIdStates.setValueAt(checkedIndex, searchPos); + break; + } + } + + if (!found) { + mCheckedIdStates.delete(id); + checkedIndex--; + } + } else { + mCheckStates.put(lastPos, true); + } + } + } + + public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) { + boolean checked = isItemChecked(position); + if (vh.itemView instanceof Checkable) { + ((Checkable) vh.itemView).setChecked(checked); + } + ViewCompat.setActivated(vh.itemView, checked); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + byte[] states = savedInstanceState.getByteArray(SELECTED_ITEMS_KEY); + if ( null != states ) { + Parcel inParcel = Parcel.obtain(); + inParcel.unmarshall(states, 0, states.length); + inParcel.setDataPosition(0); + mCheckStates = inParcel.readSparseBooleanArray(); + final int numStates = inParcel.readInt(); + mCheckedIdStates.clear(); + for (int i=0; i + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-port/activity_main.xml b/app/src/main/res/layout-sw600dp-port/activity_main.xml index bf094d1..5ed510d 100644 --- a/app/src/main/res/layout-sw600dp-port/activity_main.xml +++ b/app/src/main/res/layout-sw600dp-port/activity_main.xml @@ -29,7 +29,8 @@ android:elevation="@dimen/appbar_elevation" android:layout_alignParentTop="true" app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + /> + android:contentDescription="@string/app_name" + /> + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_forecast_today.xml b/app/src/main/res/layout/list_item_forecast_today.xml index 180e716..1a3af29 100644 --- a/app/src/main/res/layout/list_item_forecast_today.xml +++ b/app/src/main/res/layout/list_item_forecast_today.xml @@ -17,72 +17,13 @@ - - - - - - - - - - - - - + /> \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/styles.xml b/app/src/main/res/values-sw600dp/styles.xml index e127770..26e7a12 100644 --- a/app/src/main/res/values-sw600dp/styles.xml +++ b/app/src/main/res/values-sw600dp/styles.xml @@ -15,7 +15,7 @@ --> - diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 9332d8a..85563af 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -18,4 +18,8 @@ + + + +