Skip to content

Commit 549fe19

Browse files
afohrmandrchen
authored andcommitted
[Catalog] Added search/filter functionality to MDC Catalog.
This speeds up our development process and enhances the user experience of our catalog. Also added a bottom line to the last row of boxes in the table of contents grid in order to avoid this visual breakage when the grid does not fill up the screen: screenm/9V8HPkufqM9BkVc.png. Previously, GridDividerDecoration avoided drawing the bottom line of the grid in the last row of the grid, but now that there can be less boxes than the screen size it's important to handle that case. PiperOrigin-RevId: 465643774
1 parent f3a5f2f commit 549fe19

File tree

11 files changed

+256
-86
lines changed

11 files changed

+256
-86
lines changed

catalog/java/io/material/catalog/application/theme/res/values/ids.xml

Lines changed: 0 additions & 21 deletions
This file was deleted.

catalog/java/io/material/catalog/application/theme/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
<resources>
1818
<string name="toolbar_navigation_close_description">Close demo</string>
1919
<string name="cat_choose_preferences_description">Open preferences dialog</string>
20+
<string name="cat_search_description">Open the search bar</string>
2021
</resources>

catalog/java/io/material/catalog/application/theme/res/values/styles.xml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
-->
17-
<resources xmlns:tools="http://schemas.android.com/tools">
17+
<resources>
1818

1919
<!-- For the catalog we know that toolbars will always be on top of a "surface" color,
2020
so it's better for them to have no background, especially because it's common for
@@ -27,16 +27,9 @@
2727
</style>
2828

2929
<style name="Widget.Catalog.TocButton" parent="Widget.AppCompat.ImageButton">
30-
<item name="android:layout_width">@dimen/mtrl_min_touch_target_size</item>
31-
<item name="android:layout_height">@dimen/mtrl_min_touch_target_size</item>
30+
<item name="android:minWidth">@dimen/mtrl_min_touch_target_size</item>
31+
<item name="android:minHeight">@dimen/mtrl_min_touch_target_size</item>
3232
<item name="android:background">?attr/actionBarItemBackground</item>
3333
</style>
3434

35-
<style name="Widget.Catalog.ChoosePreferencesButton" parent="Widget.Catalog.TocButton">
36-
<item name="android:id">@id/cat_toc_preferences_button</item>
37-
<item name="android:layout_gravity">center_vertical|end</item>
38-
<item name="srcCompat">@drawable/ic_settings_24px</item>
39-
<item name="android:contentDescription">@string/cat_choose_preferences_description</item>
40-
</style>
41-
4235
</resources>

catalog/java/io/material/catalog/tableofcontents/GridDividerDecoration.java

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* will only draw dividers that are internal to the grid, meaning it will not draw lines for the
3737
* outermost left, top, right, or bottom edges.
3838
*/
39-
public class GridDividerDecoration extends RecyclerView.ItemDecoration {
39+
public final class GridDividerDecoration extends RecyclerView.ItemDecoration {
4040

4141
private final int spanCount;
4242
private final Paint dividerPaint;
@@ -58,16 +58,10 @@ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
5858
}
5959

6060
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
61-
final int itemCount = parent.getAdapter().getItemCount();
6261
final int childCount = parent.getChildCount();
63-
final int lastRowChildCount = getLastRowChildCount(itemCount);
6462
for (int i = 0; i < childCount; i++) {
6563
final View child = parent.getChildAt(i);
6664

67-
if (isChildInLastRow(parent, child, itemCount, lastRowChildCount)) {
68-
continue;
69-
}
70-
7165
parent.getDecoratedBoundsWithMargins(child, bounds);
7266
final int y = bounds.bottom;
7367
final int startX = bounds.left;
@@ -95,16 +89,6 @@ private void drawVertical(Canvas canvas, RecyclerView parent) {
9589
}
9690
}
9791

98-
private int getLastRowChildCount(int itemCount) {
99-
final int gridChildRemainder = itemCount % spanCount;
100-
return gridChildRemainder == 0 ? spanCount : gridChildRemainder;
101-
}
102-
103-
private boolean isChildInLastRow(
104-
RecyclerView parent, View child, int itemCount, int lastRowChildCount) {
105-
return parent.getChildAdapterPosition(child) >= itemCount - lastRowChildCount;
106-
}
107-
10892
private boolean isChildInLastColumn(RecyclerView parent, View child) {
10993
return parent.getChildAdapterPosition(child) % spanCount == spanCount - 1;
11094
}

catalog/java/io/material/catalog/tableofcontents/TocAdapter.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,57 @@
1919
import androidx.fragment.app.FragmentActivity;
2020
import androidx.recyclerview.widget.RecyclerView.Adapter;
2121
import android.view.ViewGroup;
22+
import android.widget.Filter;
23+
import android.widget.Filterable;
24+
import com.google.common.collect.ImmutableList;
2225
import io.material.catalog.feature.FeatureDemo;
26+
import java.util.ArrayList;
27+
import java.util.Collection;
2328
import java.util.List;
29+
import java.util.Locale;
2430

2531
/** Handles individual items in the catalog table of contents. */
26-
class TocAdapter extends Adapter<TocViewHolder> {
32+
final class TocAdapter extends Adapter<TocViewHolder> implements Filterable {
2733

2834
private final FragmentActivity activity;
2935
private final List<FeatureDemo> featureDemos;
36+
private final ImmutableList<FeatureDemo> featureDemosAll;
37+
38+
private final Filter featureDemoFilter =
39+
new Filter() {
40+
@Override
41+
protected FilterResults performFiltering(CharSequence constraint) {
42+
List<FeatureDemo> filteredList = new ArrayList<>();
43+
44+
if (constraint.length() == 0) {
45+
filteredList.addAll(featureDemosAll);
46+
} else {
47+
for (FeatureDemo featureDemo : featureDemosAll) {
48+
if (activity
49+
.getString(featureDemo.getTitleResId())
50+
.toLowerCase(Locale.ROOT)
51+
.contains(constraint.toString().toLowerCase(Locale.ROOT))) {
52+
filteredList.add(featureDemo);
53+
}
54+
}
55+
}
56+
FilterResults filterResults = new FilterResults();
57+
filterResults.values = filteredList;
58+
return filterResults;
59+
}
60+
61+
@Override
62+
protected void publishResults(CharSequence constraint, FilterResults filterResults) {
63+
featureDemos.clear();
64+
featureDemos.addAll((Collection<? extends FeatureDemo>) filterResults.values);
65+
notifyDataSetChanged();
66+
}
67+
};
3068

3169
TocAdapter(FragmentActivity activity, List<FeatureDemo> featureDemos) {
3270
this.activity = activity;
3371
this.featureDemos = featureDemos;
72+
this.featureDemosAll = ImmutableList.copyOf(featureDemos);
3473
}
3574

3675
@Override
@@ -47,4 +86,9 @@ public void onBindViewHolder(TocViewHolder tocViewHolder, int i) {
4786
public int getItemCount() {
4887
return featureDemos.size();
4988
}
89+
90+
@Override
91+
public Filter getFilter() {
92+
return featureDemoFilter;
93+
}
5094
}

catalog/java/io/material/catalog/tableofcontents/TocFragment.java

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,33 @@
1818

1919
import io.material.catalog.R;
2020

21-
import android.annotation.SuppressLint;
2221
import android.os.Build.VERSION;
2322
import android.os.Build.VERSION_CODES;
2423
import android.os.Bundle;
2524
import androidx.fragment.app.Fragment;
2625
import androidx.appcompat.app.AppCompatActivity;
2726
import androidx.recyclerview.widget.GridLayoutManager;
2827
import androidx.recyclerview.widget.RecyclerView;
28+
import android.support.v7.widget.SearchView;
29+
import android.support.v7.widget.SearchView.OnQueryTextListener;
2930
import androidx.appcompat.widget.Toolbar;
3031
import android.util.DisplayMetrics;
3132
import android.view.LayoutInflater;
3233
import android.view.View;
3334
import android.view.ViewGroup;
3435
import android.widget.ImageButton;
36+
import androidx.annotation.Dimension;
37+
import androidx.annotation.NonNull;
3538
import androidx.annotation.Nullable;
39+
import androidx.constraintlayout.widget.ConstraintLayout;
40+
import androidx.constraintlayout.widget.ConstraintSet;
3641
import androidx.core.content.ContextCompat;
3742
import androidx.core.math.MathUtils;
3843
import androidx.core.view.ViewCompat;
44+
import androidx.transition.Transition;
45+
import androidx.transition.TransitionManager;
3946
import com.google.android.material.appbar.AppBarLayout;
47+
import com.google.android.material.transition.MaterialSharedAxis;
4048
import dagger.android.support.DaggerFragment;
4149
import io.material.catalog.feature.FeatureDemo;
4250
import io.material.catalog.feature.FeatureDemoUtils;
@@ -54,13 +62,21 @@ public class TocFragment extends DaggerFragment {
5462
private static final int GRID_SPAN_COUNT_MIN = 1;
5563
private static final int GRID_SPAN_COUNT_MAX = 4;
5664

65+
@Dimension(unit = Dimension.DP)
66+
private static final int CATALOG_NARROW_SCREEN_SIZE_CUTOFF = 350;
67+
5768
@Inject Set<FeatureDemo> featureDemos;
5869
@Inject TocResourceProvider tocResourceProvider;
5970

6071
private AppBarLayout appBarLayout;
6172
private View gridTopDivider;
62-
private RecyclerView recyclerView;
73+
private ConstraintLayout headerContainer;
6374
private ImageButton preferencesButton;
75+
private SearchView searchView;
76+
private ImageButton searchButton;
77+
private TocAdapter tocAdapter;
78+
private Transition openSearchViewTransition;
79+
private Transition closeSearchViewTransition;
6480

6581
@Override
6682
public void onCreate(@Nullable Bundle bundle) {
@@ -71,9 +87,9 @@ public void onCreate(@Nullable Bundle bundle) {
7187
}
7288
}
7389

74-
@SuppressLint("MissingInflatedId")
7590
@Nullable
7691
@Override
92+
@SuppressWarnings("MissingInflatedId")
7793
public View onCreateView(
7894
LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
7995
View view =
@@ -85,12 +101,18 @@ public View onCreateView(
85101
activity.getSupportActionBar().setDisplayShowTitleEnabled(false);
86102

87103
ViewGroup content = view.findViewById(R.id.content);
88-
View.inflate(getContext(), tocResourceProvider.getHeaderContent(), content);
104+
View.inflate(getContext(), R.layout.cat_toc_header, content);
89105

90106
appBarLayout = view.findViewById(R.id.cat_toc_app_bar_layout);
91107
gridTopDivider = view.findViewById(R.id.cat_toc_grid_top_divider);
92-
recyclerView = view.findViewById(R.id.cat_toc_grid);
108+
headerContainer = view.findViewById(R.id.cat_toc_header_container);
109+
RecyclerView recyclerView = view.findViewById(R.id.cat_toc_grid);
93110
preferencesButton = view.findViewById(R.id.cat_toc_preferences_button);
111+
searchView = view.findViewById(R.id.cat_toc_search_view);
112+
searchButton = view.findViewById(R.id.cat_toc_search_button);
113+
114+
// Inflate logo into the header container.
115+
View.inflate(getContext(), tocResourceProvider.getLogoLayout(), headerContainer);
94116

95117
ViewCompat.setOnApplyWindowInsetsListener(
96118
view,
@@ -126,14 +148,46 @@ public View onCreateView(
126148
getContext().getString(feature1.getTitleResId()),
127149
getContext().getString(feature2.getTitleResId())));
128150

129-
TocAdapter tocAdapter = new TocAdapter(getActivity(), featureList);
151+
tocAdapter = new TocAdapter(getActivity(), featureList);
130152
recyclerView.setAdapter(tocAdapter);
131153

154+
adjustLogoConstraintsForNarrowScreenWidths();
155+
132156
initPreferencesButton();
157+
initSearchButton();
158+
initSearchView();
159+
initSearchViewTransitions();
133160

134161
return view;
135162
}
136163

164+
private void adjustLogoConstraintsForNarrowScreenWidths() {
165+
// Adjust logo constraints so that the logo text does not overlap with the search image button.
166+
if (getScreenWidth() < CATALOG_NARROW_SCREEN_SIZE_CUTOFF) {
167+
ConstraintSet narrowHeaderConstraintSet = createNarrowHeaderConstraintSet(headerContainer);
168+
narrowHeaderConstraintSet.applyTo(headerContainer);
169+
}
170+
}
171+
172+
@Dimension(unit = Dimension.DP)
173+
private int getScreenWidth() {
174+
return getResources().getConfiguration().screenWidthDp;
175+
}
176+
177+
private ConstraintSet createNarrowHeaderConstraintSet(ConstraintLayout constraintLayout) {
178+
ConstraintSet constraintSet = new ConstraintSet();
179+
constraintSet.clone(constraintLayout);
180+
constraintSet.connect(
181+
R.id.header_logo, ConstraintSet.END, R.id.cat_toc_search_button, ConstraintSet.START);
182+
// Add a bit of space on the start side of the logo to compensate for the extra 12dp of space
183+
// the search icon button includes on each side because of its 48dp minimum touch target width.
184+
constraintSet.setMargin(
185+
R.id.header_logo,
186+
ConstraintSet.START,
187+
getResources().getDimensionPixelOffset(R.dimen.cat_toc_header_additional_start_margin));
188+
return constraintSet;
189+
}
190+
137191
private void addGridTopDividerVisibilityListener() {
138192
appBarLayout.addOnOffsetChangedListener(
139193
(appBarLayout, verticalOffset) -> {
@@ -157,8 +211,76 @@ private int calculateGridSpanCount() {
157211

158212
private void initPreferencesButton() {
159213
preferencesButton.setOnClickListener(
160-
v -> new CatalogPreferencesDialogFragment().show(
161-
getParentFragmentManager(), "preferences-screen"));
214+
v ->
215+
new CatalogPreferencesDialogFragment()
216+
.show(getParentFragmentManager(), "preferences-screen"));
217+
}
218+
219+
private void initSearchButton() {
220+
searchButton.setOnClickListener(v -> openSearchView());
221+
}
222+
223+
private void initSearchView() {
224+
searchView.setOnClickListener(v -> closeSearchView());
225+
226+
searchView.setOnQueryTextListener(
227+
new OnQueryTextListener() {
228+
@Override
229+
public boolean onQueryTextSubmit(String query) {
230+
return false;
231+
}
232+
233+
@Override
234+
public boolean onQueryTextChange(String newText) {
235+
tocAdapter.getFilter().filter(newText);
236+
return false;
237+
}
238+
});
239+
}
240+
241+
private void initSearchViewTransitions() {
242+
openSearchViewTransition = createSearchViewTransition(true);
243+
closeSearchViewTransition = createSearchViewTransition(false);
244+
}
245+
246+
private void openSearchView() {
247+
TransitionManager.beginDelayedTransition(headerContainer, openSearchViewTransition);
248+
249+
headerContainer.setVisibility(View.GONE);
250+
searchView.setVisibility(View.VISIBLE);
251+
252+
searchView.requestFocus();
253+
}
254+
255+
private void closeSearchView() {
256+
TransitionManager.beginDelayedTransition(headerContainer, closeSearchViewTransition);
257+
258+
headerContainer.setVisibility(View.VISIBLE);
259+
searchView.setVisibility(View.GONE);
260+
261+
clearSearchView();
262+
}
263+
264+
@NonNull
265+
private MaterialSharedAxis createSearchViewTransition(boolean entering) {
266+
MaterialSharedAxis sharedAxisTransition =
267+
new MaterialSharedAxis(MaterialSharedAxis.X, entering);
268+
269+
sharedAxisTransition.addTarget(headerContainer);
270+
sharedAxisTransition.addTarget(searchView);
271+
return sharedAxisTransition;
272+
}
273+
274+
@Override
275+
public void onPause() {
276+
super.onPause();
277+
clearSearchView();
278+
}
279+
280+
private void clearSearchView() {
281+
if (searchView != null) {
282+
searchView.setQuery("", true);
283+
}
162284
}
163285

164286
private void startDefaultDemoLandingIfNeeded() {

catalog/java/io/material/catalog/tableofcontents/TocResourceProvider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020

2121
import androidx.annotation.LayoutRes;
2222

23-
/** A helper class that facilitates overriding of certain resources in the Catalog app. */
23+
/** A helper class that facilitates resource overrides in the Catalog app. */
2424
public class TocResourceProvider {
2525

2626
@LayoutRes
27-
protected int getHeaderContent() {
28-
return R.layout.cat_toc_header;
27+
protected int getLogoLayout() {
28+
return R.layout.cat_toc_logo;
2929
}
3030
}

0 commit comments

Comments
 (0)