Media detail page redone as a "slide-up" panel.

Loads and displays default or English description, and categories.
No caching of this info yet.

Scrollable pane is a ListView, with the title/desc/category label
in a 'header' view along with a spacer view. The height of the spacer
is set dynamically to the height of the total fragment minus 48dp,
giving room for an initially-visible title section and a little
spillover so you can see it's scrollable.

Clicking on a category in the cats list opens the category page in
an external web browser. In the future this should open the category
within the app, but we don't have a per-cat view yet.

Description and category list are not yet editable.

GitHub: https://github.com/wikimedia/apps-android-commons/pull/41
Change-Id: I46d0a77481dbe64a268a72f3efe49ae72168541f
This commit is contained in:
Brion Vibber 2013-06-25 13:41:29 -07:00 committed by YuviPanda
parent a1d435f86e
commit 4df8ec8fa9
13 changed files with 586 additions and 30 deletions

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#00000000"
android:endColor="#ff000000"
android:angle="270"
>
</gradient>
</shape>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:padding="8dp"
android:gravity="center_vertical"
android:id="@+id/mediaDetailCategoryItemText"
android:textSize="18sp"
android:background="#AA000000"
/>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- Placeholder. Height gets set at runtime based on container size; the initial value is a hack to keep
the detail info offscreen until it's placed properly. May be a better way to do this. -->
<org.wikimedia.commons.media.MediaDetailSpacer
android:layout_width="fill_parent"
android:layout_height="1600dp"
android:id="@+id/mediaDetailSpacer"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#AA000000"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title of the media"
android:id="@+id/mediaDetailTitle"
android:layout_gravity="left|start"
android:textColor="@android:color/white"
android:textSize="18sp" /> <!-- 18sp == MediumText -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Description of the media goes here. This can potentially be fairly long, and will need to wrap across multiple lines. We hope it looks nice though."
android:id="@+id/mediaDetailDesc"
android:layout_gravity="left|start"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/detail_panel_cats_label"
android:textSize="18sp"
android:layout_gravity="left|start"
android:paddingTop="24dp" android:textColor="@android:color/white"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/mediaDetailCategoryList"
android:layout_gravity="left|start"/>
</LinearLayout>
</LinearLayout>

View file

@ -28,31 +28,14 @@
android:scaleType="fitCenter"
/>
<RelativeLayout
<ListView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:background="#AA000000"
android:padding="8dp"
>
<EditText
android:id="@+id/mediaDetailTitle"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:imeOptions="flagNoExtractUi"
android:inputType="textNoSuggestions"
android:singleLine="true"
android:textColor="#FFFFFF"/>
<!-- <TextView
android:id="@+id/mediaDetailDescription"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/mediaDetailTitle"
android:layout_alignParentBottom="true"
style="?android:textAppearanceSmall"
android:textColor="#FFFFFFFF"
/> -->
</RelativeLayout>
android:id="@+id/mediaDetailListView"
android:divider="#00A0A0A0"
android:fillViewport="true"
android:background="@android:color/transparent"
android:cacheColorHint="@android:color/transparent"
/>
</FrameLayout>

View file

@ -89,4 +89,7 @@
<string name="welcome_final_text">Message asking user if they understand what kinds of images to upload.</string>
<string name="welcome_final_button_text">Button text for confirming the user understands what kinds of images to upload.
{{Identical|Yes}}</string>
<string name="detail_panel_cats_label">Label for categories list in media detail panel</string>
<string name="detail_panel_cats_loading">Placeholder for categories list in media detail panel, while loading from network.</string>
<string name="detail_panel_cats_none">Placeholder for categories list in media detail panel, if no categories found.</string>
</resources>

View file

@ -102,4 +102,7 @@
<string name="welcome_copyright_subtext">Avoid copyrighted materials you found from the Internet as well as images of posters, book covers, etc.</string>
<string name="welcome_final_text">You think you got it?</string>
<string name="welcome_final_button_text">Yes!</string>
<string name="detail_panel_cats_label">Categories</string>
<string name="detail_panel_cats_loading">Loading...</string>
<string name="detail_panel_cats_none">None selected</string>
</resources>

View file

@ -210,5 +210,4 @@ public class CommonsApplication extends Application {
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
}

View file

@ -2,6 +2,7 @@ package org.wikimedia.commons;
import android.net.Uri;
import android.os.*;
import android.util.Log;
import java.util.*;
import java.util.regex.*;
@ -19,6 +20,8 @@ public class Media implements Parcelable {
};
protected Media() {
this.categories = new ArrayList<String>();
this.descriptions = new HashMap<String, String>();
}
private HashMap<String, Object> tags = new HashMap<String, Object>();
@ -127,10 +130,11 @@ public class Media implements Parcelable {
this.license = license;
}
// Primary metadata fields
protected Uri localUri;
protected String imageUrl;
protected String filename;
protected String description;
protected String description; // monolingual description on input...
protected long dataLength;
protected Date dateCreated;
protected Date dateUploaded;
@ -141,8 +145,45 @@ public class Media implements Parcelable {
protected String creator;
protected ArrayList<String> categories; // as loaded at runtime?
protected Map<String, String> descriptions; // multilingual descriptions as loaded
public ArrayList<String> getCategories() {
return (ArrayList<String>)categories.clone(); // feels dirty
}
public void setCategories(List<String> categories) {
this.categories.removeAll(this.categories);
this.categories.addAll(categories);
}
public void setDescriptions(Map<String,String> descriptions) {
for (String key : this.descriptions.keySet()) {
this.descriptions.remove(key);
}
for (String key : descriptions.keySet()) {
this.descriptions.put(key, descriptions.get(key));
}
}
public String getDescription(String preferredLanguage) {
if (descriptions.containsKey(preferredLanguage)) {
// See if the requested language is there.
return descriptions.get(preferredLanguage);
} else if (descriptions.containsKey("en")) {
// Ah, English. Language of the world, until the Chinese crush us.
return descriptions.get("en");
} else if (descriptions.containsKey("default")) {
// No languages marked...
return descriptions.get("default");
} else {
// FIXME: return the first available non-English description?
return "";
}
}
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.imageUrl = imageUrl;
this.filename = filename;
@ -170,6 +211,8 @@ public class Media implements Parcelable {
parcel.writeInt(width);
parcel.writeInt(height);
parcel.writeString(license);
parcel.writeStringList(categories);
parcel.writeMap(descriptions);
}
public Media(Parcel in) {
@ -185,6 +228,8 @@ public class Media implements Parcelable {
width = in.readInt();
height = in.readInt();
license = in.readString();
in.readStringList(categories);
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
}
public void setDescription(String description) {

View file

@ -0,0 +1,252 @@
package org.wikimedia.commons;
import android.util.Log;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Fetch additional media data from the network that we don't store locally.
*
* This includes things like category lists and multilingual descriptions,
* which are not intrinsic to the media and may change due to editing.
*/
public class MediaDataExtractor {
private boolean fetched;
private boolean processed;
private String filename;
private ArrayList<String> categories;
private Map<String, String> descriptions;
private String author;
private Date date;
/**
* @param filename of the target media object, should include 'File:' prefix
*/
public MediaDataExtractor(String filename) {
this.filename = filename;
categories = new ArrayList<String>();
descriptions = new HashMap<String, String>();
fetched = false;
processed = false;
}
/**
* Actually fetch the data over the network.
* todo: use local caching?
*
* Warning: synchronous i/o, call on a background thread
*/
public void fetch() throws IOException {
if (fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
}
MWApi api = CommonsApplication.createMWApi();
ApiResult result = api.action("query")
.param("prop", "revisions")
.param("titles", filename)
.param("rvprop", "content")
.param("rvlimit", 1)
.param("rvgeneratexml", 1)
.get();
processResult(result);
fetched = true;
}
private void processResult(ApiResult result) throws IOException {
String wikiSource = result.getString("/api/query/pages/page/revisions/rev");
String parseTreeXmlSource = result.getString("/api/query/pages/page/revisions/rev/@parsetree");
// In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(wikiSource);
// Description template info is extracted from preprocessor XML
processWikiParseTree(parseTreeXmlSource);
}
/**
* We could fetch all category links from API, but we actually only want the ones
* directly in the page source so they're editable. In the future this may change.
*
* @param source wikitext source code
*/
private void extractCategories(String source) {
Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE);
Matcher matcher = regex.matcher(source);
while (matcher.find()) {
String cat = matcher.group(1).trim();
categories.add(cat);
}
}
private void processWikiParseTree(String source) throws IOException {
Document doc;
try {
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
doc = docBuilder.parse(new ByteArrayInputStream(source.getBytes("UTF-8")));
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (IllegalStateException e) {
throw new IOException(e);
} catch (SAXException e) {
throw new IOException(e);
}
Node templateNode = findTemplate(doc.getDocumentElement(), "information");
if (templateNode != null) {
Node descriptionNode = findTemplateParameter(templateNode, "description");
descriptions = getMultilingualText(descriptionNode);
Node authorNode = findTemplateParameter(templateNode, "author");
author = Utils.getStringFromDOM(authorNode);
}
}
private Node findTemplate(Element parentNode, String title) throws IOException {
String ucTitle= Utils.capitalize(title);
NodeList nodes = parentNode.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("template")) {
String foundTitle = getTemplateTitle(node);
if (Utils.capitalize(foundTitle).equals(ucTitle)) {
return node;
}
}
}
return null;
}
private String getTemplateTitle(Node templateNode) throws IOException {
NodeList nodes = templateNode.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("title")) {
return node.getTextContent().trim();
}
}
throw new IOException("Template has no title element.");
}
private static abstract class TemplateChildNodeComparator {
abstract public boolean match(Node node);
}
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
final String theName = name;
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
@Override
public boolean match(Node node) {
return (Utils.capitalize(node.getTextContent().trim()).equals(Utils.capitalize(theName)));
}
});
}
private Node findTemplateParameter(Node templateNode, int index) throws IOException {
final String theIndex = "" + index;
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
@Override
public boolean match(Node node) {
Element el = (Element)node;
if (el.getTextContent().trim().equals(theIndex)) {
return true;
} else if (el.getAttribute("index") != null && el.getAttribute("index").trim().equals(theIndex)) {
return true;
} else {
return false;
}
}
});
}
private Node findTemplateParameter(Node templateNode, TemplateChildNodeComparator comparator) throws IOException {
NodeList nodes = templateNode.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("part")) {
NodeList childNodes = node.getChildNodes();
for (int j = 0; j < childNodes.getLength(); j++) {
Node childNode = childNodes.item(j);
if (childNode.getNodeName().equals("name")) {
if (comparator.match(childNode)) {
// yay! Now fetch the value node.
for (int k = j + 1; k < childNodes.getLength(); k++) {
Node siblingNode = childNodes.item(k);
if (siblingNode.getNodeName().equals("value")) {
return siblingNode;
}
}
throw new IOException("No value node found for matched template parameter.");
}
}
}
}
}
throw new IOException("No matching template parameter node found.");
}
// Extract a dictionary of multilingual texts from a subset of the parse tree.
// Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}.
// Text outside those wrappers is stuffed into a 'default' faux language key if present.
private Map<String, String> getMultilingualText(Node parentNode) throws IOException {
Map<String, String> texts = new HashMap<String, String>();
StringBuilder localText = new StringBuilder();
NodeList nodes = parentNode.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("template")) {
// process a template node
String title = getTemplateTitle(node);
if (title.length() < 3) {
// Hopefully a language code. Nasty hack!
String lang = title;
Node valueNode = findTemplateParameter(node, 1);
String value = Utils.getStringFromDOM(valueNode); // hope there's no subtemplates or formatting for now
texts.put(lang, value);
}
} else if (node.getNodeType() == Node.TEXT_NODE) {
localText.append(node.getTextContent());
}
}
// Some descriptions don't list multilingual variants
String defaultText = localText.toString().trim();
if (defaultText.length() > 0) {
texts.put("default", localText.toString());
}
return texts;
}
/**
* Take our metadata and inject it into a live Media object.
* Media object might contain stale or cached data, or emptiness.
* @param media
*/
public void fill(Media media) {
if (!fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch().");
}
media.setCategories(categories);
media.setDescriptions(descriptions);
// add author, date, etc fields
}
}

View file

@ -1,5 +1,6 @@
package org.wikimedia.commons;
import android.net.Uri;
import android.os.*;
import com.nostra13.universalimageloader.core.*;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
@ -174,4 +175,23 @@ public class Utils {
throw new RuntimeException("Unrecognized license value");
}
public static String implode(String glue, Iterable<String> pieces) {
StringBuffer buffer = new StringBuffer();
boolean first = true;
for (String piece : pieces) {
if (first) {
first = false;
} else {
buffer.append(glue);
}
buffer.append(pieces);
}
return buffer.toString();
}
public static Uri uriForWikiPage(String name) {
String underscored = name.trim().replace(" ", "_");
String uriStr = CommonsApplication.HOME_URL + urlEncode(underscored);
return Uri.parse(uriStr);
}
}

View file

@ -204,6 +204,7 @@ public class Contribution extends Media {
}
public Contribution() {
super();
timestamp = new Date(System.currentTimeMillis());
}

View file

@ -1,9 +1,12 @@
package org.wikimedia.commons.media;
import android.content.Intent;
import android.graphics.*;
import android.net.Uri;
import android.os.*;
import android.text.*;
import android.util.Log;
import android.util.TypedValue;
import android.view.*;
import android.widget.*;
import com.actionbarsherlock.app.SherlockFragment;
@ -14,8 +17,13 @@ import com.nostra13.universalimageloader.core.assist.ImageLoadingListener;
import com.android.volley.toolbox.*;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.wikimedia.commons.*;
import java.io.IOException;
import java.util.ArrayList;
public class MediaDetailFragment extends SherlockFragment {
private boolean editable;
@ -33,6 +41,8 @@ public class MediaDetailFragment extends SherlockFragment {
Bundle state = new Bundle();
state.putBoolean("editable", editable);
state.putInt("index", index);
state.putInt("listIndex", 0);
state.putInt("listTop", 0);
mf.setArguments(state);
@ -40,9 +50,22 @@ public class MediaDetailFragment extends SherlockFragment {
}
private ImageView image;
private EditText title;
//private EditText title;
private ProgressBar loadingProgress;
private ImageView loadingFailed;
private MediaDetailSpacer spacer;
private int initialListIndex = 0;
private int initialListTop = 0;
private TextView title;
private TextView desc;
private ListView listView;
private ArrayList<String> categoryNames;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private ArrayAdapter categoryAdapter;
private ViewTreeObserver.OnGlobalLayoutListener observer; // for layout stuff, only used once!
private AsyncTask<Void,Void,Boolean> detailFetchTask;
@Override
@ -50,6 +73,20 @@ public class MediaDetailFragment extends SherlockFragment {
super.onSaveInstanceState(outState);
outState.putInt("index", index);
outState.putBoolean("editable", editable);
getScrollPosition();
outState.putInt("listIndex", initialListIndex);
outState.putInt("listTop", initialListTop);
}
private void getScrollPosition() {
int initialListIndex = listView.getFirstVisiblePosition();
View firstVisibleItem = listView.getChildAt(initialListIndex);
if (firstVisibleItem == null) {
initialListTop = 0;
} else {
initialListTop = firstVisibleItem.getTop();
}
}
@Override
@ -59,19 +96,46 @@ public class MediaDetailFragment extends SherlockFragment {
if(savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
index = savedInstanceState.getInt("index");
initialListIndex = savedInstanceState.getInt("listIndex");
initialListTop = savedInstanceState.getInt("listTop");
} else {
editable = getArguments().getBoolean("editable");
index = getArguments().getInt("index");
}
final Media media = detailProvider.getMediaAtPosition(index);
categoryNames = new ArrayList<String>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
image = (ImageView) view.findViewById(R.id.mediaDetailImage);
title = (EditText) view.findViewById(R.id.mediaDetailTitle);
loadingProgress = (ProgressBar) view.findViewById(R.id.mediaDetailImageLoading);
loadingFailed = (ImageView) view.findViewById(R.id.mediaDetailImageFailed);
listView = (ListView) view.findViewById(R.id.mediaDetailListView);
// Detail consists of a list view with main pane in header view, plus category list.
View detailView = getActivity().getLayoutInflater().inflate(R.layout.detail_main_panel, null, false);
listView.addHeaderView(detailView, null, false);
categoryAdapter = new ArrayAdapter(getActivity(), R.layout.detail_category_item, categoryNames);
listView.setAdapter(categoryAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
if (categoriesLoaded && categoriesPresent) {
String selectedCategoryTitle = "Category:" + categoryNames.get(position - 1);
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(Utils.uriForWikiPage(selectedCategoryTitle));
startActivity(viewIntent);
}
}
});
spacer = (MediaDetailSpacer) detailView.findViewById(R.id.mediaDetailSpacer);
title = (TextView) detailView.findViewById(R.id.mediaDetailTitle);
desc = (TextView) detailView.findViewById(R.id.mediaDetailDesc);
// Enable or disable editing on the title
/*
title.setClickable(editable);
title.setFocusable(editable);
title.setCursorVisible(editable);
@ -79,6 +143,8 @@ public class MediaDetailFragment extends SherlockFragment {
if(!editable) {
title.setBackgroundDrawable(null);
}
*/
String actualUrl = TextUtils.isEmpty(media.getImageUrl()) ? media.getLocalUri().toString() : media.getThumbnailUrl(640);
if(actualUrl.startsWith("http")) {
@ -88,6 +154,56 @@ public class MediaDetailFragment extends SherlockFragment {
mwImage.setMedia(media, loader);
Log.d("Volley", actualUrl);
// FIXME: For transparent images
// Load image metadata: desc, license, categories
// FIXME: keep the spinner going while we load data
// FIXME: cache this data
detailFetchTask = new AsyncTask<Void, Void, Boolean>() {
private MediaDataExtractor extractor;
@Override
protected void onPreExecute() {
extractor = new MediaDataExtractor(media.getFilename());
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
extractor.fetch();
return Boolean.TRUE;
} catch (IOException e) {
e.printStackTrace();
}
return Boolean.FALSE;
}
@Override
protected void onPostExecute(Boolean success) {
detailFetchTask = null;
if (success.booleanValue()) {
extractor.fill(media);
// Fill some fields
desc.setText(media.getDescription("en"));
categoryNames.removeAll(categoryNames);
categoryNames.addAll(media.getCategories());
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
categoryAdapter.notifyDataSetChanged();
} else {
Log.d("Commons", "Failed to load photo details.");
}
}
};
Utils.executeAsyncTask(detailFetchTask);
} else {
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() {
public void onLoadingStarted(String s, View view) {
@ -113,8 +229,11 @@ public class MediaDetailFragment extends SherlockFragment {
}
});
}
title.setText(media.getDisplayTitle());
title.setText(media.getDisplayTitle());
desc.setText("");
/*
title.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
@ -130,6 +249,35 @@ public class MediaDetailFragment extends SherlockFragment {
}
});
*/
// Layout observer to size the spacer item relative to the available space.
// There may be a .... better way to do this.
observer = new ViewTreeObserver.OnGlobalLayoutListener() {
private int currentHeight = -1;
public void onGlobalLayout() {
int viewHeight = view.getHeight();
//int textHeight = title.getLineHeight();
int paddingDp = 48;
float paddingPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingDp, getResources().getDisplayMetrics());
int newHeight = viewHeight - Math.round(paddingPx);
if (newHeight != currentHeight) {
currentHeight = newHeight;
ViewGroup.LayoutParams params = spacer.getLayoutParams();
params.height = newHeight;
spacer.setLayoutParams(params);
// hack hack to trigger relayout
categoryAdapter.notifyDataSetChanged();
listView.setSelectionFromTop(initialListIndex, initialListTop);
}
}
};
view.getViewTreeObserver().addOnGlobalLayoutListener(observer);
return view;
}
@ -139,4 +287,17 @@ public class MediaDetailFragment extends SherlockFragment {
displayOptions = Utils.getGenericDisplayOptions().build();
}
@Override
public void onDestroyView() {
if (detailFetchTask != null) {
detailFetchTask.cancel(true);
detailFetchTask = null;
}
if (observer != null) {
getView().getViewTreeObserver().removeGlobalOnLayoutListener(observer); // old Android was on crack. CRACK IS WHACK
observer = null;
}
super.onDestroyView();
}
}

View file

@ -0,0 +1,19 @@
package org.wikimedia.commons.media;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class MediaDetailSpacer extends View {
public MediaDetailSpacer(Context context) {
super(context);
}
public MediaDetailSpacer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MediaDetailSpacer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}