jenkins-bot has submitted this change and it was merged. Change subject: Add Feed Activity, Fragment, & card aggregator UI ......................................................................
Add Feed Activity, Fragment, & card aggregator UI • Add FeedActivity and wire it to the nav drawer. An Activity is used to help enforce separation of concerns. It's sparse and may be removed for a pure View or Fragment implementation at any time. • Add bare bones FeedFragment with a FeedView and a button to add dummy cards. • Add supporting components (a couple placeholders and AutoFitRecyclerView). Bug: T129078 Change-Id: Icd6f10097d6b32f40f27b1409ead4ec803acb7ea --- M app/src/main/AndroidManifest.xml A app/src/main/java/org/wikipedia/feed/FeedActivity.java A app/src/main/java/org/wikipedia/feed/FeedFragment.java A app/src/main/java/org/wikipedia/feed/model/FeedCard.java A app/src/main/java/org/wikipedia/feed/view/FeedCardView.java A app/src/main/java/org/wikipedia/feed/view/FeedView.java M app/src/main/java/org/wikipedia/page/NavDrawerHelper.java A app/src/main/java/org/wikipedia/views/AutoFitRecyclerView.java A app/src/main/java/org/wikipedia/views/DefaultViewHolder.java A app/src/main/res/layout/fragment_feed.xml A app/src/main/res/layout/view_feed.xml A app/src/main/res/layout/view_feed_card.xml M app/src/main/res/values/attrs.xml M app/src/main/res/values/dimens.xml M app/src/main/res/values/strings_no_translate.xml 15 files changed, 426 insertions(+), 5 deletions(-) Approvals: Dbrant: Looks good to me, approved jenkins-bot: Verified diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d34780..dcfbd88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,11 @@ <meta-data android:name="net.hockeyapp.android.appIdentifier" android:value="@string/hockeyapp_app_id" /> + <activity android:name=".feed.FeedActivity" + android:label="@string/activity_feed_title" + android:configChanges="orientation|keyboardHidden|keyboard|screenSize" + /> + <activity android:name=".page.PageActivity" android:theme="@style/PageTheme" android:windowSoftInputMode="stateHidden" diff --git a/app/src/main/java/org/wikipedia/feed/FeedActivity.java b/app/src/main/java/org/wikipedia/feed/FeedActivity.java new file mode 100644 index 0000000..fec2f57 --- /dev/null +++ b/app/src/main/java/org/wikipedia/feed/FeedActivity.java @@ -0,0 +1,16 @@ +package org.wikipedia.feed; + +import android.content.Context; +import android.content.Intent; + +import org.wikipedia.activity.SingleFragmentActivity; + +public class FeedActivity extends SingleFragmentActivity<FeedFragment> { + public static Intent newIntent(Context context) { + return new Intent(context, FeedActivity.class); + } + + @Override protected FeedFragment createFragment() { + return FeedFragment.newInstance(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/feed/FeedFragment.java b/app/src/main/java/org/wikipedia/feed/FeedFragment.java new file mode 100644 index 0000000..69c6244 --- /dev/null +++ b/app/src/main/java/org/wikipedia/feed/FeedFragment.java @@ -0,0 +1,63 @@ +package org.wikipedia.feed; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.wikipedia.R; +import org.wikipedia.activity.CallbackFragment; +import org.wikipedia.activity.FragmentUtil; +import org.wikipedia.feed.model.FeedCard; +import org.wikipedia.feed.view.FeedView; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; + +public class FeedFragment extends Fragment + implements CallbackFragment<CallbackFragment.Callback> { + @BindView(R.id.fragment_feed_feed) FeedView feedView; + private Unbinder unbinder; + + @NonNull private final List<FeedCard> cards = new ArrayList<>(); + + public static FeedFragment newInstance() { + return new FeedFragment(); + } + + @Nullable @Override public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + View view = inflater.inflate(R.layout.fragment_feed, container, false); + + unbinder = ButterKnife.bind(this, view); + feedView.set(cards); + + return view; + } + + @Override public void onDestroyView() { + unbinder.unbind(); + super.onDestroyView(); + } + + @Override @Nullable public Callback getCallback() { + return FragmentUtil.getCallback(this, Callback.class); + } + + // TODO: [Feed] remove. + @OnClick(R.id.fragment_feed_add_card) void addCard() { + cards.add(new FeedCard()); + feedView.update(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/feed/model/FeedCard.java b/app/src/main/java/org/wikipedia/feed/model/FeedCard.java new file mode 100644 index 0000000..f8fabe6 --- /dev/null +++ b/app/src/main/java/org/wikipedia/feed/model/FeedCard.java @@ -0,0 +1,37 @@ +package org.wikipedia.feed.model; + +import android.support.annotation.ColorInt; + +import java.util.Random; + +public class FeedCard { + // TODO: [Feed] remove this fun data and fill in model data. + private final int height; + @ColorInt private final int color; + + public FeedCard() { + height = randomHeight(); + color = randomColor(); + } + + public int height() { + return height; + } + + @ColorInt public int color() { + return color; + } + + @ColorInt int randomColor() { + final int alphaMask = 0xff000000; + final int colorMask = 0x00ffffff; + return new Random().nextInt(colorMask) | alphaMask; + } + + private int randomHeight() { + final int minHeight = 128; + final int range = 384; + return minHeight + new Random().nextInt(range); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/feed/view/FeedCardView.java b/app/src/main/java/org/wikipedia/feed/view/FeedCardView.java new file mode 100644 index 0000000..4fa4be4 --- /dev/null +++ b/app/src/main/java/org/wikipedia/feed/view/FeedCardView.java @@ -0,0 +1,40 @@ +package org.wikipedia.feed.view; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.CardView; + +import org.wikipedia.R; +import org.wikipedia.feed.model.FeedCard; +import org.wikipedia.util.DimenUtil; + +import butterknife.ButterKnife; + +public class FeedCardView extends CardView { + public FeedCardView(Context context) { + super(context); + + inflate(getContext(), R.layout.view_feed_card, this); + ButterKnife.bind(this); + } + + public void update(@NonNull FeedCard card) { + // TODO: [Feed] replace with real data. + setBackgroundColor(card.color()); + setMinimumHeight(card.height()); + } + + @Override protected void onAttachedToWindow() { + super.onAttachedToWindow(); + setLayoutAttrs(); + } + + private void setLayoutAttrs() { + final int margin = Math.round(DimenUtil.getDimension(R.dimen.margin)); + MarginLayoutParams params = ((MarginLayoutParams) getLayoutParams()); + params.topMargin = margin; + params.rightMargin = margin; + params.bottomMargin = margin; + params.leftMargin = margin; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/feed/view/FeedView.java b/app/src/main/java/org/wikipedia/feed/view/FeedView.java new file mode 100644 index 0000000..9c2bda8 --- /dev/null +++ b/app/src/main/java/org/wikipedia/feed/view/FeedView.java @@ -0,0 +1,101 @@ +package org.wikipedia.feed.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView.Adapter; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.wikipedia.R; +import org.wikipedia.feed.model.FeedCard; +import org.wikipedia.views.AutoFitRecyclerView; +import org.wikipedia.views.DefaultViewHolder; + +import java.util.Collections; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class FeedView extends FrameLayout { + @BindView(R.id.view_feed_recycler) AutoFitRecyclerView recyclerView; + private StaggeredGridLayoutManager recyclerLayoutManager; + private RecyclerAdapter recyclerAdapter; + + public FeedView(Context context) { + super(context); + init(); + } + + public FeedView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public FeedView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public FeedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public void set(@NonNull List<FeedCard> cards) { + // TODO: should this class be responsible for showing a "no items in collection" view? It + // would be nice to show placeholder elements while it loads. + recyclerAdapter = new RecyclerAdapter(cards); + recyclerView.setAdapter(recyclerAdapter); + } + + public void update() { + recyclerAdapter.notifyDataSetChanged(); + } + + private void init() { + inflate(getContext(), R.layout.view_feed, this); + ButterKnife.bind(this); + initRecycler(); + } + + private void initRecycler() { + recyclerLayoutManager = new StaggeredGridLayoutManager(recyclerView.getColumns(), + StaggeredGridLayoutManager.VERTICAL); + recyclerView.setLayoutManager(recyclerLayoutManager); + recyclerView.callback(new RecyclerViewColumnCallback()); + set(Collections.<FeedCard>emptyList()); + } + + private class RecyclerAdapter extends Adapter<DefaultViewHolder<FeedCardView>> { + @NonNull private final List<FeedCard> cards; + + RecyclerAdapter(@NonNull List<FeedCard> cards) { + this.cards = cards; + } + + @Override public DefaultViewHolder<FeedCardView> onCreateViewHolder(ViewGroup parent, + int viewType) { + return new DefaultViewHolder<>(new FeedCardView(getContext())); + } + + @Override public void onBindViewHolder(DefaultViewHolder<FeedCardView> holder, int position) { + holder.getView().update(cards.get(position)); + } + + @Override public int getItemCount() { + return cards.size(); + } + } + + private class RecyclerViewColumnCallback implements AutoFitRecyclerView.Callback { + @Override public void onColumns(int columns) { + recyclerLayoutManager.setSpanCount(columns); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/page/NavDrawerHelper.java b/app/src/main/java/org/wikipedia/page/NavDrawerHelper.java index 776334a..4ca6b49 100644 --- a/app/src/main/java/org/wikipedia/page/NavDrawerHelper.java +++ b/app/src/main/java/org/wikipedia/page/NavDrawerHelper.java @@ -18,6 +18,8 @@ import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.LoginFunnel; import org.wikipedia.analytics.NavMenuFunnel; +import org.wikipedia.feed.FeedActivity; +import org.wikipedia.feed.FeedFragment; import org.wikipedia.history.HistoryEntry; import org.wikipedia.history.HistoryFragment; import org.wikipedia.login.LoginActivity; @@ -76,7 +78,7 @@ public boolean onNavigationItemSelected(MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.nav_item_feed: - // TODO: [Feed] show Feed Fragment. + launchFeedActivity(); // TODO: [Feed] add Feed logging. break; case R.id.nav_item_today: @@ -198,10 +200,9 @@ } @Nullable @IdRes private Integer fragmentToMenuId(Class<? extends Fragment> fragment) { - // TODO: [Feed] add Feed Fragment. - /*if (fragment == FeedFragment.class) { + if (fragment == FeedFragment.class) { return R.id.nav_item_feed; - } else*/ if (fragment == PageFragment.class) { + } else if (fragment == PageFragment.class) { return R.id.nav_item_today; } else if (fragment == HistoryFragment.class) { return R.id.nav_item_history; @@ -246,6 +247,11 @@ } } + private void launchFeedActivity() { + activity.closeNavDrawer(); + activity.startActivity(FeedActivity.newIntent(activity)); + } + private void launchSettingsActivity() { activity.closeNavDrawer(); activity.startActivityForResult(new Intent().setClass(app, SettingsActivity.class), diff --git a/app/src/main/java/org/wikipedia/views/AutoFitRecyclerView.java b/app/src/main/java/org/wikipedia/views/AutoFitRecyclerView.java new file mode 100644 index 0000000..b900c85 --- /dev/null +++ b/app/src/main/java/org/wikipedia/views/AutoFitRecyclerView.java @@ -0,0 +1,82 @@ +package org.wikipedia.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.AttrRes; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; + +import org.wikipedia.R; + +/** {@link RecyclerView} that invokes a callback when the number of columns should be updated. */ +public class AutoFitRecyclerView extends RecyclerView { + public interface Callback { + void onColumns(int columns); + } + + private static final int MIN_COLUMNS = 1; + + @IntRange(from = MIN_COLUMNS) private int columns = MIN_COLUMNS; + private int minColumnWidth; + + @NonNull private Callback callback = new DefaultCallback(); + + public AutoFitRecyclerView(Context context) { + super(context, null); + init(null, 0); + } + + public AutoFitRecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public AutoFitRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs, defStyleAttr); + } + + @IntRange(from = MIN_COLUMNS) public int getColumns() { + return columns; + } + + public void minColumnWidth(int minColumnWidth) { + if (this.minColumnWidth != minColumnWidth) { + this.minColumnWidth = minColumnWidth; + requestLayout(); + } + } + + public void callback(@Nullable Callback callback) { + this.callback = callback == null ? new DefaultCallback() : callback; + } + + @Override protected void onMeasure(int widthSpec, int heightSpec) { + super.onMeasure(widthSpec, heightSpec); + int cols = calculateColumns(minColumnWidth, getMeasuredWidth()); + if (this.columns != cols) { + this.columns = cols; + callback.onColumns(this.columns); + } + } + + private int calculateColumns(int columnWidth, int availableWidth) { + return columnWidth > 0 ? Math.max(MIN_COLUMNS, availableWidth / columnWidth) : MIN_COLUMNS; + } + + private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + if (attrs != null) { + TypedArray array = getContext().obtainStyledAttributes(attrs, + R.styleable.AutoFitRecyclerView, defStyleAttr, 0); + minColumnWidth = array.getDimensionPixelSize(R.styleable.AutoFitRecyclerView_minColumnWidth, 0); + array.recycle(); + } + } + + public static class DefaultCallback implements Callback { + @Override public void onColumns(int columns) { } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/views/DefaultViewHolder.java b/app/src/main/java/org/wikipedia/views/DefaultViewHolder.java new file mode 100644 index 0000000..42d9d5d --- /dev/null +++ b/app/src/main/java/org/wikipedia/views/DefaultViewHolder.java @@ -0,0 +1,17 @@ +package org.wikipedia.views; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** The minimum boilerplate required by the view holder pattern for use with custom views. */ +public class DefaultViewHolder<T extends View> extends RecyclerView.ViewHolder { + public DefaultViewHolder(@NonNull T view) { + super(view); + } + + @NonNull public T getView() { + //noinspection unchecked + return (T) itemView; + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml new file mode 100644 index 0000000..f8bcbf4 --- /dev/null +++ b/app/src/main/res/layout/fragment_feed.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin"> + + <TextView + style="@style/TextAppearance.AppCompat.Large" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/activity_feed_title" /> + + <!-- TODO: [Feed] remove. --> + <Button + android:id="@+id/fragment_feed_add_card" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Add card" /> + + <org.wikipedia.feed.view.FeedView + android:id="@+id/fragment_feed_feed" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/view_feed.xml b/app/src/main/res/layout/view_feed.xml new file mode 100644 index 0000000..bcb3412 --- /dev/null +++ b/app/src/main/res/layout/view_feed.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <org.wikipedia.views.AutoFitRecyclerView + android:id="@+id/view_feed_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:minColumnWidth="@dimen/view_feed_min_column_width" /> + +</merge> \ No newline at end of file diff --git a/app/src/main/res/layout/view_feed_card.xml b/app/src/main/res/layout/view_feed_card.xml new file mode 100644 index 0000000..20fdd3e --- /dev/null +++ b/app/src/main/res/layout/view_feed_card.xml @@ -0,0 +1,4 @@ +<merge + xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- TODO: [Feed] fill in. --> +</merge> \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 20245f0..9dab66b 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -34,6 +34,10 @@ <attr name="cabEnabled" format="boolean" /> </declare-styleable> + <declare-styleable name="AutoFitRecyclerView"> + <attr name="minColumnWidth" format="dimension" /> + </declare-styleable> + <declare-styleable name="EditTextAutoSummarizePreference"> <attr name="editTextAutoSummarizePreferenceStyle" format="reference" /> <attr name="autoSummarize" format="boolean" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 04cd6f2..c12dbd9 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -87,4 +87,7 @@ <!-- Article menu bar --> <dimen name="view_article_menu_bar_button_padding">12dp</dimen> + + <!-- The Feed --> + <dimen name="view_feed_min_column_width">192dp</dimen> </resources> diff --git a/app/src/main/res/values/strings_no_translate.xml b/app/src/main/res/values/strings_no_translate.xml index a9b74d2..9822eb0 100644 --- a/app/src/main/res/values/strings_no_translate.xml +++ b/app/src/main/res/values/strings_no_translate.xml @@ -42,6 +42,8 @@ <!-- The Feed --> <!-- TODO: [Feed] move translatable strings to strings.xml. --> - <string name="nav_item_feed">Home</string> + <string name="feed">Home</string> + <string name="activity_feed_title">@string/feed</string> + <string name="nav_item_feed">@string/feed</string> <!-- /The Feed --> </resources> -- To view, visit https://gerrit.wikimedia.org/r/289792 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Icd6f10097d6b32f40f27b1409ead4ec803acb7ea Gerrit-PatchSet: 7 Gerrit-Project: apps/android/wikipedia Gerrit-Branch: master Gerrit-Owner: Niedzielski <sniedziel...@wikimedia.org> Gerrit-Reviewer: BearND <bsitzm...@wikimedia.org> Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org> Gerrit-Reviewer: Dbrant <dbr...@wikimedia.org> Gerrit-Reviewer: Intrications <mich...@intrications.com> Gerrit-Reviewer: Mholloway <mhollo...@wikimedia.org> Gerrit-Reviewer: Niedzielski <sniedziel...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits