Fetch and use thumbnail across the app (#2906)

This commit is contained in:
Vivek Maskara 2019-04-23 22:33:09 +05:30 committed by Josephine Lim
parent 17d69cde02
commit 37e9eae314
18 changed files with 161 additions and 260 deletions

View file

@ -42,6 +42,7 @@ public class Media implements Parcelable {
// Primary metadata fields
protected Uri localUri;
private String thumbUrl;
protected String imageUrl;
protected String filename;
protected String description; // monolingual description on input...
@ -93,6 +94,7 @@ public class Media implements Parcelable {
long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.thumbUrl = imageUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
@ -107,6 +109,7 @@ public class Media implements Parcelable {
@SuppressWarnings("unchecked")
public Media(Parcel in) {
localUri = in.readParcelable(Uri.class.getClassLoader());
thumbUrl = in.readString();
imageUrl = in.readString();
filename = in.readString();
description = in.readString();
@ -124,6 +127,67 @@ public class Media implements Parcelable {
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
}
/**
* Creating Media object from MWQueryPage.
* Earlier only basic details were set for the media object but going forward,
* a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
*
* @param page response from the API
* @return Media object
*/
@Nullable
public static Media from(MwQueryPage page) {
ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) {
return null;
}
ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) {
Media media = new Media(null, imageInfo.getOriginalUrl(),
page.title(), "", 0, null, null, null);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl());
}
return media;
}
Media media = new Media(null,
imageInfo.getOriginalUrl(),
page.title(),
"",
0,
safeParseDate(metadata.dateTimeOriginal().value()),
safeParseDate(metadata.dateTime().value()),
StringUtil.fromHtml(metadata.artist().value()).toString()
);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl());
}
String language = Locale.getDefault().getLanguage();
if (StringUtils.isBlank(language)) {
language = "default";
}
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
String latitude = metadata.gpsLatitude().value();
String longitude = metadata.gpsLongitude().value();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
media.setCoordinates(latLng);
}
media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
return media;
}
public String getThumbUrl() {
return thumbUrl;
}
/**
* Gets tag of media
* @param key Media key
@ -322,53 +386,8 @@ public class Media implements Parcelable {
return license;
}
/**
* Creating Media object from MWQueryPage.
* Earlier only basic details were set for the media object but going forward,
* a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
*
* @param page response from the API
* @return Media object
*/
@Nullable
public static Media from(MwQueryPage page) {
ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) {
return null;
}
ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) {
return new Media(null, imageInfo.getOriginalUrl(),
page.title(), "", 0, null, null, null);
}
Media media = new Media(null,
imageInfo.getOriginalUrl(),
page.title(),
"",
0,
safeParseDate(metadata.dateTimeOriginal().value()),
safeParseDate(metadata.dateTime().value()),
StringUtil.fromHtml(metadata.artist().value()).toString()
);
String language = Locale.getDefault().getLanguage();
if (StringUtils.isBlank(language)) {
language = "default";
}
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
String latitude = metadata.gpsLatitude().value();
String longitude = metadata.gpsLongitude().value();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
media.setCoordinates(latLng);
}
media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
return media;
public void setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
}
public String getLicenseUrl() {
@ -482,6 +501,7 @@ public class Media implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeParcelable(localUri, flags);
parcel.writeString(thumbUrl);
parcel.writeString(imageUrl);
parcel.writeString(filename);
parcel.writeString(description);

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons;
import android.text.Html;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -36,7 +34,7 @@ public class MediaDataExtractor {
* @return full Media object with all details including deletion status and talk page
*/
public Single<Media> fetchMediaDetails(String filename) {
Single<Media> mediaSingle = okHttpJsonApiClient.getMedia(filename, false);
Single<Media> mediaSingle = getMediaFromFileName(filename);
Single<Boolean> pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
Single<String> discussionSingle = getDiscussion(filename);
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
@ -48,6 +46,15 @@ public class MediaDataExtractor {
});
}
/**
* Method can be used to fetch media for a given filename
* @param filename Eg. File:Test.jpg
* @return return data rich Media object
*/
public Single<Media> getMediaFromFileName(String filename) {
return okHttpJsonApiClient.getMedia(filename, false);
}
/**
* Fetch talk page from the MediaWiki API
* @param filename

View file

@ -1,123 +0,0 @@
package fr.free.nrw.commons;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
public class MediaWikiImageView extends SimpleDraweeView {
@Inject MediaWikiApi mwApi;
@Inject LruCache<String, String> thumbnailUrlCache;
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
public MediaWikiImageView(Context context) {
this(context, null);
init();
}
public MediaWikiImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* Sets the media. Fetches its thumbnail if necessary.
* @param media the new media
*/
public void setMedia(Media media) {
if (media == null) {
return;
}
Disposable disposable = fetchMediaThumbnail(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(thumbnail -> {
if (!StringUtils.isBlank(thumbnail)) {
setImageUrl(thumbnail);
}
}, throwable -> Timber.e(throwable, "Error occurred while fetching thumbnail"));
compositeDisposable.add(disposable);
}
@Override
protected void onDetachedFromWindow() {
compositeDisposable.clear();
super.onDetachedFromWindow();
}
/**
* Initializes MediaWikiImageView.
*/
private void init() {
ApplicationlessInjection
.getInstance(getContext()
.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_image_black_24dp, getContext().getTheme()))
.setFailureImage(VectorDrawableCompat.create(getResources(),
R.drawable.ic_image_black_24dp, getContext().getTheme()))
.build());
}
//TODO: refactor the logic for thumbnails. ImageInfo API can be used to fetch thumbnail upfront
/**
* Fetches media thumbnail from the server
*
* @param media
* @return
*/
public Single<String> fetchMediaThumbnail(Media media) {
if (media.getFilename() != null && thumbnailUrlCache.get(media.getFilename()) != null) {
return Single.just(thumbnailUrlCache.get(media.getFilename()));
}
return mwApi.findThumbnailByFilename(media.getFilename())
.map(result -> {
if (TextUtils.isEmpty(result) && media.getLocalUri() != null) {
return media.getLocalUri().toString();
} else {
thumbnailUrlCache.put(media.getFilename(), result);
return result;
}
});
}
/**
* Displays the image from the URL.
* @param url the URL of the image
*/
private void setImageUrl(@Nullable String url) {
setImageURI(url);
}
}

View file

@ -8,12 +8,13 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
/**
@ -82,12 +83,12 @@ public class GridViewAdapter extends ArrayAdapter {
}
Media item = data.get(position);
MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView);
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getDisplayTitle());
setAuthorView(item, author);
imageView.setMedia(item);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}

View file

@ -6,23 +6,37 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewHolder;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
class ContributionViewHolder implements ViewHolder<DisplayableContribution> {
@BindView(R.id.contributionImage) MediaWikiImageView imageView;
public class ContributionViewHolder implements ViewHolder<DisplayableContribution> {
@BindView(R.id.contributionImage)
SimpleDraweeView imageView;
@BindView(R.id.contributionTitle) TextView titleView;
@BindView(R.id.contributionState) TextView stateView;
@BindView(R.id.contributionSequenceNumber) TextView seqNumView;
@BindView(R.id.contributionProgress) ProgressBar progressView;
@BindView(R.id.failed_image_options) LinearLayout failedImageOptions;
@Inject
MediaDataExtractor mediaDataExtractor;
private DisplayableContribution contribution;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
ContributionViewHolder(View parent) {
ButterKnife.bind(this, parent);
@ -30,8 +44,10 @@ class ContributionViewHolder implements ViewHolder<DisplayableContribution> {
@Override
public void bindModel(Context context, DisplayableContribution contribution) {
ApplicationlessInjection.getInstance(context)
.getCommonsApplicationComponent().inject(this);
this.contribution = contribution;
imageView.setMedia(contribution);
fetchAndDisplayThumbnail(contribution);
titleView.setText(contribution.getDisplayTitle());
seqNumView.setText(String.valueOf(contribution.getPosition() + 1));
@ -71,6 +87,26 @@ class ContributionViewHolder implements ViewHolder<DisplayableContribution> {
}
}
/**
* This method fetches the thumbnail url from file name
* This can be removed once #2904 is in place and contribution contains all metadata beforehand
* @param contribution
*/
private void fetchAndDisplayThumbnail(DisplayableContribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
Disposable disposable = mediaDataExtractor.getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(media -> {
imageView.setImageURI(media.getThumbUrl());
});
compositeDisposable.add(disposable);
}
public void clear() {
compositeDisposable.clear();
}
/**
* Retry upload when it is failed
*/

View file

@ -50,6 +50,7 @@ class ContributionsListAdapter extends CursorAdapter {
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
final Contribution contribution = contributionDao.fromCursor(cursor);
Timber.d("Cursor position is %d", cursor.getPosition());
DisplayableContribution displayableContribution = new DisplayableContribution(contribution,
cursor.getPosition(),
new DisplayableContribution.ContributionActions() {

View file

@ -7,8 +7,8 @@ import dagger.android.AndroidInjectionModule;
import dagger.android.AndroidInjector;
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionViewHolder;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
import fr.free.nrw.commons.nearby.PlaceRenderer;
@ -36,8 +36,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(ModificationsSyncAdapter syncAdapter);
void inject(MediaWikiImageView mediaWikiImageView);
void inject(LoginActivity activity);
void inject(SettingsFragment fragment);
@ -53,6 +51,8 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(PicOfDayAppWidget picOfDayAppWidget);
void inject(ContributionViewHolder viewHolder);
@Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"})
interface Builder {

View file

@ -5,12 +5,12 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
/**
@ -19,8 +19,7 @@ import fr.free.nrw.commons.R;
class SearchImagesRenderer extends Renderer<Media> {
@BindView(R.id.categoryImageTitle) TextView tvImageName;
@BindView(R.id.categoryImageAuthor) TextView categoryImageAuthor;
@BindView(R.id.categoryImageView)
MediaWikiImageView browseImage;
@BindView(R.id.categoryImageView) SimpleDraweeView browseImage;
private final ImageClickedListener listener;
@ -52,7 +51,7 @@ class SearchImagesRenderer extends Renderer<Media> {
public void render() {
Media item = getContent();
tvImageName.setText(item.getDisplayTitle());
browseImage.setMedia(item);
browseImage.setImageURI(item.getThumbUrl());
setAuthorView(item, categoryImageAuthor);
}

View file

@ -22,6 +22,11 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.request.ImageRequest;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import org.wikipedia.util.StringUtil;
@ -37,7 +42,6 @@ import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
@ -52,12 +56,6 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Provider;
import timber.log.Timber;
import static android.view.View.GONE;
@ -100,7 +98,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private int initialListTop = 0;
@BindView(R.id.mediaDetailImage)
MediaWikiImageView image;
SimpleDraweeView image;
@BindView(R.id.mediaDetailSpacer)
MediaDetailSpacer spacer;
@BindView(R.id.mediaDetailTitle)
@ -265,7 +263,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void displayMediaDetails() {
//Always load image from Internet to allow viewing the desc, license, and cats
image.setMedia(media);
setupImageView();
title.setText(media.getDisplayTitle());
desc.setHtmlText(media.getDescription());
license.setText(media.getLicense());
@ -277,6 +275,20 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
compositeDisposable.add(disposable);
}
/**
* Uses two image sources.
* - low resolution thumbnail is shown initially
* - when the high resolution image is available, it replaces the low resolution image
*/
private void setupImageView() {
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setLowResImageRequest(ImageRequest.fromUri(media.getThumbUrl()))
.setImageRequest(ImageRequest.fromUri(media.getImageUrl()))
.setOldController(image.getController())
.build();
image.setController(controller);
}
@Override
public void onDestroyView() {
if (layoutListener != null && getView() != null) {
@ -297,6 +309,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void setTextFields(Media media) {
this.media = media;
setupImageView();
desc.setHtmlText(prettyDescription(media));
license.setText(prettyLicense(media));
coordinates.setText(prettyCoordinates(media));

View file

@ -52,7 +52,6 @@ import timber.log.Timber;
* @author Addshore
*/
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private CustomMwApi api;
private CustomMwApi wikidataApi;
@ -293,18 +292,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/edit/@result");
}
@Override
public Single<String> findThumbnailByFilename(String filename) {
return Single.fromCallable(() -> api.action("query")
.param("format", "xml")
.param("prop", "imageinfo")
.param("iiprop", "url")
.param("iiurlwidth", THUMB_SIZE)
.param("titles", filename)
.get()
.getString("/api/query/pages/page/imageinfo/ii/@thumburl"));
}
@Override
public Single<String> parseWikicode(String source) {
return Single.fromCallable(() -> api.action("flow-parsoid-utils")

View file

@ -34,8 +34,6 @@ public interface MediaWikiApi {
Single<Boolean> pageExists(String pageName);
Single<String> findThumbnailByFilename(String filename);
List<String> getSubCategoryList(String categoryName);
List<String> getParentCategoryList(String categoryName);

View file

@ -47,6 +47,7 @@ import timber.log.Timber;
*/
@Singleton
public class OkHttpJsonApiClient {
private static final String THUMB_SIZE = "640";
public static final Type mapType = new TypeToken<Map<String, String>>() {
}.getType();
@ -274,6 +275,7 @@ public class OkHttpJsonApiClient {
private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) {
builder.addQueryParameter("prop", "imageinfo")
.addQueryParameter("iiprop", "url|extmetadata")
.addQueryParameter("iiurlwidth", THUMB_SIZE)
.addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl");
String language = Locale.getDefault().getLanguage();

View file

@ -1,38 +0,0 @@
package fr.free.nrw.commons.utils;
import android.view.View;
/**
* This class includes utilities for contribution list fragment indicators, such as number of
* uploads, notification and nearby cards and their progress bar behind them.
*/
public class ContributionListViewUtils {
/**
* Sets indicator and progress bar visibility according to 3 states, data is ready to display,
* data still loading, both should be invisible because media details fragment is visible
* @param indicator this can be numOfUploads text view, notification/nearby card views
* @param progressBar this is the progress bar behind indicators, displays they are loading
* @param isIndicatorReady is indicator fetched the information will be displayed
* @param isBothInvisible true if contribution list fragment is not active (ie. Media Details Fragment is active)
*/
public static void setIndicatorVisibility(View indicator, View progressBar, boolean isIndicatorReady, boolean isBothInvisible) {
if (indicator!=null && progressBar!=null) {
if (isIndicatorReady) {
// Indicator ready, display them
indicator.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
} else {
if (isBothInvisible) {
// Media Details Fragment is visible, hide both
indicator.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
} else {
// Indicator is not ready, still loading
indicator.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
}
}
}
}

View file

@ -82,7 +82,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, viewIntent, 0);
views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent);
loadImageFromUrl(response.getImageUrl(), context, views, appWidgetManager, appWidgetId);
loadImageFromUrl(response.getThumbUrl(), context, views, appWidgetManager, appWidgetId);
}
},
t -> Timber.e(t, "Fetching picture of the day failed")

View file

@ -18,11 +18,11 @@
android:contentDescription="@string/mediaimage_failed"
/>
<fr.free.nrw.commons.MediaWikiImageView
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/mediaDetailImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:actualImageScaleType="fitCenter"
app:actualImageScaleType="fitXY"
/>
<ScrollView

View file

@ -18,7 +18,7 @@
android:layout_gravity="end|bottom"
/>
<fr.free.nrw.commons.MediaWikiImageView
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/categoryImageView"
android:layout_width="match_parent"
android:layout_height="240dp"

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp"
android:paddingBottom="0dp"
>
android:paddingBottom="0dp">
<TextView
android:id="@+id/contributionSequenceNumber"
@ -18,11 +18,12 @@
android:layout_gravity="end|bottom"
/>
<fr.free.nrw.commons.MediaWikiImageView
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/contributionImage"
android:layout_width="match_parent"
android:layout_height="240dp"
app:actualImageScaleType="fitCenter"
app:actualImageScaleType="fitXY"
fresco:placeholderImage="@drawable/ic_image_black_24dp"
/>
<LinearLayout

View file

@ -1,6 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MediaWikiImageView">
<attr name="isThumbnail" format="boolean" />
</declare-styleable>
</resources>