Skip to content

Commit 1157cfe

Browse files
committed
[Catalog] Add preferences card items demo with first/middle/last/single shaped focus rings
PiperOrigin-RevId: 899593077
1 parent bda8e38 commit 1157cfe

18 files changed

Lines changed: 834 additions & 0 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.material.catalog.topappbar;
18+
19+
import android.content.Context;
20+
import android.util.AttributeSet;
21+
import android.widget.LinearLayout;
22+
import androidx.annotation.Nullable;
23+
24+
class DrawableStateLinearLayout extends LinearLayout {
25+
private int[] extraDrawableState;
26+
27+
public DrawableStateLinearLayout(Context context) {
28+
super(context);
29+
}
30+
31+
public DrawableStateLinearLayout(Context context, @Nullable AttributeSet attrs) {
32+
super(context, attrs);
33+
}
34+
35+
public DrawableStateLinearLayout(
36+
Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
37+
super(context, attrs, defStyleAttr);
38+
}
39+
40+
public DrawableStateLinearLayout(
41+
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
42+
super(context, attrs, defStyleAttr, defStyleRes);
43+
}
44+
45+
@Override
46+
protected int[] onCreateDrawableState(int extraSpace) {
47+
if (extraDrawableState == null) {
48+
return super.onCreateDrawableState(extraSpace);
49+
}
50+
return mergeDrawableStates(
51+
super.onCreateDrawableState(extraSpace + extraDrawableState.length), extraDrawableState);
52+
}
53+
54+
public void setExtraDrawableState(int[] extraDrawableState) {
55+
this.extraDrawableState = extraDrawableState;
56+
refreshDrawableState();
57+
}
58+
}

catalog/java/io/material/catalog/topappbar/TopAppBarFragment.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ public Fragment createFragment() {
8989
return new TopAppBarPreferencesFragment();
9090
}
9191
});
92+
additionalDemos.add(
93+
new Demo(R.string.cat_topappbar_preferences_card_items_demo_title) {
94+
@Override
95+
public Fragment createFragment() {
96+
return new TopAppBarPreferencesCardItemsFragment();
97+
}
98+
});
9299
additionalDemos.addAll(getCollapsingToolbarDemos());
93100
additionalDemos.add(getToolbarDemo());
94101
additionalDemos.addAll(getActionBarDemos());
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.material.catalog.topappbar;
18+
19+
import io.material.catalog.R;
20+
21+
import android.content.res.Resources;
22+
import android.os.Bundle;
23+
import androidx.appcompat.app.AppCompatActivity;
24+
import androidx.recyclerview.widget.RecyclerView;
25+
import androidx.appcompat.widget.Toolbar;
26+
import android.view.LayoutInflater;
27+
import android.view.Menu;
28+
import android.view.MenuInflater;
29+
import android.view.MenuItem;
30+
import android.view.View;
31+
import android.view.ViewGroup;
32+
import android.view.ViewGroup.MarginLayoutParams;
33+
import androidx.annotation.NonNull;
34+
import androidx.annotation.Nullable;
35+
import androidx.preference.Preference;
36+
import androidx.preference.PreferenceCategory;
37+
import androidx.preference.PreferenceFragmentCompat;
38+
import androidx.preference.PreferenceGroup;
39+
import androidx.preference.PreferenceGroupAdapter;
40+
import androidx.preference.PreferenceScreen;
41+
import androidx.preference.PreferenceViewHolder;
42+
import com.google.android.material.color.MaterialColors;
43+
import io.material.catalog.feature.DemoFragment;
44+
import io.material.catalog.feature.DemoUtils;
45+
46+
/** A fragment that displays a preferences Top App Bar demo for the Catalog app. */
47+
public class TopAppBarPreferencesCardItemsFragment extends DemoFragment {
48+
49+
@Override
50+
public void onCreate(@Nullable Bundle bundle) {
51+
super.onCreate(bundle);
52+
setHasOptionsMenu(true);
53+
}
54+
55+
@Nullable
56+
@Override
57+
public View onCreateDemoView(
58+
@NonNull LayoutInflater layoutInflater,
59+
@Nullable ViewGroup viewGroup,
60+
@Nullable Bundle bundle) {
61+
View view =
62+
layoutInflater.inflate(R.layout.cat_topappbar_preferences_fragment, viewGroup, false);
63+
64+
Toolbar toolbar = view.findViewById(R.id.toolbar);
65+
AppCompatActivity activity = (AppCompatActivity) getActivity();
66+
activity.setSupportActionBar(toolbar);
67+
68+
view.findViewById(R.id.coordinator)
69+
.setBackgroundColor(MaterialColors.getColor(view, com.google.android.material.R.attr.colorSurfaceContainerHigh));
70+
71+
getChildFragmentManager()
72+
.beginTransaction()
73+
.replace(R.id.cat_topappbar_preferences_container, new PreferencesFragment())
74+
.commit();
75+
76+
return view;
77+
}
78+
79+
@Override
80+
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
81+
menuInflater.inflate(R.menu.cat_topappbar_menu, menu);
82+
super.onCreateOptionsMenu(menu, menuInflater);
83+
}
84+
85+
@Override
86+
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
87+
return DemoUtils.showSnackbar(getActivity(), item) || super.onOptionsItemSelected(item);
88+
}
89+
90+
@Override
91+
public boolean shouldShowDefaultDemoActionBar() {
92+
return false;
93+
}
94+
95+
/** Example preferences fragment. */
96+
public static class PreferencesFragment extends PreferenceFragmentCompat {
97+
98+
@Override
99+
public void onCreate(@Nullable Bundle savedInstanceState) {
100+
requireContext().setTheme(R.style.ThemeOverlay_Preferences_CardItems);
101+
super.onCreate(savedInstanceState);
102+
}
103+
104+
@Override
105+
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @NonNull String rootKey) {
106+
setPreferencesFromResource(R.xml.cat_topappbar_card_item_preferences, rootKey);
107+
}
108+
109+
@NonNull
110+
@Override
111+
protected RecyclerView.Adapter<PreferenceViewHolder> onCreateAdapter(@NonNull PreferenceScreen preferenceScreen) {
112+
return new CardItemPreferenceGroupAdapter(preferenceScreen);
113+
}
114+
115+
@SuppressWarnings("RestrictTo")
116+
private static class CardItemPreferenceGroupAdapter extends PreferenceGroupAdapter {
117+
118+
private final int[] firstStateSet = {android.R.attr.state_first};
119+
private final int[] middleStateSet = {android.R.attr.state_middle};
120+
private final int[] lastStateSet = {android.R.attr.state_last};
121+
private final int[] singleStateSet = {android.R.attr.state_single};
122+
123+
private final int verticalSpace;
124+
private final int listBottomMargin;
125+
126+
public CardItemPreferenceGroupAdapter(@NonNull PreferenceGroup preferenceGroup) {
127+
super(preferenceGroup);
128+
129+
Resources resources = preferenceGroup.getContext().getResources();
130+
verticalSpace =
131+
resources.getDimensionPixelSize(
132+
R.dimen.cat_topappbar_preference_card_item_vertical_space);
133+
listBottomMargin =
134+
resources.getDimensionPixelSize(
135+
R.dimen.cat_topappbar_preference_card_item_list_bottom_margin);
136+
}
137+
138+
@Override
139+
public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) {
140+
super.onBindViewHolder(holder, position);
141+
142+
Preference currentItem = getItem(position);
143+
144+
if (currentItem instanceof PreferenceCategory) {
145+
return;
146+
}
147+
148+
Preference previousItem = position == 0 ? null : getItem(position - 1);
149+
Preference nextItem = position == getItemCount() - 1 ? null : getItem(position + 1);
150+
boolean first = previousItem == null || previousItem instanceof PreferenceCategory;
151+
boolean last = nextItem == null || nextItem instanceof PreferenceCategory;
152+
153+
DrawableStateLinearLayout linearLayout = (DrawableStateLinearLayout) holder.itemView;
154+
MarginLayoutParams layoutParams = (MarginLayoutParams) linearLayout.getLayoutParams();
155+
156+
if (first && last) {
157+
layoutParams.topMargin = 0;
158+
layoutParams.bottomMargin = 0;
159+
linearLayout.setExtraDrawableState(singleStateSet);
160+
} else if (first) {
161+
layoutParams.topMargin = 0;
162+
layoutParams.bottomMargin = verticalSpace;
163+
linearLayout.setExtraDrawableState(firstStateSet);
164+
} else if (last) {
165+
layoutParams.topMargin = verticalSpace;
166+
layoutParams.bottomMargin = 0;
167+
linearLayout.setExtraDrawableState(lastStateSet);
168+
} else {
169+
layoutParams.topMargin = verticalSpace;
170+
layoutParams.bottomMargin = verticalSpace;
171+
linearLayout.setExtraDrawableState(middleStateSet);
172+
}
173+
174+
if (nextItem == null) {
175+
layoutParams.bottomMargin = listBottomMargin;
176+
}
177+
}
178+
}
179+
}
180+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2026 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
https://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:app="http://schemas.android.com/apk/res-auto"
19+
android:color="?android:colorControlHighlight"
20+
android:paddingStart="32dp"
21+
android:paddingEnd="32dp">
22+
<item
23+
android:start="?android:attr/listPreferredItemPaddingStart"
24+
android:end="?android:attr/listPreferredItemPaddingEnd">
25+
<com.google.android.material.focus.FocusRingDrawable
26+
app:focusRingsShapeAppearance="@style/ShapeAppearance.Preference.First">
27+
<shape android:shape="rectangle">
28+
<solid
29+
android:color="?attr/colorSurfaceBright" />
30+
<corners
31+
android:topLeftRadius="@dimen/cat_topappbar_preference_card_item_radius_large"
32+
android:bottomLeftRadius="@dimen/cat_topappbar_preference_card_item_radius_small"
33+
android:topRightRadius="@dimen/cat_topappbar_preference_card_item_radius_large"
34+
android:bottomRightRadius="@dimen/cat_topappbar_preference_card_item_radius_small" />
35+
</shape>
36+
</com.google.android.material.focus.FocusRingDrawable>
37+
</item>
38+
</ripple>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2026 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
https://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:app="http://schemas.android.com/apk/res-auto"
19+
android:color="?android:colorControlHighlight"
20+
android:paddingStart="32dp"
21+
android:paddingEnd="32dp">
22+
<item
23+
android:start="?android:attr/listPreferredItemPaddingStart"
24+
android:end="?android:attr/listPreferredItemPaddingEnd">
25+
<com.google.android.material.focus.FocusRingDrawable
26+
app:focusRingsShapeAppearance="@style/ShapeAppearance.Preference.Last">
27+
<shape android:shape="rectangle">
28+
<solid
29+
android:color="?attr/colorSurfaceBright" />
30+
<corners
31+
android:topLeftRadius="@dimen/cat_topappbar_preference_card_item_radius_small"
32+
android:bottomLeftRadius="@dimen/cat_topappbar_preference_card_item_radius_large"
33+
android:topRightRadius="@dimen/cat_topappbar_preference_card_item_radius_small"
34+
android:bottomRightRadius="@dimen/cat_topappbar_preference_card_item_radius_large" />
35+
</shape>
36+
</com.google.android.material.focus.FocusRingDrawable>
37+
</item>
38+
</ripple>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2026 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
https://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
20+
android:color="?android:colorControlHighlight"
21+
android:paddingStart="32dp"
22+
android:paddingEnd="32dp">
23+
<item
24+
android:start="?android:attr/listPreferredItemPaddingStart"
25+
android:end="?android:attr/listPreferredItemPaddingEnd">
26+
<com.google.android.material.focus.FocusRingDrawable
27+
app:focusRingsShapeAppearance="@style/ShapeAppearance.Preference.Middle">
28+
<shape android:shape="rectangle">
29+
<solid
30+
android:color="?attr/colorSurfaceBright" />
31+
<corners
32+
android:radius="@dimen/cat_topappbar_preference_card_item_radius_small" />
33+
</shape>
34+
</com.google.android.material.focus.FocusRingDrawable>
35+
</item>
36+
</ripple>

0 commit comments

Comments
 (0)