Skip to content

Commit cd83f64

Browse files
Refactor preferences and Android Studio lessons to use Material 3 components
This commit updates the styling of preference items and Android Studio lesson items to align with Material 3 guidelines. Specific changes include: - Renamed layout files: - `item_android_studio_lesson.xml` to `item_preference.xml` - `item_android_studio_category.xml` to `item_preference_category.xml` - Updated `AndroidStudioFragment.java`: - Inflates `item_preference.xml` and `item_preference_category.xml` directly instead of using data binding for these layouts. - Updated `LessonHolder` and `CategoryHolder` to use standard `findViewById` with new Android system IDs for title, summary, icon, etc. - `LessonHolder` now inflates `item_preference_widget_open_in_new.xml` into its `widgetFrame`. - Handles visibility and clickability of the external link button more robustly. - Updated `preferences_settings.xml`: - Assigns `item_preference_category.xml` as the layout for `PreferenceCategory`. - Assigns `item_preference.xml` as the layout for individual `Preference` and `ListPreference` items. - Assigns `widget_preference_switch.xml` as the `widgetLayout` for `SwitchPreferenceCompat`. - Assigns `item_preference_widget_open_notifications.xml` and `item_preference_widget_open_in_new.xml` as `widgetLayout` for specific preferences. - Sets `app:iconSpaceReserved="false"` for preferences to allow icon visibility to be controlled by the presence of an icon. - Created `SettingsFragment.java`: - Implements custom styling for preference items within a `RecyclerView`. - Adds `PreferenceSpacingDecoration` to manage spacing between preference items. - Dynamically updates the corner radius of `MaterialCardView` for preferences to create a grouped appearance within categories (first item has rounded top corners, last item has rounded bottom corners). - Synchronizes the visibility of icon frames and widget frames based on the visibility of their content. - Added new layout files: - `item_preference_widget_open_notifications.xml`: Layout for an icon button to open notification settings. - `item_preference_widget_open_in_new.xml`: Layout for an icon button to open links in a new window/browser. - Added `ic_arrow_outward.xml` drawable. - Modified `widget_preference_switch.xml`: - Changed ID to `@android:id/switch_widget`. - Set `clickable` and `focusable` to true. - Added `preference_list_vertical_padding` dimension in `dimens.xml`. - Added padding to the bottom of `activity_help.xml`'s ScrollView content.
1 parent 7810403 commit cd83f64

File tree

10 files changed

+425
-55
lines changed

10 files changed

+425
-55
lines changed

app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/AndroidStudioFragment.java

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
import android.view.MenuItem;
1414
import android.view.View;
1515
import android.view.ViewGroup;
16+
import android.widget.FrameLayout;
1617

1718
import androidx.annotation.NonNull;
1819
import androidx.annotation.Nullable;
19-
import androidx.appcompat.widget.AppCompatImageView;
2020
import androidx.appcompat.widget.SearchView;
2121
import androidx.core.view.MenuHost;
2222
import androidx.core.view.MenuProvider;
@@ -30,12 +30,11 @@
3030
import com.d4rk.androidtutorials.java.ads.AdUtils;
3131
import com.d4rk.androidtutorials.java.ads.views.NativeAdBannerView;
3232
import com.d4rk.androidtutorials.java.databinding.FragmentAndroidStudioBinding;
33-
import com.d4rk.androidtutorials.java.databinding.ItemAndroidStudioCategoryBinding;
34-
import com.d4rk.androidtutorials.java.databinding.ItemAndroidStudioLessonBinding;
3533
import com.google.android.gms.ads.AdListener;
3634
import com.google.android.gms.ads.LoadAdError;
3735
import com.google.android.material.button.MaterialButton;
3836
import com.google.android.material.card.MaterialCardView;
37+
import com.google.android.material.imageview.ShapeableImageView;
3938
import com.google.android.material.shape.CornerFamily;
4039
import com.google.android.material.shape.ShapeAppearanceModel;
4140
import com.google.android.material.textview.MaterialTextView;
@@ -375,13 +374,13 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
375374
adView.setNativeAdLayout(R.layout.ad_android_studio_list);
376375
return new AdHolder(adView);
377376
} else if (viewType == TYPE_CATEGORY) {
378-
ItemAndroidStudioCategoryBinding binding = ItemAndroidStudioCategoryBinding.inflate(
379-
LayoutInflater.from(parent.getContext()), parent, false);
380-
return new CategoryHolder(binding);
377+
View view = LayoutInflater.from(parent.getContext())
378+
.inflate(R.layout.item_preference_category, parent, false);
379+
return new CategoryHolder(view);
381380
} else {
382-
ItemAndroidStudioLessonBinding binding = ItemAndroidStudioLessonBinding.inflate(
383-
LayoutInflater.from(parent.getContext()), parent, false);
384-
return new LessonHolder(binding);
381+
View view = LayoutInflater.from(parent.getContext())
382+
.inflate(R.layout.item_preference, parent, false);
383+
return new LessonHolder(view);
385384
}
386385
}
387386

@@ -425,26 +424,38 @@ static class AdHolder extends RecyclerView.ViewHolder {
425424

426425
static class LessonHolder extends RecyclerView.ViewHolder {
427426
final MaterialCardView card;
428-
final AppCompatImageView icon;
427+
final ShapeableImageView icon;
429428
final MaterialTextView title;
430429
final MaterialTextView summary;
430+
final FrameLayout widgetFrame;
431431
final MaterialButton externalButton;
432+
final FrameLayout iconFrame;
432433

433-
LessonHolder(@NonNull ItemAndroidStudioLessonBinding binding) {
434-
super(binding.getRoot());
435-
card = binding.lessonCard;
436-
icon = binding.lessonIcon;
437-
title = binding.lessonTitle;
438-
summary = binding.lessonSummary;
439-
externalButton = binding.lessonExternalIcon;
434+
LessonHolder(@NonNull View itemView) {
435+
super(itemView);
436+
card = (MaterialCardView) itemView;
437+
icon = itemView.findViewById(android.R.id.icon);
438+
title = itemView.findViewById(android.R.id.title);
439+
summary = itemView.findViewById(android.R.id.summary);
440+
widgetFrame = itemView.findViewById(android.R.id.widget_frame);
441+
iconFrame = itemView.findViewById(android.R.id.icon_frame);
442+
LayoutInflater.from(itemView.getContext())
443+
.inflate(R.layout.item_preference_widget_open_in_new, widgetFrame, true);
444+
externalButton = widgetFrame.findViewById(R.id.open_in_new);
440445
}
441446

442447
void bind(Lesson lesson, boolean first, boolean last) {
443448
if (lesson.iconRes != 0) {
444449
icon.setImageResource(lesson.iconRes);
445450
icon.setVisibility(View.VISIBLE);
451+
if (iconFrame != null) {
452+
iconFrame.setVisibility(View.VISIBLE);
453+
}
446454
} else {
447455
icon.setVisibility(View.GONE);
456+
if (iconFrame != null) {
457+
iconFrame.setVisibility(View.GONE);
458+
}
448459
}
449460
title.setText(lesson.title);
450461
if (lesson.summary != null) {
@@ -454,10 +465,19 @@ void bind(Lesson lesson, boolean first, boolean last) {
454465
summary.setVisibility(View.GONE);
455466
}
456467
boolean showExternalButton = lesson.opensInBrowser && lesson.intent != null;
468+
widgetFrame.setVisibility(showExternalButton ? View.VISIBLE : View.GONE);
457469
externalButton.setVisibility(showExternalButton ? View.VISIBLE : View.GONE);
470+
externalButton.setEnabled(showExternalButton);
471+
externalButton.setFocusableInTouchMode(showExternalButton);
458472
if (showExternalButton) {
473+
externalButton.setClickable(true);
474+
externalButton.setFocusable(true);
475+
externalButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
459476
externalButton.setOnClickListener(v -> v.getContext().startActivity(lesson.intent));
460477
} else {
478+
externalButton.setClickable(false);
479+
externalButton.setFocusable(false);
480+
externalButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
461481
externalButton.setOnClickListener(null);
462482
}
463483
itemView.setOnClickListener(v -> {
@@ -485,9 +505,9 @@ private void applyCorners(boolean first, boolean last) {
485505
static class CategoryHolder extends RecyclerView.ViewHolder {
486506
final MaterialTextView title;
487507

488-
CategoryHolder(@NonNull ItemAndroidStudioCategoryBinding binding) {
489-
super(binding.getRoot());
490-
title = binding.categoryTitle;
508+
CategoryHolder(@NonNull View itemView) {
509+
super(itemView);
510+
title = itemView.findViewById(android.R.id.title);
491511
}
492512

493513
void bind(Category category) {

app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/settings/SettingsFragment.java

Lines changed: 227 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,45 @@
44
import android.content.ClipboardManager;
55
import android.content.Context;
66
import android.content.Intent;
7+
import android.graphics.Rect;
78
import android.net.Uri;
89
import android.os.Build;
910
import android.os.Bundle;
1011
import android.provider.Settings;
1112
import android.text.TextUtils;
13+
import android.util.TypedValue;
14+
import android.view.View;
15+
import android.view.ViewTreeObserver;
16+
import android.view.ViewGroup;
1217
import android.widget.Toast;
1318

19+
import androidx.annotation.NonNull;
20+
import androidx.annotation.Nullable;
1421
import androidx.preference.ListPreference;
1522
import androidx.preference.Preference;
23+
import androidx.preference.PreferenceCategory;
1624
import androidx.preference.PreferenceFragmentCompat;
25+
import androidx.preference.PreferenceGroupAdapter;
1726
import androidx.preference.SwitchPreferenceCompat;
27+
import androidx.recyclerview.widget.RecyclerView;
1828

1929
import com.d4rk.androidtutorials.java.R;
2030
import com.d4rk.androidtutorials.java.ui.components.dialogs.RequireRestartDialog;
2131
import com.d4rk.androidtutorials.java.utils.OpenSourceLicensesUtils;
32+
import com.google.android.material.card.MaterialCardView;
33+
import com.google.android.material.shape.CornerFamily;
34+
import com.google.android.material.shape.ShapeAppearanceModel;
2235

2336
public class SettingsFragment extends PreferenceFragmentCompat {
37+
@Nullable
38+
private RecyclerView settingsList;
39+
@Nullable
40+
private RecyclerView.AdapterDataObserver preferenceAdapterObserver;
41+
@Nullable
42+
private RecyclerView.OnChildAttachStateChangeListener preferenceChildAttachListener;
43+
@Nullable
44+
private ViewTreeObserver.OnGlobalLayoutListener preferenceLayoutListener;
45+
2446
@Override
2547
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
2648
setPreferencesFromResource(R.xml.preferences_settings, rootKey);
@@ -96,4 +118,208 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
96118
});
97119
}
98120
}
99-
}
121+
122+
@Override
123+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
124+
super.onViewCreated(view, savedInstanceState);
125+
setDivider(null);
126+
setDividerHeight(0);
127+
settingsList = getListView();
128+
RecyclerView listView = settingsList;
129+
int verticalPadding = getResources().getDimensionPixelSize(R.dimen.preference_list_vertical_padding);
130+
listView.setPadding(listView.getPaddingLeft(), verticalPadding,
131+
listView.getPaddingRight(), verticalPadding);
132+
listView.setClipToPadding(false);
133+
listView.addItemDecoration(new PreferenceSpacingDecoration(requireContext()));
134+
setupPreferenceCardStyling(listView);
135+
}
136+
137+
@Override
138+
public void onDestroyView() {
139+
if (settingsList != null) {
140+
if (settingsList.getAdapter() != null && preferenceAdapterObserver != null) {
141+
settingsList.getAdapter().unregisterAdapterDataObserver(preferenceAdapterObserver);
142+
}
143+
if (preferenceChildAttachListener != null) {
144+
settingsList.removeOnChildAttachStateChangeListener(preferenceChildAttachListener);
145+
}
146+
if (preferenceLayoutListener != null) {
147+
ViewTreeObserver observer = settingsList.getViewTreeObserver();
148+
if (observer.isAlive()) {
149+
observer.removeOnGlobalLayoutListener(preferenceLayoutListener);
150+
}
151+
}
152+
}
153+
preferenceAdapterObserver = null;
154+
preferenceChildAttachListener = null;
155+
preferenceLayoutListener = null;
156+
settingsList = null;
157+
super.onDestroyView();
158+
}
159+
160+
private void setupPreferenceCardStyling(@NonNull RecyclerView listView) {
161+
Runnable updateRunnable = () -> updatePreferenceCardShapes(listView);
162+
RecyclerView.Adapter<?> adapter = listView.getAdapter();
163+
if (adapter != null) {
164+
preferenceAdapterObserver = new RecyclerView.AdapterDataObserver() {
165+
@Override
166+
public void onChanged() {
167+
updateRunnable.run();
168+
}
169+
170+
@Override
171+
public void onItemRangeInserted(int positionStart, int itemCount) {
172+
updateRunnable.run();
173+
}
174+
175+
@Override
176+
public void onItemRangeRemoved(int positionStart, int itemCount) {
177+
updateRunnable.run();
178+
}
179+
180+
@Override
181+
public void onItemRangeChanged(int positionStart, int itemCount) {
182+
updateRunnable.run();
183+
}
184+
185+
@Override
186+
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
187+
updateRunnable.run();
188+
}
189+
};
190+
adapter.registerAdapterDataObserver(preferenceAdapterObserver);
191+
}
192+
preferenceChildAttachListener = new RecyclerView.OnChildAttachStateChangeListener() {
193+
@Override
194+
public void onChildViewAttachedToWindow(@NonNull View view) {
195+
updateRunnable.run();
196+
}
197+
198+
@Override
199+
public void onChildViewDetachedFromWindow(@NonNull View view) {
200+
updateRunnable.run();
201+
}
202+
};
203+
listView.addOnChildAttachStateChangeListener(preferenceChildAttachListener);
204+
preferenceLayoutListener = updateRunnable::run;
205+
listView.getViewTreeObserver().addOnGlobalLayoutListener(preferenceLayoutListener);
206+
listView.post(updateRunnable);
207+
}
208+
209+
private void updatePreferenceCardShapes(@NonNull RecyclerView listView) {
210+
RecyclerView.Adapter<?> adapter = listView.getAdapter();
211+
if (!(adapter instanceof PreferenceGroupAdapter)) {
212+
return;
213+
}
214+
PreferenceGroupAdapter preferenceAdapter = (PreferenceGroupAdapter) adapter;
215+
int itemCount = preferenceAdapter.getItemCount();
216+
for (int position = 0; position < itemCount; position++) {
217+
Preference preference = preferenceAdapter.getItem(position);
218+
if (preference instanceof PreferenceCategory) {
219+
continue;
220+
}
221+
RecyclerView.ViewHolder holder = listView.findViewHolderForAdapterPosition(position);
222+
if (holder == null) {
223+
continue;
224+
}
225+
View itemView = holder.itemView;
226+
if (!(itemView instanceof MaterialCardView)) {
227+
continue;
228+
}
229+
boolean first = isFirstPreferenceInSection(preferenceAdapter, position);
230+
boolean last = isLastPreferenceInSection(preferenceAdapter, position);
231+
applyRoundedCorners((MaterialCardView) itemView, first, last);
232+
syncAccessoryVisibility(itemView);
233+
}
234+
}
235+
236+
private boolean isFirstPreferenceInSection(@NonNull PreferenceGroupAdapter adapter, int position) {
237+
for (int index = position - 1; index >= 0; index--) {
238+
Preference previous = adapter.getItem(index);
239+
if (!previous.isVisible()) {
240+
continue;
241+
}
242+
if (previous instanceof PreferenceCategory) {
243+
return true;
244+
}
245+
return false;
246+
}
247+
return true;
248+
}
249+
250+
private boolean isLastPreferenceInSection(@NonNull PreferenceGroupAdapter adapter, int position) {
251+
int itemCount = adapter.getItemCount();
252+
for (int index = position + 1; index < itemCount; index++) {
253+
Preference next = adapter.getItem(index);
254+
if (!next.isVisible()) {
255+
continue;
256+
}
257+
if (next instanceof PreferenceCategory) {
258+
return true;
259+
}
260+
return false;
261+
}
262+
return true;
263+
}
264+
265+
private void applyRoundedCorners(@NonNull MaterialCardView card, boolean first, boolean last) {
266+
float dp4 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4,
267+
card.getResources().getDisplayMetrics());
268+
float dp24 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24,
269+
card.getResources().getDisplayMetrics());
270+
ShapeAppearanceModel shapeAppearanceModel = card.getShapeAppearanceModel().toBuilder()
271+
.setTopLeftCorner(CornerFamily.ROUNDED, first ? dp24 : dp4)
272+
.setTopRightCorner(CornerFamily.ROUNDED, first ? dp24 : dp4)
273+
.setBottomLeftCorner(CornerFamily.ROUNDED, last ? dp24 : dp4)
274+
.setBottomRightCorner(CornerFamily.ROUNDED, last ? dp24 : dp4)
275+
.build();
276+
card.setShapeAppearanceModel(shapeAppearanceModel);
277+
}
278+
279+
private void syncAccessoryVisibility(@NonNull View itemView) {
280+
View icon = itemView.findViewById(android.R.id.icon);
281+
View iconFrame = itemView.findViewById(android.R.id.icon_frame);
282+
if (iconFrame != null) {
283+
boolean showIcon = icon != null && icon.getVisibility() == View.VISIBLE;
284+
iconFrame.setVisibility(showIcon ? View.VISIBLE : View.GONE);
285+
}
286+
View widgetFrame = itemView.findViewById(android.R.id.widget_frame);
287+
if (widgetFrame instanceof ViewGroup) {
288+
ViewGroup widgetGroup = (ViewGroup) widgetFrame;
289+
boolean hasChild = widgetGroup.getChildCount() > 0;
290+
widgetFrame.setVisibility(hasChild ? View.VISIBLE : View.GONE);
291+
if (hasChild) {
292+
for (int index = 0; index < widgetGroup.getChildCount(); index++) {
293+
View child = widgetGroup.getChildAt(index);
294+
child.setDuplicateParentStateEnabled(true);
295+
}
296+
}
297+
}
298+
}
299+
300+
private static class PreferenceSpacingDecoration extends RecyclerView.ItemDecoration {
301+
private final int spacing;
302+
303+
PreferenceSpacingDecoration(@NonNull Context context) {
304+
spacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2,
305+
context.getResources().getDisplayMetrics());
306+
}
307+
308+
@Override
309+
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
310+
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
311+
RecyclerView.Adapter<?> adapter = parent.getAdapter();
312+
if (!(adapter instanceof PreferenceGroupAdapter)) {
313+
return;
314+
}
315+
int position = parent.getChildAdapterPosition(view);
316+
if (position == RecyclerView.NO_POSITION) {
317+
return;
318+
}
319+
Preference preference = ((PreferenceGroupAdapter) adapter).getItem(position);
320+
if (!(preference instanceof PreferenceCategory)) {
321+
outRect.bottom = spacing;
322+
}
323+
}
324+
}
325+
}

0 commit comments

Comments
 (0)