Skip to content

Commit e24eb8d

Browse files
committed
Adapt M3 style preferences
Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent eff7f58 commit e24eb8d

18 files changed

Lines changed: 690 additions & 436 deletions
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.github.muntashirakon.AppManager.settings;
2+
3+
import android.annotation.SuppressLint;
4+
import android.graphics.Rect;
5+
import android.view.View;
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Px;
8+
import androidx.preference.PreferenceCategory;
9+
import androidx.preference.PreferenceGroupAdapter;
10+
import androidx.recyclerview.widget.RecyclerView;
11+
import com.google.android.material.card.MaterialCardView;
12+
import com.google.android.material.shape.AbsoluteCornerSize;
13+
import com.google.android.material.shape.ShapeAppearanceModel;
14+
15+
@SuppressLint("RestrictedApi")
16+
public class M3PreferenceGroupDecoration extends RecyclerView.ItemDecoration {
17+
@Px
18+
private final float mRadius;
19+
@Px
20+
private final float mRadiusInner;
21+
22+
public M3PreferenceGroupDecoration(@Px float radius, @Px float innerRadius) {
23+
mRadius = radius;
24+
mRadiusInner = innerRadius;
25+
}
26+
27+
@Override
28+
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
29+
super.getItemOffsets(outRect, view, parent, state);
30+
31+
if (!(view instanceof MaterialCardView)) {
32+
return;
33+
}
34+
35+
MaterialCardView cardView = (MaterialCardView) view;
36+
RecyclerView.Adapter<?> rawAdapter = parent.getAdapter();
37+
if (!(rawAdapter instanceof PreferenceGroupAdapter)) {
38+
return;
39+
}
40+
41+
PreferenceGroupAdapter adapter = (PreferenceGroupAdapter) rawAdapter;
42+
int position = parent.getChildAdapterPosition(view);
43+
if (position == RecyclerView.NO_POSITION) return;
44+
45+
// Skip category header
46+
if (isCategoryHeader(adapter, position)) {
47+
return;
48+
}
49+
50+
boolean isFirst = isFirstInGroup(adapter, position);
51+
boolean isLast = isLastInGroup(adapter, position);
52+
53+
ShapeAppearanceModel.Builder shapeBuilder = cardView.getShapeAppearanceModel().toBuilder();
54+
55+
if (isFirst && isLast) {
56+
// Standalone preference
57+
shapeBuilder
58+
.setTopLeftCornerSize(new AbsoluteCornerSize(mRadius))
59+
.setTopRightCornerSize(new AbsoluteCornerSize(mRadius))
60+
.setBottomLeftCornerSize(new AbsoluteCornerSize(mRadius))
61+
.setBottomRightCornerSize(new AbsoluteCornerSize(mRadius));
62+
} else if (isFirst) {
63+
// Top of a preference group
64+
shapeBuilder
65+
.setTopLeftCornerSize(new AbsoluteCornerSize(mRadius))
66+
.setTopRightCornerSize(new AbsoluteCornerSize(mRadius))
67+
.setBottomLeftCornerSize(new AbsoluteCornerSize(mRadiusInner))
68+
.setBottomRightCornerSize(new AbsoluteCornerSize(mRadiusInner));
69+
} else if (isLast) {
70+
// Bottom of a preference group
71+
shapeBuilder
72+
.setTopLeftCornerSize(new AbsoluteCornerSize(mRadiusInner))
73+
.setTopRightCornerSize(new AbsoluteCornerSize(mRadiusInner))
74+
.setBottomLeftCornerSize(new AbsoluteCornerSize(mRadius))
75+
.setBottomRightCornerSize(new AbsoluteCornerSize(mRadius));
76+
} else {
77+
// Middle of a preference group
78+
shapeBuilder
79+
.setTopLeftCornerSize(new AbsoluteCornerSize(mRadiusInner))
80+
.setTopRightCornerSize(new AbsoluteCornerSize(mRadiusInner))
81+
.setBottomLeftCornerSize(new AbsoluteCornerSize(mRadiusInner))
82+
.setBottomRightCornerSize(new AbsoluteCornerSize(mRadiusInner));
83+
}
84+
85+
cardView.setShapeAppearanceModel(shapeBuilder.build());
86+
}
87+
88+
private boolean isCategoryHeader(@NonNull PreferenceGroupAdapter adapter, int position) {
89+
return adapter.getItem(position) instanceof PreferenceCategory;
90+
}
91+
92+
private boolean isFirstInGroup(@NonNull PreferenceGroupAdapter adapter, int position) {
93+
if (position == 0) return true;
94+
95+
// If the element right above this choice is a section separator, this is the top card.
96+
return isCategoryHeader(adapter, position - 1);
97+
}
98+
99+
private boolean isLastInGroup(@NonNull PreferenceGroupAdapter adapter, int position) {
100+
if (position == adapter.getItemCount() - 1) return true;
101+
102+
// If the element right below this card is a header separator, this is the bottom card.
103+
return isCategoryHeader(adapter, position + 1);
104+
}
105+
}

app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferences.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
import io.github.muntashirakon.AppManager.self.life.FundingCampaignChecker;
2020
import io.github.muntashirakon.AppManager.utils.LangUtils;
2121
import io.github.muntashirakon.AppManager.utils.UIUtils;
22-
import io.github.muntashirakon.preference.InfoAlertPreference;
23-
import io.github.muntashirakon.preference.WarningAlertPreference;
2422

2523
public class MainPreferences extends PreferenceFragment {
2624
@NonNull
@@ -52,10 +50,10 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
5250
MainPreferencesViewModel model = new ViewModelProvider(requireActivity()).get(MainPreferencesViewModel.class);
5351
mActivity = requireActivity();
5452
// Expiry notice
55-
WarningAlertPreference buildExpiringNotice = requirePreference("app_manager_expiring_notice");
53+
Preference buildExpiringNotice = requirePreference("app_manager_expiring_notice");
5654
buildExpiringNotice.setVisible(!Boolean.FALSE.equals(BuildExpiryChecker.buildExpired()));
5755
// Funding campaign notice
58-
InfoAlertPreference fundingCampaignNotice = requirePreference("funding_campaign_notice");
56+
Preference fundingCampaignNotice = requirePreference("funding_campaign_notice");
5957
fundingCampaignNotice.setVisible(FundingCampaignChecker.campaignRunning());
6058
// Custom locale
6159
mLocalePref = requirePreference("custom_locale");

app/src/main/java/io/github/muntashirakon/AppManager/settings/PreferenceFragment.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import android.annotation.SuppressLint;
66
import android.os.Bundle;
7+
import android.util.TypedValue;
78
import android.view.View;
89

910
import androidx.annotation.CallSuper;
@@ -37,7 +38,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
3738
requireArguments().remove(PREF_KEY);
3839
requireArguments().remove(PREF_SECONDARY);
3940
}
40-
// https://github.com/androidx/androidx/blob/androidx-main/preference/preference/res/layout/preference_recyclerview.xml
41+
// https://github.com/androidx/androidx/blob/29fe2b988bba2b56c0ff406d4edf1060e9154c82/preference/preference/src/main/java/androidx/preference/PreferenceFragment.java#L548
4142
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
4243
recyclerView.setFitsSystemWindows(true);
4344
recyclerView.setClipToPadding(false);
@@ -48,6 +49,27 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
4849
UiUtils.applyWindowInsetsAsPadding(recyclerView, false, true, false, true);
4950
}
5051
} else UiUtils.applyWindowInsetsAsPaddingNoTop(recyclerView);
52+
53+
// Use M3 design for preference cards
54+
float m3CornerRadius;
55+
float m3CornerRadiusInner;
56+
TypedValue typedValue = new TypedValue();
57+
if (requireContext().getTheme().resolveAttribute(io.github.muntashirakon.ui.R.attr.listItemCornerRadius, typedValue, true)) {
58+
m3CornerRadius = typedValue.getDimension(getResources().getDisplayMetrics());
59+
} else {
60+
throw new RuntimeException("?attr/listItemCornerRadius not defined.");
61+
}
62+
if (requireContext().getTheme().resolveAttribute(io.github.muntashirakon.ui.R.attr.listItemCornerRadiusInner, typedValue, true)) {
63+
m3CornerRadiusInner = typedValue.getDimension(getResources().getDisplayMetrics());
64+
} else {
65+
throw new RuntimeException("?attr/listItemCornerRadiusInner not defined.");
66+
}
67+
68+
// Apply the decorator with the specific radius
69+
recyclerView.addItemDecoration(new M3PreferenceGroupDecoration(m3CornerRadius, m3CornerRadiusInner));
70+
71+
// Remove old style line dividers
72+
setDivider(null);
5173
}
5274

5375
@CallSuper
@@ -70,7 +92,7 @@ public <T extends androidx.preference.Preference> T requirePreference(CharSequen
7092
return Objects.requireNonNull(findPreference(key));
7193
}
7294

73-
protected void enablePrefs(boolean enable, Preference ...prefs) {
95+
protected void enablePrefs(boolean enable, Preference... prefs) {
7496
if (prefs == null) {
7597
return;
7698
}

app/src/main/java/io/github/muntashirakon/AppManager/settings/VirusTotalPreferences.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public void onCreatePreferences(@Nullable Bundle bundle, @Nullable String rootKe
3636
boolean isVtEnabled = FeatureController.isVirusTotalEnabled();
3737
String apiKey = Prefs.VirusTotal.getApiKey();
3838
TopSwitchPreference useVtPref = requirePreference("use_vt");
39-
DefaultAlertPreference infoNoInternetPref = requirePreference("info_no_internet");
39+
Preference infoNoInternetPref = requirePreference("info_no_internet");
4040
Preference vtApiKeyPref = requirePreference("virus_total_api_key");
4141
SwitchPreferenceCompat promptBeforeUploadPref = requirePreference("virus_total_prompt_before_uploading");
4242
DefaultAlertPreference infoPref = requirePreference("info");

app/src/main/res/xml/preferences_about.xml

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,63 @@
22
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
33
xmlns:tools="http://schemas.android.com/tools">
44

5-
<io.github.muntashirakon.preference.HyperlinkPreference
6-
app:title="@string/app_name"
7-
app:summary="@string/copyright_message"
8-
app:iconSpaceReserved="false" />
9-
10-
<Preference
11-
app:key="version"
12-
app:title="@string/pref_version_changelog"
13-
tools:summary="v3.0.0 (410)"
14-
app:iconSpaceReserved="false" />
15-
16-
<io.github.muntashirakon.preference.HyperlinkPreference
17-
app:key="license"
18-
app:title="@string/license"
19-
app:summary="@string/license_message"
20-
app:iconSpaceReserved="false" />
21-
22-
<io.github.muntashirakon.preference.HyperlinkPreference
23-
app:title="@string/source_code"
24-
app:summary="@string/source_code_links"
25-
app:iconSpaceReserved="false" />
26-
27-
<io.github.muntashirakon.preference.HyperlinkPreference
28-
app:title="@string/community"
29-
app:summary="@string/community_links"
30-
app:iconSpaceReserved="false" />
31-
32-
<Preference
33-
app:key="website"
34-
app:title="@string/website"
35-
app:iconSpaceReserved="false" />
36-
37-
<Preference
38-
app:key="user_manual"
39-
app:title="@string/user_manual"
40-
app:iconSpaceReserved="false" />
41-
42-
<Preference
43-
app:key="get_help"
44-
app:title="@string/get_help"
45-
app:iconSpaceReserved="false" />
46-
47-
<Preference
48-
app:key="third_party_libraries"
49-
app:title="@string/third_party"
50-
app:iconSpaceReserved="false" />
51-
52-
<Preference
53-
app:key="credits"
54-
app:title="@string/credits"
55-
app:iconSpaceReserved="false" />
5+
<PreferenceCategory app:layout="@layout/m3_preference_category_empty">
6+
<io.github.muntashirakon.preference.HyperlinkPreference
7+
app:iconSpaceReserved="false"
8+
app:summary="@string/copyright_message"
9+
app:title="@string/app_name" />
10+
11+
<Preference
12+
app:iconSpaceReserved="false"
13+
app:key="version"
14+
app:title="@string/pref_version_changelog"
15+
tools:summary="v3.0.0 (410)" />
16+
17+
<io.github.muntashirakon.preference.HyperlinkPreference
18+
app:iconSpaceReserved="false"
19+
app:key="license"
20+
app:summary="@string/license_message"
21+
app:title="@string/license" />
22+
23+
</PreferenceCategory>
24+
25+
<PreferenceCategory app:layout="@layout/m3_preference_category_empty">
26+
<io.github.muntashirakon.preference.HyperlinkPreference
27+
app:iconSpaceReserved="false"
28+
app:summary="@string/source_code_links"
29+
app:title="@string/source_code" />
30+
31+
<io.github.muntashirakon.preference.HyperlinkPreference
32+
app:iconSpaceReserved="false"
33+
app:summary="@string/community_links"
34+
app:title="@string/community" />
35+
36+
<Preference
37+
app:iconSpaceReserved="false"
38+
app:key="website"
39+
app:title="@string/website" />
40+
41+
<Preference
42+
app:iconSpaceReserved="false"
43+
app:key="user_manual"
44+
app:title="@string/user_manual" />
45+
46+
<Preference
47+
app:iconSpaceReserved="false"
48+
app:key="get_help"
49+
app:title="@string/get_help" />
50+
</PreferenceCategory>
51+
52+
<PreferenceCategory app:layout="@layout/m3_preference_category_empty">
53+
<Preference
54+
app:iconSpaceReserved="false"
55+
app:key="third_party_libraries"
56+
app:title="@string/third_party" />
57+
58+
<Preference
59+
app:iconSpaceReserved="false"
60+
app:key="credits"
61+
app:title="@string/credits" />
62+
</PreferenceCategory>
5663

5764
</PreferenceScreen>
Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,46 @@
11
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
22
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
33
xmlns:tools="http://schemas.android.com/tools">
4-
<Preference
5-
app:key="selected_users"
6-
app:title="@string/pref_selected_users"
7-
app:summary="@string/pref_selected_users_msg"
8-
app:iconSpaceReserved="false" />
9-
10-
<Preference
11-
app:key="saved_apk_format"
12-
app:title="@string/pref_saved_apk_name_format"
13-
app:summary="@string/pref_saved_apk_name_format_msg"
14-
app:iconSpaceReserved="false" />
15-
16-
<Preference
17-
app:key="thread_count"
18-
app:title="@string/pref_thread_count"
19-
tools:summary="Execute at most 5 operations in parallel"
20-
app:iconSpaceReserved="false" />
21-
22-
<Preference
23-
app:key="adb_local_server_port"
24-
app:title="@string/adb_local_server_port"
25-
tools:summary="60001"
26-
app:iconSpaceReserved="false" />
27-
28-
<Preference
29-
app:key="import_export_keystore"
30-
app:title="@string/pref_import_export_keystore"
31-
app:summary="@string/pref_import_export_keystore_msg"
32-
app:iconSpaceReserved="false" />
33-
34-
<SwitchPreferenceCompat
35-
app:key="send_notifications_to_connected_devices"
36-
app:title="@string/pref_send_notifications_to_connected_devices"
37-
app:summary="@string/pref_send_notification_to_connected_devices_msg"
38-
app:iconSpaceReserved="false" />
4+
5+
<PreferenceCategory app:layout="@layout/m3_preference_category_empty">
6+
<Preference
7+
app:iconSpaceReserved="false"
8+
app:key="selected_users"
9+
app:summary="@string/pref_selected_users_msg"
10+
app:title="@string/pref_selected_users" />
11+
12+
<Preference
13+
app:iconSpaceReserved="false"
14+
app:key="saved_apk_format"
15+
app:summary="@string/pref_saved_apk_name_format_msg"
16+
app:title="@string/pref_saved_apk_name_format" />
17+
18+
<Preference
19+
app:iconSpaceReserved="false"
20+
app:key="thread_count"
21+
app:title="@string/pref_thread_count"
22+
tools:summary="Execute at most 5 operations in parallel" />
23+
24+
<Preference
25+
app:iconSpaceReserved="false"
26+
app:key="adb_local_server_port"
27+
app:title="@string/adb_local_server_port"
28+
tools:summary="60001" />
29+
30+
31+
<SwitchPreferenceCompat
32+
app:iconSpaceReserved="false"
33+
app:key="send_notifications_to_connected_devices"
34+
app:summary="@string/pref_send_notification_to_connected_devices_msg"
35+
app:title="@string/pref_send_notifications_to_connected_devices" />
36+
</PreferenceCategory>
37+
38+
<PreferenceCategory app:layout="@layout/m3_preference_category_empty">
39+
<Preference
40+
app:iconSpaceReserved="false"
41+
app:key="import_export_keystore"
42+
app:summary="@string/pref_import_export_keystore_msg"
43+
app:title="@string/pref_import_export_keystore" />
44+
</PreferenceCategory>
3945

4046
</PreferenceScreen>

0 commit comments

Comments
 (0)