Whats New: Added list of recently updated apps to the main view.

Smooshes the recently updated and recently added lists into one,
and adds a status line under each app saying which of the two it
is (i.e. "Recnelty Updated" or "Whats New".
This commit is contained in:
Peter Serwylo 2016-11-24 10:56:15 +11:00
parent 517301194b
commit 69b58c7646
14 changed files with 711 additions and 24 deletions

View File

@ -29,7 +29,11 @@ import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.text.Editable; import android.text.Editable;
import android.text.Html; import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.TypefaceSpan;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
@ -487,6 +491,23 @@ public final class Utils {
return formatDateFormat(TIME_FORMAT, date, fallback); return formatDateFormat(TIME_FORMAT, date, fallback);
} }
/**
* Formats the app name using "sans-serif" and then appends the summary after a space with
* "sans-serif-light". Doesn't mandate any font sizes or any other styles, that is up to the
* {@link android.widget.TextView} which it ends up being displayed in.
*/
public static CharSequence formatAppNameAndSummary(String appName, String summary) {
String toFormat = appName + ' ' + summary;
CharacterStyle normal = new TypefaceSpan("sans-serif");
CharacterStyle light = new TypefaceSpan("sans-serif-light");
SpannableStringBuilder sb = new SpannableStringBuilder(toFormat);
sb.setSpan(normal, 0, appName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.setSpan(light, appName.length(), toFormat.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return sb;
}
// Need this to add the unimplemented support for ordered and unordered // Need this to add the unimplemented support for ordered and unordered
// lists to Html.fromHtml(). // lists to Html.fromHtml().
public static class HtmlTagHandler implements Html.TagHandler { public static class HtmlTagHandler implements Html.TagHandler {

View File

@ -373,7 +373,6 @@ public class AppProvider extends FDroidProvider {
protected static final String PATH_APPS = "apps"; protected static final String PATH_APPS = "apps";
protected static final String PATH_SPECIFIC_APP = "app"; protected static final String PATH_SPECIFIC_APP = "app";
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated"; private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
private static final String PATH_NEWLY_ADDED = "newlyAdded";
private static final String PATH_CATEGORY = "category"; private static final String PATH_CATEGORY = "category";
private static final String PATH_REPO = "repo"; private static final String PATH_REPO = "repo";
private static final String PATH_HIGHEST_PRIORITY = "highestPriority"; private static final String PATH_HIGHEST_PRIORITY = "highestPriority";
@ -386,8 +385,7 @@ public class AppProvider extends FDroidProvider {
private static final int SEARCH_TEXT = INSTALLED + 1; private static final int SEARCH_TEXT = INSTALLED + 1;
private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1; private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1;
private static final int RECENTLY_UPDATED = SEARCH_TEXT_AND_CATEGORIES + 1; private static final int RECENTLY_UPDATED = SEARCH_TEXT_AND_CATEGORIES + 1;
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int CATEGORY = RECENTLY_UPDATED + 1;
private static final int CATEGORY = NEWLY_ADDED + 1;
private static final int CALC_SUGGESTED_APKS = CATEGORY + 1; private static final int CALC_SUGGESTED_APKS = CATEGORY + 1;
private static final int REPO = CALC_SUGGESTED_APKS + 1; private static final int REPO = CALC_SUGGESTED_APKS + 1;
private static final int SEARCH_REPO = REPO + 1; private static final int SEARCH_REPO = REPO + 1;
@ -401,7 +399,6 @@ public class AppProvider extends FDroidProvider {
MATCHER.addURI(getAuthority(), null, CODE_LIST); MATCHER.addURI(getAuthority(), null, CODE_LIST);
MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS); MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS);
MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED);
MATCHER.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES);
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT);
@ -425,10 +422,6 @@ public class AppProvider extends FDroidProvider {
return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED); return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED);
} }
public static Uri getNewlyAddedUri() {
return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED);
}
private static Uri calcSuggestedApksUri() { private static Uri calcSuggestedApksUri() {
return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS); return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS);
} }
@ -666,12 +659,6 @@ public class AppProvider extends FDroidProvider {
return new AppQuerySelection(selection); return new AppQuerySelection(selection);
} }
private AppQuerySelection queryNewlyAdded() {
final String selection = getTableName() + "." + Cols.ADDED + " > ?";
final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")};
return new AppQuerySelection(selection, args);
}
/** /**
* Ensures that for each app metadata row with the same package name, only the one from the repo * Ensures that for each app metadata row with the same package name, only the one from the repo
* with the best priority is represented in the result set. While possible to calculate this * with the best priority is represented in the result set. While possible to calculate this
@ -689,9 +676,7 @@ public class AppProvider extends FDroidProvider {
} }
private AppQuerySelection queryRecentlyUpdated() { private AppQuerySelection queryRecentlyUpdated() {
final String app = getTableName(); final String selection = getTableName() + "." + Cols.LAST_UPDATED + " > ? ";
final String lastUpdated = app + "." + Cols.LAST_UPDATED;
final String selection = app + "." + Cols.ADDED + " != " + lastUpdated + " AND " + lastUpdated + " > ?";
final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")}; final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")};
return new AppQuerySelection(selection, args); return new AppQuerySelection(selection, args);
} }
@ -810,12 +795,6 @@ public class AppProvider extends FDroidProvider {
includeSwap = false; includeSwap = false;
break; break;
case NEWLY_ADDED:
sortOrder = getTableName() + "." + Cols.ADDED + " DESC";
selection = selection.add(queryNewlyAdded());
includeSwap = false;
break;
case HIGHEST_PRIORITY: case HIGHEST_PRIORITY:
selection = selection.add(queryPackageName(uri.getLastPathSegment())); selection = selection.add(queryPackageName(uri.getLastPathSegment()));
includeSwap = false; includeSwap = false;

View File

@ -0,0 +1,179 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.util.Pair;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import java.util.Date;
/**
* The {@link AppCardController} can bind an app to several different layouts, as long as the layout
* contains the following elements:
* + {@link R.id#icon} ({@link ImageView}, required)
* + {@link R.id#summary} ({@link TextView}, required)
* + {@link R.id#featured_image} ({@link ImageView}, optional)
* + {@link R.id#status} ({@link TextView}, optional)
*/
public class AppCardController extends RecyclerView.ViewHolder implements ImageLoadingListener, View.OnClickListener {
@NonNull
private final ImageView icon;
@NonNull
private final TextView summary;
@Nullable
private final TextView status;
@Nullable
private final ImageView featuredImage;
@Nullable
private App currentApp;
private final Activity activity;
private final int defaultFeaturedImageColour;
private final DisplayImageOptions displayImageOptions;
private final Date recentCuttoffDate;
public AppCardController(Activity activity, View itemView) {
super(itemView);
this.activity = activity;
recentCuttoffDate = Preferences.get().calcMaxHistory();
icon = (ImageView) findViewAndEnsureNonNull(itemView, R.id.icon);
summary = (TextView) findViewAndEnsureNonNull(itemView, R.id.summary);
featuredImage = (ImageView) itemView.findViewById(R.id.featured_image);
status = (TextView) itemView.findViewById(R.id.status);
defaultFeaturedImageColour = activity.getResources().getColor(R.color.cardview_light_background);
displayImageOptions = Utils.getImageLoadingOptions().build();
itemView.setOnClickListener(this);
}
/**
* The contract that this controller has is that it will consume any layout resource, given
* it has some specific view types (with specific IDs) available. This helper function will
* throw an {@link IllegalArgumentException} if the view doesn't exist,
*/
@NonNull
private View findViewAndEnsureNonNull(View view, @IdRes int res) {
View found = view.findViewById(res);
if (found == null) {
String resName = activity.getResources().getResourceName(res);
throw new IllegalArgumentException("Layout for AppCardController requires " + resName);
}
return found;
}
public void bindApp(@NonNull App app) {
currentApp = app;
summary.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
if (status != null) {
if (app.added != null && app.added.after(recentCuttoffDate) && (app.lastUpdated == null || app.added.equals(app.lastUpdated))) {
status.setText(activity.getString(R.string.category_Whats_New));
status.setVisibility(View.VISIBLE);
} else if (app.lastUpdated != null && app.lastUpdated.after(recentCuttoffDate)) {
status.setText(activity.getString(R.string.category_Recently_Updated));
status.setVisibility(View.VISIBLE);
} else {
status.setVisibility(View.GONE);
}
}
if (featuredImage != null) {
featuredImage.setBackgroundColor(defaultFeaturedImageColour);
}
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions, this);
}
/**
* When the user clicks/touches an app card, we launch the {@link AppDetails} activity in response.
*/
@Override
public void onClick(View v) {
if (currentApp == null) {
return;
}
Intent intent = new Intent(activity, AppDetails.class);
intent.putExtra(AppDetails.EXTRA_APPID, currentApp.packageName);
if (Build.VERSION.SDK_INT >= 21) {
Pair<View, String> iconTransitionPair = Pair.create((View) icon, activity.getString(R.string.transition_app_item_icon));
@SuppressWarnings("unchecked") // We are passing the right type as the second varargs argument (i.e. a Pair<View, String>).
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, iconTransitionPair).toBundle();
activity.startActivity(intent, bundle);
} else {
activity.startActivity(intent);
}
}
// =============================================================================================
// Icon loader callbacks
//
// Most are unused, the main goal is to specify a background colour for the featured image if
// no featured image is specified in the apps metadata.
// =============================================================================================
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
final ImageView image = featuredImage;
if (image != null) {
new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
image.setBackgroundColor(palette.getDominantColor(defaultFeaturedImageColour));
}
});
}
}
@Override
public void onLoadingStarted(String imageUri, View view) {
// Do nothing
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
// Do nothing
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
// Do nothing
}
}

View File

@ -178,7 +178,9 @@ public class AvailableAppsFragment extends AppListFragment implements
return AppProvider.getRecentlyUpdatedUri(); return AppProvider.getRecentlyUpdatedUri();
} }
if (currentCategory.equals(CategoryProvider.Helper.getCategoryWhatsNew(getActivity()))) { if (currentCategory.equals(CategoryProvider.Helper.getCategoryWhatsNew(getActivity()))) {
return AppProvider.getNewlyAddedUri(); // Removed this feature in the new UI. this fragment will be gone soon so not implementing it again.
// return AppProvider.getNewlyAddedUri();
return AppProvider.getRecentlyUpdatedUri();
} }
return AppProvider.getCategoryUri(currentCategory); return AppProvider.getCategoryUri(currentCategory);
} }

View File

@ -31,7 +31,11 @@ class MainViewController extends RecyclerView.ViewHolder {
frame.removeAllViews(); frame.removeAllViews();
} }
/**
* @see WhatsNewViewBinder
*/
public void bindWhatsNewView() { public void bindWhatsNewView() {
new WhatsNewViewBinder(activity, frame);
} }
public void bindMyApps() { public void bindMyApps() {

View File

@ -0,0 +1,96 @@
package org.fdroid.fdroid.views.main;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter;
/**
* Loads a list of newly added or recently updated apps and displays them to the user.
*/
class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 978015789;
private final WhatsNewAdapter whatsNewAdapter;
private final AppCompatActivity activity;
private static RecyclerView.ItemDecoration appListDecorator;
WhatsNewViewBinder(AppCompatActivity activity, FrameLayout parent) {
this.activity = activity;
View whatsNewView = activity.getLayoutInflater().inflate(R.layout.main_tab_whats_new, parent, true);
whatsNewAdapter = new WhatsNewAdapter(activity);
GridLayoutManager layoutManager = new GridLayoutManager(activity, 2);
layoutManager.setSpanSizeLookup(new WhatsNewAdapter.SpanSizeLookup());
RecyclerView appList = (RecyclerView) whatsNewView.findViewById(R.id.app_list);
appList.setHasFixedSize(true);
appList.setLayoutManager(layoutManager);
appList.setAdapter(whatsNewAdapter);
// This is a bit hacky, but for some reason even though we are inflating the main_tab_whats_new
// layout above, the app_list RecyclerView seems to remember that it has decorations from before.
// If we blindly call addItemDecoration here without first removing the existing one, it will
// double up on all of the paddings the second time we view it. The third time it will triple up
// on the paddings, etc. In addition, the API doesn't allow us to "clearAllDecorators()". Instead
// we need to hold onto the reference to the one we added in order to remove it.
if (appListDecorator == null) {
appListDecorator = new WhatsNewAdapter.ItemDecorator(activity);
} else {
appList.removeItemDecoration(appListDecorator);
}
appList.addItemDecoration(appListDecorator);
activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id != LOADER_ID) {
return null;
}
return new CursorLoader(
activity,
AppProvider.getRecentlyUpdatedUri(),
Schema.AppMetadataTable.Cols.ALL,
null,
null,
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId() != LOADER_ID) {
return;
}
whatsNewAdapter.setAppsCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() != LOADER_ID) {
return;
}
whatsNewAdapter.setAppsCursor(null);
}
}

View File

@ -0,0 +1,129 @@
package org.fdroid.fdroid.views.whatsnew;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.categories.AppCardController;
public class WhatsNewAdapter extends RecyclerView.Adapter<AppCardController> {
private Cursor cursor;
private final Activity activity;
public WhatsNewAdapter(Activity activity) {
this.activity = activity;
}
@Override
public AppCardController onCreateViewHolder(ViewGroup parent, int viewType) {
int layout;
if (viewType == R.id.whats_new_feature) {
layout = R.layout.app_card_featured;
} else if (viewType == R.id.whats_new_large_tile) {
layout = R.layout.app_card_large;
} else if (viewType == R.id.whats_new_small_tile) {
layout = R.layout.app_card_horizontal;
} else if (viewType == R.id.whats_new_regular_list) {
layout = R.layout.app_card_list_item;
} else {
throw new IllegalArgumentException("Unknown view type when rendering \"Whats New\": " + viewType);
}
return new AppCardController(activity, activity.getLayoutInflater().inflate(layout, parent, false));
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return R.id.whats_new_feature;
} else if (position <= 2) {
return R.id.whats_new_large_tile;
} else if (position <= 4) {
return R.id.whats_new_small_tile;
} else {
return R.id.whats_new_regular_list;
}
}
@Override
public void onBindViewHolder(AppCardController holder, int position) {
cursor.moveToPosition(position);
holder.bindApp(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setAppsCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
// TODO: Replace with https://github.com/lucasr/twoway-view which looks really really cool, but
// no longer under active development (despite heaps of forks/stars on github).
public static class SpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int position) {
if (position == 0) {
return 2;
} else if (position <= 4) {
return 1;
} else {
return 2;
}
}
}
/**
* Applies padding to items, ensuring that the spacing on the left, centre, and right all match.
* The vertical padding is slightly shorter than the horizontal padding also.
* @see org.fdroid.fdroid.R.dimen#whats_new__padding__app_card__horizontal
* @see org.fdroid.fdroid.R.dimen#whats_new__padding__app_card__vertical
*/
public static class ItemDecorator extends RecyclerView.ItemDecoration {
private final Context context;
public ItemDecorator(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int horizontalPadding = (int) context.getResources().getDimension(R.dimen.whats_new__padding__app_card__horizontal);
int verticalPadding = (int) context.getResources().getDimension(R.dimen.whats_new__padding__app_card__vertical);
if (position == 0) {
// Don't set any padding for the first item as the FeatureImage behind it needs to butt right
// up against the left/top/right of the screen.
outRect.set(0, 0, 0, verticalPadding);
} else if (position <= 4) {
// Odd items are on the left, even on the right.
// The item on the left will have both left and right padding. The item on the right
// will only have padding on the right. This will allow the same amount of padding
// on the left, centre, and right of the grid, rather than double the padding in the
// middle (which would happen if both left+right paddings were set for both items).
boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
boolean isAtStart = (position % 2) == 1;
int paddingStart = isAtStart ? horizontalPadding : 0;
int paddingLeft = isLtr ? paddingStart : horizontalPadding;
int paddingRight = isLtr ? horizontalPadding : paddingStart;
outRect.set(paddingLeft, 0, paddingRight, verticalPadding);
} else {
outRect.set(horizontalPadding, 0, horizontalPadding, verticalPadding);
}
}
}
}

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="2dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/featured_image"
android:layout_width="0dp"
android:layout_height="120dp"
tools:src="@color/fdroid_green"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/header_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="80dp" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/whats_new__padding__app_card__horizontal"
android:layout_marginStart="@dimen/whats_new__padding__app_card__horizontal"
android:layout_marginRight="@dimen/whats_new__padding__app_card__horizontal"
android:layout_marginEnd="@dimen/whats_new__padding__app_card__horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/header_height"
>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize, perhaps after wrapping to a new line"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:lines="2"
android:textSize="14sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintTop_toBottomOf="@+id/summary"
app:layout_constraintStart_toEndOf="@+id/icon"
android:textStyle="italic"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:lines="4"
android:textSize="13sp"
android:ellipsize="end"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/summary"
app:layout_constraintStart_toStartOf="parent"
android:textStyle="italic"
android:layout_marginTop="4dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="220dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="96dip"
android:layout_height="96dip"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:maxLines="4"
android:textSize="14sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/icon"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textStyle="italic"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:lines="2"
android:textSize="14sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteY="8dp" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintTop_toBottomOf="@+id/summary"
app:layout_constraintStart_toEndOf="@+id/icon"
android:textStyle="italic"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/app_card_normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scrollbars="vertical" />
</LinearLayout>

View File

@ -18,4 +18,6 @@
<!-- The selected item stands out from the background by this elevation --> <!-- The selected item stands out from the background by this elevation -->
<dimen name="details_screenshot_selected_elevation">3dp</dimen> <dimen name="details_screenshot_selected_elevation">3dp</dimen>
<dimen name="whats_new__padding__app_card__horizontal">12dp</dimen>
<dimen name="whats_new__padding__app_card__vertical">10dp</dimen>
</resources> </resources>

View File

@ -2,4 +2,9 @@
<resources> <resources>
<item type="id" name="category_spinner" /> <item type="id" name="category_spinner" />
<item type="id" name="appDetailsSummaryHeader" /> <item type="id" name="appDetailsSummaryHeader" />
<item type="id" name="whats_new_feature" />
<item type="id" name="whats_new_large_tile" />
<item type="id" name="whats_new_small_tile" />
<item type="id" name="whats_new_regular_list" />
</resources> </resources>