Categories: Show list of all categories in the main view.

This is different to the old categories drop down, because that also
included meta-categories of "Whats New" and "Recently Updated". Given
we now show them on the first page, this categories screen can do away
with them.

Each category entry loads a few apps to show to the user.

Note: The "View all" button next to each category doesn't currently
go anywhere. It will soon be hooked up to an app list that is filtered
to the selected category.
This commit is contained in:
Peter Serwylo 2016-11-24 11:42:22 +11:00
parent 53df5473f5
commit f5e6d73999
11 changed files with 499 additions and 0 deletions

View File

@ -0,0 +1,40 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
class AppPreviewAdapter extends RecyclerView.Adapter<AppCardController> {
private Cursor cursor;
private final Activity activity;
AppPreviewAdapter(Activity activity) {
this.activity = activity;
}
@Override
public AppCardController onCreateViewHolder(ViewGroup parent, int viewType) {
return new AppCardController(activity, activity.getLayoutInflater().inflate(R.layout.app_card_normal, parent, false));
}
@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 setAppCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}

View File

@ -0,0 +1,44 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.database.Cursor;
import android.support.v4.app.LoaderManager;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.Schema;
public class CategoryAdapter extends RecyclerView.Adapter<CategoryController> {
private Cursor cursor;
private final Activity activity;
private final LoaderManager loaderManager;
public CategoryAdapter(Activity activity, LoaderManager loaderManager) {
this.activity = activity;
this.loaderManager = loaderManager;
}
@Override
public CategoryController onCreateViewHolder(ViewGroup parent, int viewType) {
return new CategoryController(activity, loaderManager, activity.getLayoutInflater().inflate(R.layout.category_item, parent, false));
}
@Override
public void onBindViewHolder(CategoryController holder, int position) {
cursor.moveToPosition(position);
holder.bindModel(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME)));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setCategoriesCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}

View File

@ -0,0 +1,168 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import java.util.Random;
public class CategoryController extends RecyclerView.ViewHolder implements LoaderManager.LoaderCallbacks<Cursor> {
private final Button viewAll;
private final TextView heading;
private final AppPreviewAdapter appCardsAdapter;
private final FrameLayout background;
private final Activity activity;
private final LoaderManager loaderManager;
private String currentCategory;
CategoryController(final Activity activity, LoaderManager loaderManager, View itemView) {
super(itemView);
this.activity = activity;
this.loaderManager = loaderManager;
appCardsAdapter = new AppPreviewAdapter(activity);
viewAll = (Button) itemView.findViewById(R.id.button);
viewAll.setOnClickListener(onViewAll);
heading = (TextView) itemView.findViewById(R.id.name);
background = (FrameLayout) itemView.findViewById(R.id.category_background);
RecyclerView appCards = (RecyclerView) itemView.findViewById(R.id.app_cards);
appCards.setAdapter(appCardsAdapter);
appCards.addItemDecoration(new ItemDecorator(activity));
}
void bindModel(@NonNull String categoryName) {
currentCategory = categoryName;
heading.setText(categoryName);
viewAll.setVisibility(View.INVISIBLE);
loaderManager.initLoader(currentCategory.hashCode(), null, this);
loaderManager.initLoader(currentCategory.hashCode() + 1, null, this);
background.setBackgroundColor(getBackgroundColour(categoryName));
}
public static int getBackgroundColour(@NonNull String categoryName) {
// Seed based on the categoryName, so that each time we try to choose a colour for the same
// category it will look the same for each different user, and each different session.
Random random = new Random(categoryName.toLowerCase().hashCode());
float[] hsv = new float[3];
hsv[0] = random.nextFloat() * 360;
hsv[1] = 0.4f;
hsv[2] = 0.5f;
return Color.HSVToColor(hsv);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id == currentCategory.hashCode() + 1) {
return new CursorLoader(
activity,
AppProvider.getCategoryUri(currentCategory),
new String[]{Schema.AppMetadataTable.Cols._COUNT},
null,
null,
null
);
} else {
return new CursorLoader(
activity,
AppProvider.getTopFromCategoryUri(currentCategory, 20),
new String[]{
Schema.AppMetadataTable.Cols.NAME,
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
Schema.AppMetadataTable.Cols.SUMMARY,
Schema.AppMetadataTable.Cols.ICON_URL,
},
null,
null,
null
);
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
int topAppsId = currentCategory.hashCode();
int countAllAppsId = topAppsId + 1;
// Anything other than these IDs indicates that the loader which just finished finished
// is no longer the one this view holder is interested in, due to the user having
// scrolled away already during the asynchronous query being run.
if (loader.getId() == topAppsId) {
appCardsAdapter.setAppCursor(cursor);
} else if (loader.getId() == countAllAppsId) {
cursor.moveToFirst();
int numAppsInCategory = cursor.getInt(0);
viewAll.setVisibility(View.VISIBLE);
viewAll.setText(activity.getResources().getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, numAppsInCategory));
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
appCardsAdapter.setAppCursor(null);
}
private final View.OnClickListener onViewAll = new View.OnClickListener() {
@Override
public void onClick(View v) {
}
};
/**
* Applies excessive padding to the start of the first item. This is so that the category artwork
* can peek out and make itself visible. This is RTL friendly.
* @see org.fdroid.fdroid.R.dimen#category_preview__app_list__padding__horizontal
* @see org.fdroid.fdroid.R.dimen#category_preview__app_list__padding__horizontal__first
*/
private static class ItemDecorator extends RecyclerView.ItemDecoration {
private final Context context;
ItemDecorator(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int horizontalPadding = (int) context.getResources().getDimension(R.dimen.category_preview__app_list__padding__horizontal);
int horizontalPaddingFirst = (int) context.getResources().getDimension(R.dimen.category_preview__app_list__padding__horizontal__first);
boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
int itemPosition = parent.getChildLayoutPosition(view);
boolean first = itemPosition == 0;
// Leave this "paddingEnd" local variable here for clarity when converting from
// left/right to start/end for RTL friendly layout.
// noinspection UnnecessaryLocalVariable
int paddingEnd = horizontalPadding;
int paddingStart = first ? horizontalPaddingFirst : horizontalPadding;
int paddingLeft = isLtr ? paddingStart : paddingEnd;
int paddingRight = isLtr ? paddingEnd : paddingStart;
outRect.set(paddingLeft, 0, paddingRight, 0);
}
}
}

View File

@ -0,0 +1,80 @@
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.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.CategoryProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.categories.CategoryAdapter;
/**
* Responsible for ensuring that the categories view is inflated and then populated correctly.
* Will start a loader to get the list of categories from the database and populate a recycler
* view with relevant info about each.
*/
class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 429820532;
private final CategoryAdapter categoryAdapter;
private final AppCompatActivity activity;
CategoriesViewBinder(AppCompatActivity activity, FrameLayout parent) {
this.activity = activity;
View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true);
categoryAdapter = new CategoryAdapter(activity, activity.getSupportLoaderManager());
RecyclerView categoriesList = (RecyclerView) categoriesView.findViewById(R.id.category_list);
categoriesList.setHasFixedSize(true);
categoriesList.setLayoutManager(new LinearLayoutManager(activity));
categoriesList.setAdapter(categoryAdapter);
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,
CategoryProvider.getAllCategories(),
Schema.CategoryTable.Cols.ALL,
null,
null,
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId() != LOADER_ID) {
return;
}
categoryAdapter.setCategoriesCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() != LOADER_ID) {
return;
}
categoryAdapter.setCategoriesCursor(null);
}
}

View File

@ -46,7 +46,11 @@ class MainViewController extends RecyclerView.ViewHolder {
new MyAppsViewBinder(activity, frame);
}
/**
* @see CategoriesViewBinder
*/
public void bindCategoriesView() {
new CategoriesViewBinder(activity, frame);
}
/**

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Visually different from other app cards because it doesn't have a drop shadow, and has a larger
corner radius.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="#faf8ef" />
</shape>

View File

@ -0,0 +1,41 @@
<?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="100dp"
android:layout_height="130dp"
android:background="@drawable/category_preview_app_card_background"
android:padding="8dp">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->
<ImageView
android:id="@+id/icon"
android:layout_width="48dip"
android:layout_height="48dip"
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"
tools:ignore="ContentDescription" />
<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="3"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/icon"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,80 @@
<?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:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/name"
tools:text="Business"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBaseline_toBaselineOf="@+id/button"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:textSize="18sp"
android:textColor="#4a4a4a"
android:paddingLeft="18dp"
android:paddingStart="18dp"
android:paddingRight="18dp"
android:paddingEnd="18dp"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/button"
tools:text="View all 10"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@android:color/transparent"
android:paddingLeft="18dp"
android:paddingStart="18dp"
android:paddingRight="18dp"
android:paddingEnd="18dp"
android:paddingTop="24dp"
android:paddingBottom="12dp"
android:textSize="14sp"
android:textAllCaps="true"
android:textColor="@color/fdroid_blue"
tools:layout_editor_absoluteX="268dp" />
<FrameLayout
android:id="@+id/category_background"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/app_cards"
app:layout_constraintBottom_toBottomOf="@+id/app_cards"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:background="#ffffbbbb"/>
<ImageView
android:id="@+id/category_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
android:layout_width="100dp"
android:layout_height="100dp"
tools:ignore="ContentDescription" />
<android.support.v7.widget.RecyclerView
android:id="@+id/app_cards"
tools:listitem="@layout/app_card_normal"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layoutManager="LinearLayoutManager"
android:orientation="horizontal"
android:paddingTop="@dimen/category_preview__app_list__padding__vertical"
android:paddingBottom="@dimen/category_preview__app_list__padding__vertical"
android:clipToPadding="false"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"/>
</android.support.constraint.ConstraintLayout>

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/category_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/category_item"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scrollbars="vertical" />
</LinearLayout>

View File

@ -20,4 +20,11 @@
<dimen name="whats_new__padding__app_card__horizontal">12dp</dimen>
<dimen name="whats_new__padding__app_card__vertical">10dp</dimen>
<dimen name="category_preview__app_list__padding__horizontal">4dp</dimen>
<dimen name="category_preview__app_list__padding__horizontal__first">72dp</dimen>
<dimen name="category_preview__app_list__padding__vertical">18dp</dimen>
<dimen name="category_preview__padding__recycler_view__top">12dp</dimen>
<dimen name="category_preview__padding__app_card__horizontal">3dp</dimen>
<dimen name="category_preview__padding__app_card__vertical">4dp</dimen>
</resources>

View File

@ -277,6 +277,13 @@
<string name="category_Time">Time</string>
<string name="category_Writing">Writing</string>
<plurals name="button_view_all_apps_in_category">
<!-- Even though these are the same as eachother, Android docs suggest always specifying at
least "one" and "other": https://developer.android.com/guide/topics/resources/string-resource.html#Plurals -->
<item quantity="one">View all %d</item>
<item quantity="other">View all %d</item>
</plurals>
<string name="empty_installed_app_list">No apps installed.\n\nThere are apps on your device, but they are not available from F-Droid. This could be because you need to update your repositories, or the repositories genuinely don\'t have your apps available.</string>
<string name="empty_available_app_list">No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.</string>
<string name="empty_can_update_app_list">All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).</string>