I have successfully completed a navigation into RecyclerViewAdapter to navigate many destinations by string Resource. Because I have many lists, and each TextView it's about separate fragment.
It's wonderful to do it. But I have a small problem. That's I have 2 string Resource, "en" English as a default and "ar" as a second language.
My app is working well when I use it by English locale. But it crashes when I use it by Arabic locale.
What I want is:
To control or force the app when it converts to Arabic to still use the default, which is English string resource.
Here's the RecyclerViewAdapter code block:
override fun onBindViewHolder(holder: SubSectionListHolder, position: Int) {
val item = dataset[position]
holder.subSectionView.text = context.resources.getString(item.subSectionSrcID)
holder.subSectionView.setOnClickListener {
val stringConvertToId = it.resources.getIdentifier(
context.resources.getString(item.subSectionSrcID).replace("\\s".toRegex(), ""),
"id",
context.packageName)
it.findNavController().navigate(stringConvertToId)
}
}
Here's sample of the navGraph tag:
<fragment
android:id="#+id/CreateOrder"
android:name=".PoCreateOrderFragment"
android:label="#string/btnStr_crtOrder"
tools:layout="#layout/fragment_po_create_order">
</fragment>
<fragment
android:id="#+id/ReceivedPreOrders"
android:name=".PoRcOrdersFragment"
android:label="#string/str_whPo_rcvdPrm"
tools:layout="#layout/fragment_po_rc_orders">
</fragment>
<fragment
android:id="#+id/DeferredPreOrders"
android:name=".PoDfOrdersFragment"
android:label="#string/str_whPo_dfrdPrm"
tools:layout="#layout/fragment_po_df_orders">
</fragment>
Here's sample of the default string Resource tag:
<string name="btnStr_crtOrder">Create Order</string>
<string name="str_whPo_rcvdPrm">Received PreOrders</string>
<string name="str_whPo_dfrdPrm">Deferred PreOrders</string>
Here's sample of the Arabic string Resource tag:
<string name="btnStr_crtOrder">إضافة طلب</string>
<string name="str_whPo_rcvdPrm">الطلبيات المستلمة</string>
<string name="str_whPo_dfrdPrm">الطلبيات المؤجلة</string>
To completely reach my idea to you.
This is really brittle (as you're finding out!) and you're just going to create headaches for yourself with this kind of complicated stuff. You should really keep the UI (e.g. the text being displayed) completely separate from the business logic (in this case, uniquely identifying each item and doing a specific action based on which one is clicked). The way you're doing it, it completely breaks whenever the display text is changed
You're already holding a list of items with a resource string ID, right? And looking them up by index using the RecyclerView position. If I were you, I'd just create a lookup associating each item with a hardcoded navigation resource ID.
You could make another list with all the navigation IDs and use position to grab the correct one. Or you could make a Map associating each string resource ID with its navigation ID:
val labelsToDestinations = mapOf(
R.string.btnStr_crtOrder to R.id.createOrder,
...
)
// in onBindViewHolder
holder.subSectionView.setOnClickListener {
val destination = labelsToDestinations[item.subSectionSrcID]
it.findNavController.navigate(destination)
}
That way, it doesn't matter what the value of the string resource is, you're just looking it up by the resource's ID. The value can change (different languages, different wording) and that doesn't matter.
Or just make it another property on the item (e.g. item.destinationId), like your label string ID already is. Personally, if I have a fixed set of things I need to define like this, I usually make an enum (you could use a sealed class if you want:
enum class DestinationItem(#StringRes labelId: Int, #IdRes navigationId: Int) {
CREATE_ORDER(R.string.btnStr_crtOrder, R.id.createOrder)
RECEIVED_PRE_ORDERS(R.string.str_whPo_rcvdPrm, R.id.receivedPreOrders)
...
}
val items = DestinationItem.values()
then you can generate your list of items from that, and you have access to all the important IDs on the item itself. You can easily change which resources they use without affecting anything else - you can use a different label resource to control the display, that won't affect the navigation ID because it's a completely separate property
Related
There are some controls in our app which we'd like to update the control type read out by Talkback. For example: A TextView which would better be described as a link, or an ImageView that would better be described as a button.
We could simply update the content description to report out the desired control type, though I am wondering if there is a better way? If there is another way, can it be done both through the view XML and dynamically in the code behind?
Yes, it is possible to change the type. It is called roleDescription. You would change it as follows:
ViewCompat.setAccessibilityDelegate(yourView,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(v: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(v, info)
info.roleDescription = "Button"
}
})
(use string resources and translate the strings to all languages supported by your app)
This cannot be done via XML by default, but you could look into writing your own binding adapter for this.
I have created BindingAdapter to make clickable part inside TextView. I have used 3 parameters, one string resource with placeholder for clickable part (textWithPlaceholder), second part with clickable part (textClickablePhrase) and last one for listener (onPhraseClick). When building the app, it fails with upcoming error.
Cannot find the setter for attribute 'app:onPhraseClick' with parameter type lambda on android.widget.TextView.
The strange thing is when I try the same code without first two parameters (the strings), it can build. Even if I try to build app without last parameter (listener), it can build. So the code works but it doesn't work together.
<TextView
<!-- another attributes -->
app:textWithPlaceholder="#{#string/text_with_placeholder}"
app:textClickablePhrase="#{#string/clickable_phrase}"
app:onPhraseClick="#{() -> viewModel.onPhraseClicked()}" />
#BindingAdapter("textWithPlaceholder", "textClickablePhrase", "onPhraseClick")
#JvmStatic
fun TextView.setClickablePhrase(
textWithPlaceholder: String,
textClickablePhrase: String,
onPhraseClick: View.OnClickListener) {
// setting Spannable and click listener
}
I expect the 3 parameters should work together but they works only separated.
EDIT:
It can build even like this but I think it is not right solution because it is not defined right. All parameters are mandatory and it should be able to be defined in this way and not to use workaround to make it optional.
#BindingAdapter(value = ["textWithPlaceholder", "textClickablePhrase", "onPhraseClick"], requireAll = false)
Let's say I have an application with 2 themes: masculine and feminine. The themes simply change out the color palette and a few drawables to appeal to the user's preferred tastes.
Many thanks to http://www.androidengineer.com/2010/06/using-themes-in-android-applications.html for his hints at making that work.
But now lets say I want to get a little cuter with the app and not only change the colors and drawables, but I also want to change the strings. For instance, I might want to add a pirate theme and then "Submit" would be "Arrrrgh!"
So, my basic question is: How can I change the strings throughout my app via user selectable themes?
Edit:
Making this up: the app has 12 buttons and 32 text views I'd like to have theme dependent and I'd like to accomplish this without a giant mapping or a slew of custom attrs.
All 3 of the current solutions will work. Looking for something cleaner though I don't know that such a beast exists.
Yes, it can be done, and here's how: first you'll have to define a theme attribute, like so:
<attr name="myStringAttr" format="string|reference" />
Then, in your themes, add this line
<item name="myStringAttr">Yarrrrr!</item>
or
<item name="myStringAttr">#string/yarrrrr</item>
You can then use this attribute in an XML file like so (note the ? instead of #).
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="?attr/myStringAttr" />
or, from code, like so:
public CharSequence resolveMyStringAttr(Context context)
{
Theme theme = context.getTheme();
TypedValue value = new TypedValue();
if (!theme.resolveAttribute(R.attr.myStringAttr, value, true)) {
return null;
}
return value.string;
}
Let's say I have an application with 2 themes: masculine and feminine. The themes simply change out the color palette and a few drawables to appeal to the user's preferred tastes.
How about we pretend that you're doing something else? This is a design anti-pattern, associating particular colors based on gender (e.g., "girls like pink").
This is not to say that your technical objective is bad, just that this is a really stereotypical example.
For instance, I might want to add a pirate theme and then "Submit" would be "Arrrrgh!"
Only if "Cancel" maps to "Avast!".
How can I change the strings throughout my app via user selectable themes?
You have not said where those strings are coming from. Are they string resources? Database entries? Ones that you are retrieving from a Web service? Something else?
I will assume for the moment that these are string resources. By definition, you will need to have N copies of the strings, one per theme.
Since gender and piratical status are not things tracked by Android as possible resource set qualifiers, you can't have those string resources be in different resource sets. While they could be in different files (e.g., res/values/strings_theme1.xml), filenames are not part of resource identifiers for strings. So, you will wind up having to use some sort of prefix/suffix to keep track of which strings belong with which themes (e.g., #string/btn_submit_theme1).
If these strings are not changing at runtime -- it's just whatever is in your layout resource -- you could take a page from Chris Jenkins' Calligraphy library. He has his own subclass of LayoutInflater, used to overload some of the standard XML attributes. In his case, his focus is on android:fontFamily, where he supports that mapping to a font file in assets.
In your case, you could overload android:text. In your layout file, rather than it pointing to any of your actual strings, you could have it be the base name of your desired string resource, sans any theme identifier (e.g., if the real strings are #string/btn_submit_theme1 and kin, you could have android:text="btn_submit"). Your LayoutInflater subclass would grab that value, append the theme name suffix, use getIdentifier() on your Resources to look up the actual string resource ID, and from there get the string tied to your theme.
A variation on this would be to put the base name in android:tag instead of android:text. android:text might point to one of your real string resources, to help with GUI design and such. Your LayoutInflater would grab the tag and use that to derive the right string at runtime.
If you will be replacing text with other text pulled from theme-based string resources, you could isolate your get-the-string-given-the-base-name logic into a static utility method somewhere that you could apply.
While getting this right initially will take a bit of work, it will scale to arbitrary complexity, in terms of the number of affected UI widgets and strings. You still have to remember to add values for all themes for any new strings you define (bonus points for creating a custom Lint check or Gradle task for validating this).
Since a resource is just an int at heart you could store a number of them at runtime and them substitute them in procedurally as you use them.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="OK_NORMAL">Okay</string>
<string name="OK_PIRATE">Yaaarrrr!</string>
<string name="OK_NINJA">Hooooaaa!</string>
</resources>
public enum ThemeMode {
PIRATE,
NINJA,
NORMAL;
}
public class MyThemeStrings {
public static int OK_PIRATE = R.string.OK_PIRATE;
public static int OK_NINJA = R.string.OK_NINJA;
public static int OK_NORMAL = R.string.OK_NORMAL;
}
public setOkButtonText(ThemeMode themeMode) {
// buttonOk is instantiated elsewhere
switch (themeMode) {
case PIRATE:
buttonOk.setText(MyThemeStrings.OK_PIRATE);
break;
case NINJA:
buttonOk.setText(MyThemeStrings.OK_NINJA);
break;
default:
Log.e(TAG, "Unhandled ThemeMode: " + themeMode.name());
// no break here so it flows into the NORMAL base case as a default
case NORMAL:
buttonOk.setText(MyThemeStrings.OK_NORMAL);
break;
}
}
Although, having written all that, there is probably a better way to do all this through separate XML files. I'll look into it now and write a second solution if I find one.
Ok, I have a second option which may actually be easier to maintain and keep your code cleaner although it may be more resource hungry due to loading an array for each String. I've not benchmarked it but will offer it as another choice but I wouldn't use it if you offer too many theme choices.
public enum ThemeMode {
NORMAL(0),
PIRATE(1),
NINJA(2);
private int index;
private ThemeMode(int index) {
this.index = index;
}
public int getIndex() {
return this.index;
}
}
<resources>
<!-- ALWAYS define strings in the correct order according to
the index values defined in the enum -->
<string-array
name="OK_ARRAY">
<item>OK</item>
<item>Yaarrrr!</item>
<item>Hooaaaa!</item>
</string-array>
<string-array
name="CANCEL_ARRAY">
<item>Cancel</item>
<item>Naarrrrr!</item>
<item>Wha!</item>
</string-array>
</resources>
public setButtonTexts(Context context, ThemeMode themeMode) {
// buttons are instantiated elsewhere
buttonOk.setText(context.getResources()
.getStringArray(R.array.CANCEL_ARRAY)[themeMode.getIndex()]);
buttonCancel.setText(context.getResources()
.getStringArray(R.array.OK_ARRAY)[themeMode.getIndex()]);
}
So, I have not had a chance to test this, but from reading the file on Locale it looks like you can create your own location.
http://developer.android.com/reference/java/util/Locale.html
and help from another stackoverflow
Set Locale programmatically
A little bit of combination leads me to:
Locale pirate = new Locale("Pirate");
Configuration config = new Configuration();
config.locale = pirate;
this.getActivity().getBaseContext().getResources()
.updateConfiguration(config,
this.getActivity().getBaseContext().getResources().getDisplayMetrics());
I do believe this would let you have res/values-pirate/strings as an actual valid resource that would get used when you are a pirate. Any strings or settings you don't override would then revert to the res/values/... by default so you could do this for as many themes as you want. Again assuming it works.
Target MvvmCross, Android
Objective: A screen (ViewModel/View) where the user can select an animal group (Amphibians, Birds, Fish, Invertebrates, Mammals, Reptiles). When a group has been selected, a Fragment Views will will display information for that animal group. The fields and layout differ per animal group (e.g. fish don't have wings).
Although for this question I have chosen for animal group (which is pretty static), want the list animal groups to be flexible.
Simplified app structure:
MyApp.Core
ViewModels
MainViewModel
IAnimalGroupViewModel
AmphibiansViewModel
BirdsBViewModel
FishViewModel
MyApp.Droid
Layout
MainView
AmphibiansFragment
BirdsFragment
FishFragment
Views
MainView
AmphibiansFragment
BirdsFragment
FishFragment
The MainView.axml layout file will contain (a placeholder for) the fragment of the displayed animal group.
In WPF or WP8 app I could make use of a ContentPresenter and a Style to automatically display the selected ViewModel with its View.
How could I achieve something like that in Droid?
I could use a Switch/Case in the MainView.cs that sets the Fragment according to the type of the selected ViewGroup. But that means I have to modify the MainView every time I add a new View.
Any suggestions/ideas?
Currently MvvmCross doesn't provide any kind of automatic navigation mechanism for Fragments in the same way that it does for Activities.
However, within your use case, if you wanted to use a navigation approach, then you could automatically build a similar type of automated lookup/navigation mechanism.
To do this, the easiest developer root would probably be to use reflection to find a lookup dictionary for all the fragments
var fragments = from type in this.GetType().Assembly.GetTypes()
where typeof(IAnimalGroupView)..sAssignableFrom(type)
where type.Name.EndsWith("Fragment")
select type;
var lookup = fragments.ToDictionary(
x => x.Name.Substring(0, x.Name.Length - "Fragment".Length)
+ "ViewModel",
x => x);
With this in place, you could then create the fragments when they are needed - e.g.
assuming that you convert the Selection event via an ICommand on the ViewModel into a ShowViewModel<TViewModel> call
and assuming that you have a Custom Mvx presenter which intercepts these ShowViewModel requests and passes them to the activity (similar to the Fragment sample) - e.g.
public class CustomPresenter
: MvxAndroidViewPresenter
{
// how this gets set/cleared is up to you - possibly from `OnResume`/`OnPause` calls within your activities.
public IAnimalHostActivity AnimalHost { get; set; }
public override void Show(MvxViewModelRequest request)
{
if (AnimalHost != null && AnimalHost.Show(request))
return;
base.Show(request);
}
}
then your activity could implement Show using something like:
if (!lookup.ContainsKey(request.ViewModelType.Name))
return false;
var fragmentType = lookup[request.ViewModelType.Name];
var fragment = (IMvxFragmentView)Activator.Create(fragmentType);
fragment.LoadViewModelFrom(request);
var t = SupportFragmentManager.BeginTransaction();
t.Replace(Resource.Id.my_selected_fragment_holder, fragment);
t.Commit();
return true;
Notes:
if you aren't using ShowViewModel here then obviously this same approach could be adjusted... but this answer had to propose something...
in a larger multipage app, you would probably look to make this IAnimalHostActivity mechanism much more generic and use it in several places.
On subclasses of View there is a getTag() method, which returns the android:tag attribute's value from .xml.
I would like the same for a MenuItem... is it okay to just cast it to a View?
Because item elements also allow a tag attribute in .xml...
Update: My goal with this is setting a tag in .xml, i.e. "notranslate", and querying it at runtime (we localize by hand at runtime, don't ask...)
It is always alright to cast, however, casting any Interface cannot be checked at compile time, only runtime. This is normally the reason many do not recommend casting an Interface that you have no control over. Having the proper error checking code is the best way to insure that such a cast does not break your code.
For the casting, it doesn't really matter whether the MenuItem is an Interface or a View, but the object it references must be one of View's subclasses, if not a View itself. If you are going to cast it, try the cast and catch a ClassCastException just in case as this is the error that will be thrown in runtime.
Another option is that since the MenuItem is simply an interface, you can easily just create a View subclass that utilizes MenuItem allowing you to do the cast. If you are doing a custom ContextMenu as many launchers do, then chances are your answer is nearly complete.
Hope this helps,
FuzzicalLogic
MenuItem is an interface. Any class can implement this interface and so it will not always be safe to cast the MenuItem to a View. You can use the "instanceOf" operator to test to see if the object that implements the MenuItem interface is indeed a View or not.
I understand that you want to define a flag in the XML definition of the menu and then at run time interrogate that flag to make a programmatic decision.
The Menu Resource Documentation records what attributes can be set in the XML. You can consider using (abusing) one of those settings such as the "android:alphabeticShortcut" to encode the flag and use the MenuItem::getAlphabeticShortcut() method to get the value. This does not require casting - it just uses the existing fields in the MenuItem XML construct/class for your own purposes.
Perhaps a less hacky way to do this is to keep a simple table in a separate assets file that lists the menu item identifiers and the special behavior associated with that identifier such as to translate or not to translate.
Alternatively create a simple class that has a table with this configuration information hard coded using the logical "#[+][package:]id/resource_name" resource identifier as the keys to the table. While this doesn't keep it all in one place (in the XML) it does it in a manner that is not encoding information in unused attributes, or relying on the ids not changing. The "table" could be implemented as a static method with an embedded switch statement allowing code such as "if (TranslationTable.shouldTranslate(menuItem.getItemId())) { do translation }"
I had a similar problem in that I wanted to associate some arbitrary data with each menu item so that I could handle menu items in a generic way without having to use hardcoded checks for individual item ids in code.
What I did was for a particular menu item (e.g. #+id/foo) There was an a TypedArray that was defined using the same name as the menu item ID. You could do this with other types of resources as well.
So to do the association, you get the resouce entry name (foo in my example) and then use that to look up the id of the other resource of a different type (#array/foo in my example).
In my handler for menu I had code like this:
Resources resources = getResources();
String name = resources.getResourceEntryName(item.getItemId());
int id = resources.getIdentifier(name, "array", "com.example");
if(id != 0)
{
TypedArray data = resources.obtainTypedArray(id);
// Use the typed array to get associated data
}
EDIT:
Actually it is even easier than that. There is nothing special about the ids on menu items other than you don't want multiple menu items with the same id. The id does not have to be of the form #+id/foo. It can actually also refer to other resources. So in my example above, instead of having the menu have an id of #+id/foo and using the resource manager to use that to find #array/foo, I changed to actually have the menu item have the id of #array/foo.
Now in my onOptionsItemSelected I have this:
Resources resources = getResources();
if("array".equals(resources.getResourceTypeName(item.getItemId())))
{
TypedArray data = resources.obtainTypedArray(item.getItemId());
// Use the typed array
}