pass an event to all related views in android - java

Is there any way to change multiple TextView text sizes all at once? Also they may or may not be in single ViewGroup. I was hoping of doing by creating some kind of trigger/event that would act like pub/sub but got no ideas. Any Possibility?

You could just use a class which you register the TextViews with. Then have a method which goes through all the registered TextViews and sets the font size. Somethings like:
public class TextViewTextSizer {
private List<TextView> textViews;
public TextViewTextSizer() {
textViews = new ArrayList<>();
}
public void registerTextView(TextView textView) {
textViews.add(textView);
}
public void setTextSize(float size) {
for (TextView textView : textViews) {
textView.setTextSize(size);
}
}
}

Related

Radio button are not working normally in my Recycler View. Multiple Radio Buttons got selected of the view which are not visible with the focused one

I'm using a Recycler View to show all the images from the galley or the external storage of a device in a Grid Layout Manager. And I'm using a Radio Button to show if the image is selected or not.
PROBLEM
Whenever I select or deselect a Radio Button from the visible Views in the Recycler View some other Views which are outside the Visible Screen got selected or deselected.
It is like I'm pressing on the same View of the Recycler View, but the images are different.
PROBLEM
well that's because of the recycler view concept of reusing the views instead of creating new views every time you scroll.
you see if you have 100 items you want to show in a recycler view and only 20 of them could appear to the user, recycler view creates only 20 view holder to represent the 20 items, whenever the user scroll recycler view will still have 20 view holder only but will just switch the data stored in this view holders rather than create new view holders.
now to handle selection of your items there's two ways to do this.
the naive way
hold selection in a boolean array inside the recycle view adapter.
whenever the user scrolls, the adapter calls onBindViewHolder to update the visible viewholder with the proper data.
so when onBindViewHolder gets called just set the radio button selection according the boolean array using the position sent in the method call
at the end of your usage to the recycler view you can create a getter method in the adapter to get the selection array list of boolean and pass the data based on it
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_ClassType> data;
ArrayList<Boolean> dataSelected ;
public PhotosGalleryAdapter(ArrayList<Your_Data_ClassType> data) {
this.data = data;
dataSelected = new ArrayList<>(data.size()) ;
}
...
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
RadioButton radioButton = holder.getRadioButton()
radioButton.setChecked(dataSelected.get(position));
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
dataSelected.set(holder.getAbsoluteAdapterPosition() , isChecked) ;
}
});
...
}
}
the other way is to use a selection tracker and it should be the correct way to handle selections in a recycler view.
the problem with this way is it needs a lot of editing to the code and creating new classes to include as parameters in the selection tracker, but in the end you'll find it worth the time you spent on it.
in order to start with this way you need to do the following :
firstly, decide what should be a key (String-Long-Parcelable) so the tracker should use to differentiate between your data , the safest way is either String or Parcelable as I once tried Long and ended up with lots and lots of problems (in your case I will assume it's the photo's uri which will be of type string)
secondly, you need to create two new classes, one that extends ItemDetailsLookup, and the other extends ItemKeyProvider, and should use the key as their generic type (the type that is put between <> )
your two classes should look like this (that you might copy them straight forward)
the one that extends ItemKeyProvider :
public class GalleryItemKeyProvider extends ItemKeyProvider<String>{
PhotosGalleryAdapter adapter ;
/**
* Creates a new provider with the given scope.
*
* #param scope Scope can't be changed at runtime.
*/
public GalleryItemKeyProvider(int scope,PhotosGalleryAdapter m_adapter) {
super(scope);
this.adapter = m_adapter;
}
#Nullable
#Override
public String getKey(int position) {
return adapter.getKey(position);
}
#Override
public int getPosition(#NonNull String key) {
return adapter.getPosition(key);
}
}
the one that extends ItemDetailsLookup :
public class GalleryDetailsLookup extends ItemDetailsLookup<String> {
private final RecyclerView recView ;
public GalleryDetailsLookup(RecyclerView m_recView){
this.recView = m_recView;
}
#Nullable
#Override
public ItemDetails<String> getItemDetails(#NonNull MotionEvent e) {
View view = recView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recView.getChildViewHolder(view);
if (holder instanceof PhotosGalleryViewHolder) {
return ((PhotosGalleryViewHolder) holder).getItemDetails();
}
}
return null;
}
}
thirdly, you should include this new two methods in your adapter to be used by the above classes
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
...
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
...
}
forthly (if there's an english word called forthly), you should initialize the tracker with all the above classes that were created before and he will handle the rest, the tracker takes as parameters
a unique selection tracker id (if that will be the only selection tracker you will use then name it anything)
the ItemKeyProvider that we created
the DetailsLookup that we created
a String-Long-Parcelable Storage to store the keys that were selected in (in our case it will be a String Storage)
a Selection predicate, it's responsible to handle the way of selection you want to do, you want it to be able to (select only one item-multiple selection with no limits- based on a weird algorithm like even only or odd only), in my case I will use a default multiple selection one but if you want to alter it with another selection algorithm you should create a new class that extends SelectionPredicates and implement your way of selection, you could also just check the other default ones might be what you're looking for.
anyway, that's how the initialization should look (you should put this code wherever you initialize your recycler view at whether it's in fragment or activity method):
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
...
}
I didn't find a way to let me initialize the adapter with data and then create the tracker inorder to make the viewholders know about their selection or not, so in this case I firstly created the tracker and then made the adapter know about it's data using a setter and notifyDataSetChanged
what I mean by that is after creating the tracker instantly set the tracker and data to the adapter, so the initRecycleView should look like this
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
photosAdapter.setTracker(tracker);
photosAdapter.setData(data);
photosAdapter.notifyDataSetChanged();
...
}
Last but no least, you should handle how the view holders should know if they were selected or not, so you should let the adapter know about the tracker and its data by creating a setter method in it, that's how the adapter should look like in the end :
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_Class> data;
private SelectionTracker<String> tracker;
public PhotosGalleryAdapter() {
data = new ArrayList<>();
}
public ArrayList<Your_Data_Class> getData() {
return data;
}
public void setData(ArrayList<Your_Data_Class> m_data) {
this.data = m_data;
}
#Override
public ScheduleViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
...
}
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
boolean isSelected = tracker.isSelected(data.get(i).getUri());
RadioButton radioButton = holder.getRadioButton;
radioButton.setChecked(isSelected);
}
#Override
public int getItemCount() {
return data.size();
}
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
public void setTracker(SelectionTracker<String> m_tracker) {
this.tracker = m_tracker;
}
}
(as you may notice if you initialized the adapter with its data through the constructor, when he asks the tracker if there were an item selected or not, it will result in a NullPointerException as at the moment of initializing the adapter you still didn't initialize the tracker)
that way you could keep track of your selection the way google suggests in their documentation (which I honestly don't know why the made it very complicate like that).
if you want to know all the selected item in the end of your application/fragment use, you should call tracker.getSelection() which will return a Selection List for you to iterate on
There's a tiny problem/feature with the tracker that it won't start selecting the first item until you use a long press on it, that happens only in the first item you select, if you do want this feature (start selecting mode by long press) then leave it as it is
incase you don't want it you can make the tracker select a ghost key (any unique string key that means nothing to your data) at the beginning which should later enable the selection mode with a simple click on any photo
tracker.select("");
this also the way to make a default/old selection at the beginning, you could make a for loop and call tracker.select(Key) if you do want the tracker to start with few items being selected
N.B : incase you use the Ghost Key method you should watchout that the selection array that will get returned when you call tracker.getSelection() will also contain this Ghost Key.
at the end if you do have the curiosity of reading about selection tracker in the documentation follow this link
or maybe if you know how to read kotlin follow this two links
implementing-selection-in-recyclerview
a guide to recyclerview selection
I was stuck in the selection problem for days before I figure how to do all that so I hope you find your way through it.
Omar Shawky has covered the solutions.
With my answer I will stress on the reason why someone may face this sort of an issues with recycler views and how to avoid this common issue in the future (avoiding pitfalls).
Reason:
This issue happens because RecyclerView recycles views. So a RecyclerView item's view once inflated can get reused to show another off screen (to be scrolled to) item. This helps reduces re-inflation of views which otherwise can be taxing.
So if the radio button of an item's view is selected, and the same view gets reused to show some other item, then that new item can also have a selected radio button.
Solution:
The simplest solution for such issues is to have an if else logic in your ViewHolder to provide logic for both selected and de-selected cases. We also do not rely on information from radio button itself for initial setup (we do not use radioButton.isSelected() at the time of setup)
e.g code to write inside your ViewHolder class:
private boolean isRadioButtonChecked = false; // ViewHolder class level variable. Default value is unchecked
// Now while binding in your ViewHolder class:
// Setup Radio button (assuming there is just one radio button for a recyclerView item).
// Handle both selected and de-selected cases like below (code can be simplified but elaborating for understanding):
if (isRadioButtonChecked) {
radioButton.setChecked(true);
} else {
radioButton.setChecked(false);
}
radioButton.setOnCheckedChangeListener(
(radioButton, isChecked) -> isRadioButtonChecked = isChecked);
Do not do any of the following while setting up:
private boolean isRadioButtonChecked = false; // class variable
//while binding do not only handle select case. We should handle both cases.
if (isRadioButtonChecked) { // --> Pitfall
radioButton.setChecked(true);
}
radioButton.setOnCheckedChangeListener((radioButton, isChecked) -> isRadioButtonChecked = isChecked);
OR
// During initial setup do not use radio button itself to get information.
if (radioButton.isChecked()) { // --> Pitfall
radioButton.setChecked();
}

How to make single selection work in a nested RecyclerView

For our application I had to implement a nested RecyclerView. I'm getting a list of Tables from JSON and every Table has another list with groups from each table. I can get everything on the screen as requested, the problem is the selection.
I have 2 different RecyclerViews on the screen and I can not seem to get a single selection working in this environment, especially after scrolling. Every group and every table has a Toggle Button, and only one can be active at a time.
This is how the main screen looks like
So far I've tried putting a boolean isSelected on the Model but that didn't work out at all. The closest solution I came up with was a helper class that searches every CompoundButton on-screen and deselects them all when one is selected. The problem is this helper class cant get the Buttons which are off-screen.
How I populate ParentAdapter (in MainActivity):
public void setAdapter(List<Table> tableList)
{
RecyclerView recycler_view_parent = findViewById(R.id.recyclerparent);
LinearLayoutManager manager=new LinearLayoutManager(MainActivity.this);
manager.setOrientation(LinearLayoutManager.HORIZONTAL);
recycler_view_parent.setLayoutManager(manager);
recycler_view_parent.setHasFixedSize(true);
recycler_view_parent.setItemViewCacheSize(tableList.size());
ParentAdapter parentAdapter=new ParentAdapter(tableList,MainActivity.this);
recycler_view_parent.setAdapter(parentAdapter);
}
How i populate ChildAdapter (in onBindViewHolder of ParentAdapter):
FlexboxLayoutManager manager = new FlexboxLayoutManager(context);
manager.setFlexDirection(FlexDirection.COLUMN);
manager.setJustifyContent(JustifyContent.FLEX_START);
manager.setFlexWrap(FlexWrap.WRAP);
manager.setAlignItems(AlignItems.BASELINE);
holder.recycler_view_child.setLayoutManager(manager);
holder.recycler_view_child.setHasFixedSize(true);
adapter = new ChildAdapter(tableList, tableList.get(position).getGroups(), context);
holder.recycler_view_child.setAdapter(adapter);
The desired output should be only 1 Table OR Group at a time can be toggled (in total, not one from every RecyclerView) and the state should be the same after scrolling/device rotation).
I did a lot of research over the last days on this subject and I can not seem to find a working example of nested RecyclerView with single selection over both RVs.
So does anyone have an idea on how to solve this? I think the biggest issue is telling the Parent that a Button in Child was toggled and vice-versa.
I think for the ParentAdapter it should look something like this:
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, final int position) {
final Table table = tablelist.get(position);
ViewHolder viewHolder = (ViewHolder) holder;
if (table.isTableSelected()) {
viewHolder.toggletable.setChecked(true);
lastToggled = position;
} else {
viewHolder.toggletable.setChecked(false);
}
viewHolder.toggletable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
if (b) {
table.setTableSelected(true);
// notify ChildAdapter and group.setGroupSelected(false)
if (lastToggled >= 0) {
tablelist.get(lastToggled).setTableSelected(false);
// notify ChildAdapter and group.setGroupSelected(false)
notifyItemChanged(lastToggled);
}
lastToggled = position;
} else {
table.setTableSelected(false);
}
}
});
}
Thanks in advance.
UPDATE: Managed to come up with a solution myself, although 100% sure, not the best approach.
First of all, implement Greenrobots EventBus:
implementation 'org.greenrobot:eventbus:3.1.1'
Now in the Activity where you hold both RecyclerViews register the Event listener:
#Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
and subscribe 2 methods. One for Parent Events and one for Children Events. This methods will trigger every time an item is selected!
#Subscribe(threadMode = ThreadMode.MAIN)
public void onParentEventClicked(ParentAdapter.ParentEvent event) {
// to access the inner adapter here you must set it to public in the ParentAdapter(public ChildAdapter adapter;)
adapter.adapter.deSelectChild();
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void onChildEventClicked(ChildAdapter.ChildEvent event) {
// normal ParentAdapter reference(ParentAdapter adapter;)
adapter.deSelectParent();
}
Inside your ParentAdapter create a method to deselect all parent items and a static class to fire the event:
public void deSelectParent()
{
for (int i=0;i<data.size();i++)
{
data.get(i).setSelected(false);
}
notifyDataSetChanged();
}
public static class ParentEvent {
View view;
int position;
}
Inside your ChildAdapter create a method to deselect all child items and a static class to fire the event:
public void deSelectChild()
{
for (int i=0;i<data.size();i++)
{
datachild.get(i).setSelected(false);
}
notifyDataSetChanged();
}
public static class ChildEvent {
View view;
int position;
}
now in both Parent and Child onBindViewHolders, you need similar logic for your models:
if (item.isSelected()) {
holder.yourbutton.setChecked(true);
} else {
holder.yourbutton.setChecked(false);
}
holder.yourbutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
ParentEvent event = new ParentEvent();
event.view = holder.yourbutton;
event.position = position;
EventBus.getDefault().post(event);
if (holder.yourbutton.isChecked()) {
for (int i = 0; i < data.size(); i++) {
data.get(i).setSelected(false);
}
data.get(position).setSelected(true);
} else {
data.get(position).setSelected(false);
}
notifyDataSetChanged();
}
});
And thats pretty much it, every click on a ParentItem will trigger the deselect method for ChildAdapter and vice-versa.
Due to the high usage of notifyDataSetChanged() I recommend using this line to get rid of the blinking:
recycler_view_parent.setItemAnimator(null);
Any problems let me know!

How to get the count of a textview in a list using espresso?

I have a list where textView #+id/job_status is present number of times in the page. I need to find the count of it and assert it.
Scenario:
Filter(5)
<Item1>
<Item2>
<Item3>
I need to check whether the count displayed Filter("5") is same as the number of items displayed in the list.
I have tried the below method:
public static class RecyclerViewAssertion implements ViewAssertion {
private final int expectedCount;
public RecyclerViewAssertion(int expectedCount) {
this.expectedCount = expectedCount;
}
#Override
public void check(View view, NoMatchingViewException
noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), is(expectedCount));
}
}
onView (withId(android.R.id.list)).check(new
JobsPage.RecyclerViewAssertion(5));
It throws me error saying : android.R.id.list matches multiple views in the hierrarchy. This id has been used in multiple places of the app. Is there any other way for this problem ?

Set properties of multiple views at once

For example, if I have:
EditText A;
TextView B;
ImageView C;
And I want to set all their visibilities to View.GONE, how can I do it in a way that instead of this:
A.setVisibility(View.GONE);
B.setVisibility(View.GONE);
C.setVisibility(View.GONE);
I do this:
groupD.setVisibility(View.GONE);
without having to put all of them in one RelativeLayout and then setting the RelativeLayout to View.GONE? Is there a Java class for this? Or do I have to get a library for it?
If not, can I do it manually so I can organize my project which has 30 views? I don't want to set each group to be in its own RelativeLayout because all views are dependent on each other.
With ButterKnife you can do:
#BindViews({ R.id.A, R.id.B, R.id.C })
List<View> views;
ButterKnife.apply(views, VISIBLE);
ButterKnife.apply(views, GONE);
//Action and Setter interfaces allow specifying simple behavior.
static final ButterKnife.Action<View> VISIBLE = new ButterKnife.Action<View>() {
#Override public void apply(View view, int index) {
view.setVisibility(View.VISIBLE);
}
};
static final ButterKnife.Action<View> GONE = new ButterKnife.Action<View>() {
#Override public void set(View view, int index) {
view.setVisibility(View.GONE);
}
};
Example adapted from the website
The best way will be passing the views to a single method. All of them are Views, so you can do something like this.
public void setViews(View view){
view.setVisibility(View.GONE);
}
And call the method like this:
setViews(editText);

Android Imageview in Listview onClick change image doesn't work properly

I have a ListView and in every single ListviewItem there is an ImageView with a little star (for marking it as favourite). Therefore I put an OnClickListener to the ImageView on every item in the custom ArrayAdapter.
imgStar.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Bitmap bitmap = ((BitmapDrawable)imgStar.getDrawable()).getBitmap();
Bitmap bitmap2 = ((BitmapDrawable)(context.getResources().getDrawable(R.drawable.ic_action_not_important))).getBitmap();
if(bitmap != bitmap2) {
imgStar.setImageResource(R.drawable.ic_action_not_important);
} else {
imgStar.setImageResource(R.drawable.ic_action_important);
}
}
});
The problem: When I get some items and click for example on the star of the first item, the image changes as it should but a few items lower the image changes too o.O
I tested it with some code: The thing that won't get into my head is it is only changing the image (on the other item below), code that would be executed in the onclick is only executed for the item I really click not for the one where the image changes too.
Why does the image of a random other item in the list change also? I hope someone can help me.
Custom Adapter Constructore Code
public LinkArrayAdapter(Context con, int textViewResourceId) {
super(con, textViewResourceId);
context = con;
}
The main problem is that you can't change the image of items in the onClick then leave it and hope it will be updated on every item on the list. Because onClick get called in different time than getView. So you must set item images outside of onClick but in the getView so every time that getView called for a specific item it will set the appropriate image for that item.
Define a boolean array in your CustomAdapter class as:
private boolean[] stars;
Then in constructor method of your class, initialize it as:
this.stars = new boolean[items.size()];
In the onClick method:
// **Edited to apply image update at click**
stars[position] = !stars[position];
notifyDataSetInvalidated();
At last in the getView() method of custom adapter
(ensure this code is not in any other inner blocks):
if (stars[position])
imgStar.setImageResource(R.drawable.ic_action_important);
else
imgStar.setImageResource(R.drawable.ic_action_not_important);
private int selectedPositions ;
// /... your code.
convertView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
OneComment mComment = mlist.get(position);
mComment.isStart = !mComment.isStart;
if(mComment.isStar){
//set star image
} else{
do not set star image}
}
});
#semsamot's answer works, however
notifyDataSetInvalidated() causes the List to reload and goes to the first item.
Use notifyDataSetChanged() instead.

Categories

Resources