Skip to content

Commit 51e8282

Browse files
committed
[Settings] Add dual pane support for "Language"
Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 1f0a36d commit 51e8282

8 files changed

Lines changed: 390 additions & 104 deletions

File tree

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import io.github.muntashirakon.AppManager.R;
1919
import io.github.muntashirakon.AppManager.misc.DeviceInfo2;
20+
import io.github.muntashirakon.util.UiUtils;
2021

2122
public class AboutDeviceFragment extends Fragment {
2223
private MainPreferencesViewModel mModel;
@@ -47,7 +48,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
4748
mlp.topMargin = 0;
4849
view.setLayoutParams(mlp);
4950
}
50-
view.setFitsSystemWindows(false);
51+
boolean secondary = false;
52+
if (getArguments() != null) {
53+
secondary = requireArguments().getBoolean(PreferenceFragment.PREF_SECONDARY);
54+
requireArguments().remove(PreferenceFragment.PREF_KEY);
55+
requireArguments().remove(PreferenceFragment.PREF_SECONDARY);
56+
}
57+
if (secondary) {
58+
UiUtils.applyWindowInsetsAsPadding(view, false, true, false, true);
59+
} else UiUtils.applyWindowInsetsAsPaddingNoTop(view);
5160
return view;
5261
}
5362

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package io.github.muntashirakon.AppManager.settings;
4+
5+
import android.annotation.SuppressLint;
6+
import android.os.Build;
7+
import android.os.Bundle;
8+
import android.text.TextUtils;
9+
import android.util.TypedValue;
10+
import android.view.LayoutInflater;
11+
import android.view.View;
12+
import android.view.ViewGroup;
13+
import android.widget.CheckedTextView;
14+
import android.widget.FrameLayout;
15+
16+
import androidx.annotation.LayoutRes;
17+
import androidx.annotation.NonNull;
18+
import androidx.annotation.Nullable;
19+
import androidx.collection.ArraySet;
20+
import androidx.core.widget.TextViewCompat;
21+
import androidx.fragment.app.Fragment;
22+
import androidx.recyclerview.widget.LinearLayoutManager;
23+
24+
import com.google.android.material.color.MaterialColors;
25+
import com.google.android.material.resources.MaterialAttributes;
26+
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Locale;
31+
import java.util.Map;
32+
import java.util.Set;
33+
import java.util.concurrent.atomic.AtomicBoolean;
34+
35+
import io.github.muntashirakon.AppManager.R;
36+
import io.github.muntashirakon.AppManager.utils.LangUtils;
37+
import io.github.muntashirakon.AppManager.utils.appearance.AppearanceUtils;
38+
import io.github.muntashirakon.util.AdapterUtils;
39+
import io.github.muntashirakon.util.UiUtils;
40+
import io.github.muntashirakon.widget.RecyclerView;
41+
import io.github.muntashirakon.widget.SearchView;
42+
43+
public class ChangeLanguageFragment extends Fragment {
44+
private SearchView mSearchView;
45+
private RecyclerView mRecyclerView;
46+
private FrameLayout mViewContainer;
47+
@Nullable
48+
private SearchableRecyclerViewAdapter<String> mAdapter;
49+
private String mCurrentLang;
50+
private boolean mIsTextSelectable;
51+
52+
@Nullable
53+
@Override
54+
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
55+
View view = inflater.inflate(io.github.muntashirakon.ui.R.layout.dialog_searchable_single_choice, container, false);
56+
mRecyclerView = view.findViewById(android.R.id.list);
57+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
58+
mRecyclerView.setScrollIndicators(0);
59+
}
60+
mRecyclerView.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false));
61+
mViewContainer = view.findViewById(io.github.muntashirakon.ui.R.id.container);
62+
mSearchView = view.findViewById(io.github.muntashirakon.ui.R.id.action_search);
63+
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
64+
@Override
65+
public boolean onQueryTextSubmit(String query) {
66+
return false;
67+
}
68+
69+
@Override
70+
public boolean onQueryTextChange(String newText) {
71+
if (mAdapter != null) mAdapter.setFilteredItems(newText);
72+
return true;
73+
}
74+
});
75+
view.setFitsSystemWindows(true);
76+
boolean secondary = false;
77+
if (getArguments() != null) {
78+
secondary = requireArguments().getBoolean(PreferenceFragment.PREF_SECONDARY);
79+
requireArguments().remove(PreferenceFragment.PREF_KEY);
80+
requireArguments().remove(PreferenceFragment.PREF_SECONDARY);
81+
}
82+
if (secondary) {
83+
UiUtils.applyWindowInsetsAsPadding(view, false, true, false, true);
84+
} else UiUtils.applyWindowInsetsAsPaddingNoTop(view);
85+
return view;
86+
}
87+
88+
@Override
89+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
90+
super.onViewCreated(view, savedInstanceState);
91+
mCurrentLang = Prefs.Appearance.getLanguage();
92+
Map<String, Locale> locales = LangUtils.getAppLanguages(requireActivity());
93+
final CharSequence[] languageNames = getLanguagesL(locales);
94+
final String[] languages = new String[languageNames.length];
95+
int i = 0;
96+
int localeIndex = 0;
97+
for (Map.Entry<String, Locale> localeEntry : locales.entrySet()) {
98+
languages[i] = localeEntry.getKey();
99+
if (languages[i].equals(mCurrentLang)) {
100+
localeIndex = i;
101+
}
102+
++i;
103+
}
104+
@SuppressLint({"RestrictedApi", "PrivateResource"})
105+
int layoutId = MaterialAttributes.resolveInteger(requireContext(), androidx.appcompat.R.attr.singleChoiceItemLayout,
106+
com.google.android.material.R.layout.mtrl_alert_select_dialog_singlechoice);
107+
mAdapter = new SearchableRecyclerViewAdapter<>(Arrays.asList(languageNames), Arrays.asList(languages), layoutId);
108+
mAdapter.setSelectedIndex(localeIndex, false);
109+
mRecyclerView.setAdapter(mAdapter);
110+
}
111+
112+
@Override
113+
public void onStart() {
114+
super.onStart();
115+
requireActivity().setTitle(R.string.choose_language);
116+
}
117+
118+
@NonNull
119+
private CharSequence[] getLanguagesL(@NonNull Map<String, Locale> locales) {
120+
CharSequence[] localesL = new CharSequence[locales.size()];
121+
Locale locale;
122+
int i = 0;
123+
for (Map.Entry<String, Locale> localeEntry : locales.entrySet()) {
124+
locale = localeEntry.getValue();
125+
if (LangUtils.LANG_AUTO.equals(localeEntry.getKey())) {
126+
localesL[i] = getString(R.string.auto);
127+
} else localesL[i] = locale.getDisplayName(locale);
128+
++i;
129+
}
130+
return localesL;
131+
}
132+
133+
private void triggerSingleChoiceClickListener(int index, boolean isChecked) {
134+
if (mAdapter == null) {
135+
return;
136+
}
137+
String selectedItem = mAdapter.mItems.get(index);
138+
if (selectedItem != null && isChecked) {
139+
mCurrentLang = selectedItem;
140+
Prefs.Appearance.setLanguage(mCurrentLang);
141+
AppearanceUtils.applyConfigurationChangesToActivities();
142+
}
143+
}
144+
145+
class SearchableRecyclerViewAdapter<T> extends RecyclerView.Adapter<SearchableRecyclerViewAdapter<T>.ViewHolder> {
146+
@NonNull
147+
private final List<CharSequence> mItemNames;
148+
@NonNull
149+
private final List<T> mItems;
150+
@NonNull
151+
private final ArrayList<Integer> mFilteredItems = new ArrayList<>();
152+
private int mSelectedItem = -1;
153+
private final Set<Integer> mDisabledItems = new ArraySet<>();
154+
@LayoutRes
155+
private final int mLayoutId;
156+
157+
SearchableRecyclerViewAdapter(@NonNull List<CharSequence> itemNames, @NonNull List<T> items, int layoutId) {
158+
mItemNames = itemNames;
159+
mItems = items;
160+
mLayoutId = layoutId;
161+
synchronized (mFilteredItems) {
162+
for (int i = 0; i < items.size(); ++i) {
163+
mFilteredItems.add(i);
164+
}
165+
}
166+
}
167+
168+
void setFilteredItems(String constraint) {
169+
constraint = TextUtils.isEmpty(constraint) ? null : constraint.toLowerCase(Locale.ROOT);
170+
Locale locale = Locale.getDefault();
171+
synchronized (mFilteredItems) {
172+
int previousCount = mFilteredItems.size();
173+
mFilteredItems.clear();
174+
for (int i = 0; i < mItems.size(); ++i) {
175+
if (constraint == null
176+
|| mItemNames.get(i).toString().toLowerCase(locale).contains(constraint)
177+
|| mItems.get(i).toString().toLowerCase(Locale.ROOT).contains(constraint)) {
178+
mFilteredItems.add(i);
179+
}
180+
}
181+
AdapterUtils.notifyDataSetChanged(this, previousCount, mFilteredItems.size());
182+
}
183+
}
184+
185+
@Nullable
186+
T getSelection() {
187+
if (mSelectedItem >= 0) {
188+
return mItems.get(mSelectedItem);
189+
}
190+
return null;
191+
}
192+
193+
void setSelection(@Nullable T selectedItem, boolean triggerListener) {
194+
if (selectedItem != null) {
195+
int index = mItems.indexOf(selectedItem);
196+
if (index != -1) {
197+
setSelectedIndex(index, triggerListener);
198+
}
199+
}
200+
}
201+
202+
void setSelectedIndex(int selectedIndex, boolean triggerListener) {
203+
if (selectedIndex == mSelectedItem) {
204+
// Do nothing
205+
return;
206+
}
207+
updateSelection(false, triggerListener);
208+
mSelectedItem = selectedIndex;
209+
updateSelection(true, triggerListener);
210+
mRecyclerView.setSelection(selectedIndex);
211+
}
212+
213+
void addDisabledItems(@Nullable List<T> disabledItems) {
214+
if (disabledItems != null) {
215+
for (T item : disabledItems) {
216+
int index = mItems.indexOf(item);
217+
if (index != -1) {
218+
synchronized (mDisabledItems) {
219+
mDisabledItems.add(index);
220+
}
221+
}
222+
}
223+
}
224+
}
225+
226+
@NonNull
227+
@Override
228+
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
229+
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
230+
return new ViewHolder(view);
231+
}
232+
233+
@Override
234+
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
235+
Integer index;
236+
synchronized (mFilteredItems) {
237+
index = mFilteredItems.get(position);
238+
}
239+
final AtomicBoolean selected = new AtomicBoolean(mSelectedItem == index);
240+
holder.item.setText(mItemNames.get(index));
241+
holder.item.setTextIsSelectable(mIsTextSelectable);
242+
synchronized (mDisabledItems) {
243+
holder.item.setEnabled(!mDisabledItems.contains(index));
244+
}
245+
holder.item.setChecked(selected.get());
246+
holder.item.setOnClickListener(v -> {
247+
if (selected.get()) {
248+
// Already selected, do nothing
249+
return;
250+
}
251+
// Unselect the previous and select this one
252+
updateSelection(false, true);
253+
mSelectedItem = index;
254+
// Update selection manually
255+
selected.set(!selected.get());
256+
holder.item.setChecked(selected.get());
257+
triggerSingleChoiceClickListener(index, selected.get());
258+
});
259+
}
260+
261+
@Override
262+
public int getItemCount() {
263+
synchronized (mFilteredItems) {
264+
return mFilteredItems.size();
265+
}
266+
}
267+
268+
private void updateSelection(boolean selected, boolean triggerListener) {
269+
if (mSelectedItem < 0) {
270+
return;
271+
}
272+
int position;
273+
synchronized (mFilteredItems) {
274+
position = mFilteredItems.indexOf(mSelectedItem);
275+
}
276+
if (position >= 0) {
277+
notifyItemChanged(position, AdapterUtils.STUB);
278+
}
279+
if (triggerListener) {
280+
triggerSingleChoiceClickListener(mSelectedItem, selected);
281+
}
282+
}
283+
284+
class ViewHolder extends RecyclerView.ViewHolder {
285+
CheckedTextView item;
286+
287+
@SuppressLint("RestrictedApi")
288+
public ViewHolder(@NonNull View itemView) {
289+
super(itemView);
290+
item = itemView.findViewById(android.R.id.text1);
291+
int textAppearanceBodyLarge = MaterialAttributes.resolveInteger(item.getContext(), com.google.android.material.R.attr.textAppearanceBodyLarge, 0);
292+
TextViewCompat.setTextAppearance(item, textAppearanceBodyLarge);
293+
item.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
294+
item.setTextColor(MaterialColors.getColor(item.getContext(), com.google.android.material.R.attr.colorOnSurfaceVariant, -1));
295+
}
296+
}
297+
}
298+
}

0 commit comments

Comments
 (0)