From 445646c52128a763b56ed7bb3bd009e2f33e3e4f Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Fri, 25 Jun 2010 15:52:59 -0700 Subject: [PATCH] Improvements to ListFragment. Now deals correctly with a content view containing just a list, and adds a lot more built-in functionality: ability to show custom text for an empty list, and indeterminant progress while populating the list. In addition, reworks transaction committing to be more aggressive about committing the transactions as the containing activity moves between its states (rather than waiting for the activity's handler to process the transaction message whenever that may finally happen). And fixed a bug with saving/restoring state of transaction replace operations. Change-Id: I9617a0c4f248b50a61b319910323639b6de24f73 --- api/current.xml | 34 +++++- core/java/android/app/Activity.java | 4 + core/java/android/app/BackStackEntry.java | 37 +++++- core/java/android/app/FragmentManager.java | 63 +++++++++- core/java/android/app/ListFragment.java | 108 ++++++++++++++++-- .../android/app/LoaderManagingFragment.java | 2 +- core/res/res/layout/list_content_rich.xml | 56 +++++++++ core/res/res/values/strings.xml | 3 + 8 files changed, 288 insertions(+), 19 deletions(-) create mode 100644 core/res/res/layout/list_content_rich.xml diff --git a/api/current.xml b/api/current.xml index 50546d3629a0f..dc4ba2c9bc5da 100644 --- a/api/current.xml +++ b/api/current.xml @@ -28030,6 +28030,19 @@ + + + + + + + + + + @@ -223896,7 +223924,7 @@ deprecated="not deprecated" visibility="public" > - + @@ -247337,7 +247365,7 @@ 0) { + op.removed = new ArrayList(N); + for (int i=0; i mPendingActions; + Runnable[] mTmpActions; + boolean mExecutingActions; + ArrayList mActive; ArrayList mAdded; ArrayList mAvailIndices; @@ -91,6 +95,13 @@ public class FragmentManager { Bundle mStateBundle = null; SparseArray mStateArray = null; + Runnable mExecCommit = new Runnable() { + @Override + public void run() { + execPendingActions(); + } + }; + Animation loadAnimation(Fragment fragment, int transit, boolean enter, int transitionStyle) { Animation animObj = fragment.onCreateAnimation(transitionStyle, enter, @@ -486,6 +497,52 @@ public class FragmentManager { return null; } + public void enqueueAction(Runnable action) { + synchronized (this) { + if (mPendingActions == null) { + mPendingActions = new ArrayList(); + } + mPendingActions.add(action); + if (mPendingActions.size() == 1) { + mActivity.mHandler.removeCallbacks(mExecCommit); + mActivity.mHandler.post(mExecCommit); + } + } + } + + /** + * Only call from main thread! + */ + public void execPendingActions() { + if (mExecutingActions) { + throw new IllegalStateException("Recursive entry to execPendingActions"); + } + + while (true) { + int numActions; + + synchronized (this) { + if (mPendingActions == null || mPendingActions.size() == 0) { + return; + } + + numActions = mPendingActions.size(); + if (mTmpActions == null || mTmpActions.length < numActions) { + mTmpActions = new Runnable[numActions]; + } + mPendingActions.toArray(mTmpActions); + mPendingActions.clear(); + mActivity.mHandler.removeCallbacks(mExecCommit); + } + + mExecutingActions = true; + for (int i=0; i(); @@ -503,8 +560,9 @@ public class FragmentManager { return false; } final BackStackEntry bss = mBackStack.remove(last); - handler.post(new Runnable() { + enqueueAction(new Runnable() { public void run() { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + bss); bss.popFromBackStack(); moveToState(mCurState, reverseTransit(bss.getTransition()), bss.getTransitionStyle(), true); @@ -526,9 +584,10 @@ public class FragmentManager { for (int i=mBackStack.size()-1; i>index; i--) { states.add(mBackStack.remove(i)); } - handler.post(new Runnable() { + enqueueAction(new Runnable() { public void run() { for (int i=0; imust have a ListView whose id + * is {@link android.R.id.list android.R.id.list} and can optionally + * have a sibling view id {@link android.R.id.empty android.R.id.empty} + * that is to be shown when the list is empty. */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(com.android.internal.R.layout.list_content, + return inflater.inflate(com.android.internal.R.layout.list_content_rich, container, false); } @@ -246,6 +259,62 @@ public class ListFragment extends Fragment { return mList; } + /** + * The default content for a ListFragment has a TextView that can + * be shown when the list is empty. If you would like to have it + * shown, call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (!mSetEmptyView) { + mSetEmptyView = true; + mList.setEmptyView(mStandardEmptyView); + } + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + * @param animate If true, an animation will be used to transition to the + * new state. + */ + public void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.GONE); + } + } + /** * Get the ListAdapter associated with this activity's ListView. */ @@ -261,16 +330,33 @@ public class ListFragment extends Fragment { if (root == null) { throw new IllegalStateException("Content view not yet created"); } - View emptyView = root.findViewById(com.android.internal.R.id.empty); - mList = (ListView)root.findViewById(com.android.internal.R.id.list); - if (mList == null) { - throw new RuntimeException( - "Your content must have a ListView whose id attribute is " + - "'android.R.id.list'"); - } - if (emptyView != null) { - mList.setEmptyView(emptyView); + if (root instanceof ListView) { + mList = (ListView)root; + } else { + mStandardEmptyView = (TextView)root.findViewById( + com.android.internal.R.id.internalEmpty); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } + mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer); + mListContainer = root.findViewById(com.android.internal.R.id.listContainer); + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mList == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + if (mEmptyView != null) { + mList.setEmptyView(mEmptyView); + } } + mListShown = true; mList.setOnItemClickListener(mOnClickListener); if (mAdapter != null) { setListAdapter(mAdapter); diff --git a/core/java/android/app/LoaderManagingFragment.java b/core/java/android/app/LoaderManagingFragment.java index a8a5ca4674106..5d417a0ece944 100644 --- a/core/java/android/app/LoaderManagingFragment.java +++ b/core/java/android/app/LoaderManagingFragment.java @@ -44,7 +44,7 @@ public abstract class LoaderManagingFragment extends Fragment * when the new loader completes it's work. The callback will be delivered before the old loader * is destroyed. */ - protected Loader startLoading(int id, Bundle args) { + public Loader startLoading(int id, Bundle args) { LoaderInfo info = mLoaders.get(id); if (info != null) { // Keep track of the previous instance of this loader so we can destroy diff --git a/core/res/res/layout/list_content_rich.xml b/core/res/res/layout/list_content_rich.xml new file mode 100644 index 0000000000000..14140322b7cd4 --- /dev/null +++ b/core/res/res/layout/list_content_rich.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 6b371df717a64..de1ea68a71e59 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1919,6 +1919,9 @@ combined with setIcon(android.R.drawable.ic_dialog_alert) --> Attention + + Loading... + ON