/* | |
* Copyright (C) 2012 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 #package_name#; | |
import java.util.ArrayList; | |
import java.util.Calendar; | |
import java.util.List; | |
import android.app.Activity; | |
import android.app.LauncherActivity.ListItem; | |
import android.app.ListActivity; | |
import android.os.AsyncTask; | |
import android.os.Bundle; | |
import android.text.format.DateFormat; | |
import android.view.LayoutInflater; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import android.view.View.OnTouchListener; | |
import android.view.ViewGroup; | |
import android.widget.ArrayAdapter; | |
import android.widget.ImageView; | |
import android.widget.LinearLayout; | |
import android.widget.ListView; | |
import android.widget.ProgressBar; | |
import android.widget.TextView; | |
import android.widget.Toast; | |
import #ManifestPackageName#.R; | |
/** | |
* Class which holds an example of a endless list. It means a list will be | |
* displayed and it will always have items to be displayed. <br> | |
* New data is loaded asynchronously in order to provide a good user experience. | |
*/ | |
public class #class_name# extends ListActivity { | |
/** | |
* Adapter for endless list. | |
*/ | |
private EndlessListAdapter arrayAdapter = null; | |
/** | |
* The list header (Where is the loading and last updated labels) | |
*/ | |
private LinearLayout listHeader = null; | |
/** | |
* Last loaded item | |
*/ | |
private TextView lastUpdated = null; | |
/** | |
* Loading progress | |
*/ | |
private ProgressBar loadingProgress = null; | |
/** | |
* Just an integer to add items sequentially | |
*/ | |
private int lastAdded = 0; | |
/** | |
* Variable which controls when new items are being loaded. If this variable | |
* is true, it means items are being loaded, otherwise it is set to false. | |
*/ | |
private boolean isLoading = false; | |
/** | |
* The number of elements which are retrieved every time the service is | |
* called for retrieving elements. | |
*/ | |
private static final int BLOCK_SIZE = 2; | |
/** | |
* Property to save the number of items already loaded | |
*/ | |
private static final String PROP_ITEM_COUNT = "item_count"; | |
/** | |
* Property to save the top most index of the list | |
*/ | |
private static final String PROP_TOP_ITEM = "top_list_item"; | |
/** | |
* Property to save the time of the last update | |
*/ | |
private static final String PROP_LAST_UPDATED = "last_updated"; | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
arrayAdapter = new EndlessListAdapter(this, R.layout.#layout_name#endless_list_pull_to_refresh/listrow.xml#, | |
new ArrayList<ListElement>()); | |
listHeader = (LinearLayout) getLayoutInflater().inflate( | |
R.layout.#layout_name#endless_list_pull_to_refresh/listheader.xml#, null); | |
getListView().addHeaderView(listHeader); | |
lastUpdated = (TextView) listHeader.findViewById(R.id.lastUpdated); | |
loadingProgress = (ProgressBar) listHeader | |
.findViewById(R.id.progressBar); | |
loadingProgress.setVisibility(View.GONE); | |
lastUpdated.setText(R.string.last_updated); | |
lastUpdated.setText(lastUpdated.getText().toString() + DateFormat.format("EEEE, MMMM dd, yyyy", | |
Calendar.getInstance())); | |
if (savedInstanceState == null | |
|| (savedInstanceState != null && savedInstanceState.getInt( | |
PROP_ITEM_COUNT, 0) == 0)) { | |
// download asynchronously initial list | |
Downloaditems downloadAction = new Downloaditems(); | |
downloadAction.execute(new Integer[] { BLOCK_SIZE }); | |
} | |
setListAdapter(arrayAdapter); | |
getListView().setOnTouchListener(new PullEventListener()); | |
} | |
@Override | |
protected void onSaveInstanceState(Bundle outState) { | |
/* | |
* Save instance loaded items to be restored after rotate the device OR | |
* after you have pressed home button | |
*/ | |
outState.putInt(PROP_ITEM_COUNT, arrayAdapter.getCount()); | |
for (int i = 0; i < arrayAdapter.getCount(); i++) { | |
outState.putSerializable(Integer.toString(i), | |
arrayAdapter.getItemAt(i)); | |
} | |
outState.putInt(PROP_TOP_ITEM, getListView().getFirstVisiblePosition()); | |
outState.putString(PROP_LAST_UPDATED, lastUpdated.getText().toString()); | |
super.onSaveInstanceState(outState); | |
} | |
@Override | |
protected void onRestoreInstanceState(Bundle state) { | |
/* | |
* Restore state. Also restore lastAdded value since this class is a new | |
* instance on restore | |
*/ | |
super.onRestoreInstanceState(state); | |
int count = state.getInt(PROP_ITEM_COUNT); | |
for (int i = 0; i < count; i++) { | |
arrayAdapter.add((ListElement) state.get(Integer.toString(i))); | |
} | |
lastAdded = count; | |
getListView().setSelection(state.getInt(PROP_TOP_ITEM)); | |
lastUpdated.setText(state.getString(PROP_LAST_UPDATED)); | |
} | |
@Override | |
protected void onListItemClick(ListView lv, View v, int position, long id) { | |
int listIndex = position - 1; | |
if (arrayAdapter.getItemAt(listIndex) != null) { | |
//your action here | |
Toast.makeText( | |
lv.getContext(), | |
getString(R.string.selected_element_message, | |
arrayAdapter.getItemAt(listIndex).text), | |
Toast.LENGTH_SHORT).show(); | |
} | |
} | |
/** | |
* This method represents a service which takes a long time to be executed. | |
* To simulate it, it is inserted a lag of 1 second. <br> | |
* This method basically creates a <i>cache</i> number of | |
* {@link ListElement} and returns it. It creates {@link ListElement}s with | |
* text higher than <i>itemNumber</i>. | |
* | |
* @param itemNumber | |
* Basic number to create other elements. | |
* @param numberOfItemsToBeCreated | |
* Number of items to be created. | |
* | |
* @return Returns the created list of {@link ListElement}s. | |
*/ | |
private List<ListElement> retrieveItems(int numberOfItemsToBeCreated) { | |
List<ListElement> results = new ArrayList<ListElement>(); | |
try { | |
// wait for 2 seconds in order to simulate the long service | |
Thread.sleep(2000); | |
// create items | |
for (int i = 0; i <= numberOfItemsToBeCreated; i++) { | |
String itemToBeAdded = getString(R.string.list_item_number, | |
(lastAdded++)); | |
results.add(new ListElement(itemToBeAdded, R.drawable.#drawable_name#endless_list_pull_to_refresh/listicon.png#)); | |
} | |
} catch (InterruptedException e) { | |
// treat exception here | |
} | |
return results; | |
} | |
/** | |
* Listener which handles the endless list. It is responsible for | |
* determining when the long service will be called asynchronously. | |
*/ | |
/** | |
* Asynchronous job call. This class is responsible for calling the long | |
* service and managing the <i>isLoading</i> flag. | |
*/ | |
class Downloaditems extends AsyncTask<Integer, Void, List<ListElement>> { | |
// indexes constants | |
private static final int NUMBER_OF_ITEMS_TO_BE_CREATED_INDEX = 0; | |
@Override | |
protected void onPreExecute() { | |
// flag loading is being executed | |
isLoading = true; | |
loadingProgress.setVisibility(View.VISIBLE); | |
lastUpdated.setText(R.string.loading_message); | |
} | |
@Override | |
protected List<ListElement> doInBackground(Integer... params) { | |
// execute the long service | |
return retrieveItems(params[NUMBER_OF_ITEMS_TO_BE_CREATED_INDEX]); | |
} | |
@Override | |
protected void onPostExecute(List<ListElement> result) { | |
arrayAdapter.setNotifyOnChange(true); | |
for (ListElement item : result) { | |
// it is necessary to verify whether the item was already added | |
// because this job is called many times asynchronously | |
synchronized (arrayAdapter) { | |
if (!arrayAdapter.contains(item)) { | |
// Add items always in the beginning | |
arrayAdapter.insert(item, 0); | |
} | |
} | |
} | |
loadingProgress.setVisibility(View.GONE); | |
lastUpdated.setText(getString(R.string.last_updated, | |
DateFormat.format("EEEE, MMMM dd, yyyy", | |
Calendar.getInstance()))); | |
// flag the loading is finished | |
isLoading = false; | |
} | |
} | |
/** | |
* Adapter which handles the list be be displayed. | |
*/ | |
class EndlessListAdapter extends ArrayAdapter<ListElement> { | |
private final Activity context; | |
private final List<ListElement> items; | |
private final int rowViewId; | |
/** | |
* Instantiate the Adapter for an Endless List Activity. | |
* | |
* @param context | |
* {@link Activity} which holds the endless list. | |
* @param rowviewId | |
* Identifier of the View which holds each row of the List. | |
* @param items | |
* Initial set of items which are added to list being | |
* displayed. | |
*/ | |
public EndlessListAdapter(Activity context, int rowviewId, | |
List<ListElement> items) { | |
super(context, rowviewId, items); | |
this.context = context; | |
this.items = items; | |
this.rowViewId = rowviewId; | |
} | |
/** | |
* Check whether a {@link ListItem} is already in this adapter. | |
* | |
* @param item | |
* Item to be verified whether it is in the adapter. | |
* | |
* @return Returns <code>true</code> in case the {@link ListElement} is | |
* in the adapter, <code>false</code> otherwise. | |
*/ | |
public boolean contains(ListElement item) { | |
return items.contains(item); | |
} | |
/** | |
* Get a {@link ListElement} at a certain position. | |
* | |
* @param index | |
* Position where the {@link ListElement} is retrieved. | |
* | |
* @return Returns the {@link ListElement} give a certain position. | |
*/ | |
public ListElement getItemAt(int index) { | |
return index < items.size() ? items.get(index) : null; | |
} | |
@Override | |
public View getView(int position, View convertView, ViewGroup parent) { | |
ImageView imageView; | |
TextView textView; | |
View rowView = convertView; | |
/* | |
* We inflate the row using the determined layout. Also, we fill the | |
* necessary data in the text and image views. | |
*/ | |
LayoutInflater inflater = context.getLayoutInflater(); | |
rowView = inflater.inflate(rowViewId, null, true); | |
textView = (TextView) rowView.findViewById(R.id.text01); | |
imageView = (ImageView) rowView.findViewById(R.id.img01); | |
textView.setText(items.get(position).text); | |
imageView.setImageResource(items.get(position).imageId); | |
return rowView; | |
} | |
} | |
class PullEventListener implements OnTouchListener { | |
private float firstEventY; | |
private CharSequence previousLastUpdatedText; | |
private boolean shouldConsiderRefresh = false; | |
@Override | |
public boolean onTouch(View v, MotionEvent event) { | |
switch (event.getAction()) { | |
// Save the first touch position | |
case MotionEvent.ACTION_DOWN: | |
shouldConsiderRefresh = getListView().getFirstVisiblePosition() == 0 | |
&& listHeader.getTop() == 0; | |
previousLastUpdatedText = lastUpdated.getText(); | |
if (shouldConsiderRefresh) { | |
firstEventY = event.getY(); | |
} | |
break; | |
// Check if we can refresh with certain delta to update texts | |
case MotionEvent.ACTION_MOVE: | |
if (shouldConsiderRefresh) { | |
float currentEventY = event.getY(); | |
if (currentEventY != firstEventY) { | |
lastUpdated.setText(R.string.pull_to_refresh); | |
} | |
if (shouldRefresh(firstEventY, currentEventY) && !isLoading) { | |
lastUpdated.setText(R.string.release_to_refresh); | |
} | |
} | |
break; | |
// Check if we can refresh with certain delta and go back to the | |
// original text because the touch event is finished | |
case MotionEvent.ACTION_UP: | |
lastUpdated.setText(previousLastUpdatedText); | |
if (shouldConsiderRefresh) { | |
float currentEventY = event.getY(); | |
if (shouldRefresh(firstEventY, currentEventY) && !isLoading) { | |
lastUpdated.setText(previousLastUpdatedText); | |
Downloaditems downloadAction = new Downloaditems(); | |
downloadAction.execute(new Integer[] { BLOCK_SIZE }); | |
event.setAction(MotionEvent.ACTION_CANCEL); | |
dispatchTouchEvent(event); | |
shouldConsiderRefresh = false; | |
return true; | |
} | |
} | |
break; | |
} | |
return false; | |
} | |
/** | |
* Check if the difference between first and current touch positions are | |
* enough to dispatch a refresh | |
* | |
* @param firstTapPosition | |
* @param currentPosition | |
* @return true if the difference is big enough to refresh, false otherwise | |
*/ | |
private boolean shouldRefresh(float firstTapPosition, | |
float currentPosition) { | |
int threshold = getListView().getHeight() / 4; | |
return ((currentPosition - firstTapPosition) / 2) > threshold; | |
} | |
} | |
} |