blob: 47d2a171f5f33bee2a251517b47942f22b61eb5e [file] [log] [blame]
/*
* 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;
}
}
}