I'm using Talkback to read the content of the views on my ViewPager, and it's reading the content of the current view and the content of the next view (not visible).
For example
View 1
TextView -> hi1
TextView -> bye1
View2
TextView -> hi2
TextView -> bye2
Talkback read hi1, hi2, bye1, bye2
I've tried to change the value of pager.setOffscreenPageLimit(), but it doesn't do anything, Talkback always read the current view and the next one, even if the value of OffScreenPageLimit is 4 (it should read the next 2 views).
The only info I've found is that: https://code.google.com/p/eyes-free/issues/detail?id=139
Any idea?
You can create event custom Accessibility event inside the OnChangePageView and save the string you want to read in a list and get position from interface between Activity/fragment and adapter.
ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrollStateChanged(int arg0) { }
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) { }
#Override
public void onPageSelected(int position) {
if(isTalkbackActive(getApplicationContext())) {
AccessibilityEvent event = AccessibilityEvent.obtain();
if (onPageChangeInterface != null) {
String text = onPageChangeInterface.getTextToRead(position);
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.getText().add(text);
viewPagerWallets.requestFocus();
viewPagerWallets.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
accessibilityManager.sendAccessibilityEvent(event);
}
}
}
};
Then the same string that you saved you add it in ContentDescription of the relative that you have all the view only in the first position.
ArrayList readAccessibilityValues = new ArrayList<>();
String valor = valor + txtView1.getText()+",";
valor = valor + txtView2.getText()+",";
if(position == 0) {
relativeGeneral.setContentDescription(valor);
}
readAccessibilityValues.add(valor);
I think it's not the best solution but I have not found another
This could help you to get into the right direction:
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val event = AccessibilityEvent.obtain()
event.eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
event.text.add("Hello TalkBack!")
val accessibilityManager = requireContext().getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
accessibilityManager.sendAccessibilityEvent(event)
}
})
Simply replace "Hello Talkback!" by any string you would like to be read when changing a page in ViewPager.
Related
I have a Multi-Selection RecyclerView. I am tring to handle button click inside to RecyclerView Item when It's selected/activated. But when an item selected/activated in RecyclerView I am not able to click the ChildView (Button) It handles RootView of content and reverts to the deactivated/notselected state.
Video
I read the documantation It says
Selection Hotspot
This is an optional feature identifying an area within a view that is
single-tap to select. Ordinarily a single tap on an item when there is
no existing selection will result in that item being activated. If the
tap occurs within the "selection hotspot" the item will instead be
selected.
See OnItemActivatedListener for details on handling item activation.
I tried to override inSelectionHotspot function
/**
* Areas are often included in a view that behave similar to checkboxes, such
* as the icon to the left of an email message. "selection
* hotspot" provides a mechanism to identify such regions, and for the
* library to directly translate taps in these regions into a change
* in selection state.
*
* #return true if the event is in an area of the item that should be
* directly interpreted as a user wishing to select the item. This
* is useful for checkboxes and other UI affordances focused on enabling
* selection.
*/
public boolean inSelectionHotspot(#NonNull MotionEvent e) {
return false;
}
The document's comment reffers to Gmail application, that example to selection by clicking e-mail's leftside ImageView and you can able to move e-mail's content by clicking to Title or Description area. That's exactly what I'm trying to do.
ItemDetailsLookup.ItemDetails Class
class PurchaseItemDetails(var binding: ListObjectLevelPurchaseBinding) : ItemDetailsLookup.ItemDetails<LevelModel>() {
var itemPosition: Int = 0
lateinit var item: LevelModel
override fun getSelectionKey(): LevelModel? {
return item
}
override fun getPosition(): Int {
return itemPosition
}
override fun inSelectionHotspot(e: MotionEvent): Boolean {
val locationOnScreen = IntArray(2)
binding.buttonLevelPurchaseInspect.getLocationOnScreen(locationOnScreen)
val (left) = locationOnScreen
val right = left + binding.buttonLevelPurchaseInspect.width
Log.e("InSelectionHotspot","${e.x.roundToInt() !in left..right}")
return e.x.roundToInt() !in left..right
}
}
I also tried to override onItemActivatedListener nothing changed.
tracker = SelectionTracker.Builder<LevelModel>(
"selection.levels",
binding.recyclerviewBundleDetailPurchaseLevels,
PurchaseItemKeyProvider(adapter.currentList),
PurchaseItemLookup(binding.recyclerviewBundleDetailPurchaseLevels),
StorageStrategy.createParcelableStorage(LevelModel::class.java)
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
Although this question is old, I think there always will be Android devs that are struggling with the RecyclerView's selection API. As always the Google's library is lacking details and some useful examples. That's why I'd like to share a solution for those who keep struggling for a working setup of selection hotspot. Sorry for the inconvenience, the code is in Java since I don't use Kotlin yet. But I think it is easy to convert it to Kotlin.
ItemDetailsLookup.ItemDetails Class
public static class PurchaseItemDetails extends ItemDetailsLookup.ItemDetails<Long> {
private long position;
private Rect hotSpot;
public PurchaseItemDetails() {}
#Override
public int getPosition() {
return (int) position;
}
#Nullable
#Override
public Long getSelectionKey() {
return position;
}
#Override
public boolean inSelectionHotspot(#NonNull MotionEvent e) {
if(hotSpot == null) {
Log.d(TAG, "inSelectionHotspot: null; Event X "+e.getX()+", Y "+e.getY());
return false;
}
// Here the coordinates of selection hotspot rect can be observed
Log.d(TAG, String.format("inSelectionHotspot: Hotspot %s; event X %f, Y " +
"%f", hotSpot.toShortString(), e.getX(), e.getY()));
return hotSpot.contains((int) e.getX(), (int) e.getY());
}
void setPosition(long position) {
this.position = position;
}
void setHotSpot(Rect hotSpot) {
this.hotSpot = hotSpot;
}
}
Now that we have implemented the ItemDetails class we have to have this class within the ViewHolder class in order to be able to define and set the selection hotspot.
SampleViewHolder Class
class SampleViewHolder extends RecyclerView.ViewHolder {
private final PurchaseItemDetails details;
//...
public SampleViewHolder(#NonNull View itemView) {
super(itemView);
details = new PurchaseItemDetails();
}
void bind() {
// Setup selection hotspot
Rect rect = new Rect();
// Attention here! We must get the rectangle after the viewholder object has created.
// Otherwise the getGlobalVisibleRect() will return unexpected values.
binding.buttonLevelPurchaseInspect.getGlobalVisibleRect(rect);
// Set the rectangle spot so that the Item details can check and recognize the spot.
details.setHotSpot(rect);
// Setup position
details.setPosition(getBindingAdapterPosition());
//...
}
//...
}
And finally somewhere in the adapter class where we bind the view holder:
#Override
public void onBindViewHolder(#NonNull SampleViewHolder holder, int position) {
holder.bind();
}
Warning!!
This is not a whole code at all. The implementers must adapt it to their codes and convenience.
I have one activity with one RecyclerView (SuperSlim library) and a detail activity, when I click a item of that list, detail activity will be open. The problem is when I go back, I'm trying to set the clicked element as the first visible element in the list but I get this horrible animation:
This is my onActivityReenter()
#Override
public void onActivityReenter(int resultCode, Intent data) {
super.onActivityReenter(resultCode, data);
Log.d(TAG, "onActivityReenter() called with: resultCode = [" + resultCode + "], data = [" + data + "]");
mTmpReenterState = new Bundle(data.getExtras());
int startingPosition = mTmpReenterState.getInt(EXTRA_STARTING_ITEM_POSITION);
int currentPosition = mTmpReenterState.getInt(EXTRA_CURRENT_ITEM_POSITION);
mRecycler.scrollToPosition(currentPosition);
postponeEnterTransition();
mRecycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
mRecycler.getViewTreeObserver().removeOnPreDrawListener(this);
// TODO: figure out why it is necessary to request layout here in order to get a smooth transition.
mRecycler.requestLayout();
startPostponedEnterTransition();
return true;
}
});
}
And my SharedElementCallback:
private final SharedElementCallback exitTransitionCallBack = new SharedElementCallback() {
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (mTmpReenterState == null) {
// If mTmpReenterState is null, then the activity is exiting.
View navigationBar = findViewById(android.R.id.navigationBarBackground);
View statusBar = findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
names.add(navigationBar.getTransitionName());
sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
names.add(statusBar.getTransitionName());
sharedElements.put(statusBar.getTransitionName(), statusBar);
}
} else {
int startingPosition = mTmpReenterState.getInt(EXTRA_STARTING_ITEM_POSITION);
int currentPosition = mTmpReenterState.getInt(EXTRA_CURRENT_ITEM_POSITION);
if (startingPosition != currentPosition) {
// If startingPosition != currentPosition the user must have swiped to a
// different page in the DetailsActivity. We must update the shared element
// so that the correct one falls into place.
sharedElements.clear();
sharedElements.put("number", mLayoutManager.findViewByPosition(currentPosition).findViewById(R.id.text_number));
sharedElements.put("day", mLayoutManager.findViewByPosition(currentPosition).findViewById(R.id.text_day));
sharedElements.put("recycler", mLayoutManager.findViewByPosition(currentPosition + 1).findViewById(R.id.recycler));
}
mTmpReenterState = null;
}
}
};
I think the problem is that the activity try to make an animation from the original item position to the top to the list, but I don't know how avoid that.
Does anyone know how to fix this??
Thanks in advance guys!!!
After a while I've realized that the problem that I was updating the Main Activity list with a notifyItemChanged(int) by LocalBroadcast and the standard recyclerView animation made that glitch. I solve the problem using:
RecyclerView.setItemAnimator(null)
because the standard didn't work
((SimpleItemAnimator) RecyclerView.getItemAnimator())
.setSupportsChangeAnimations(false)
So the problem is nothing to do with SharedElements.
I have a Recyclerview, im animating a view inside individual list item, but when I scroll the recyclerview the animation is stopping. Its because recyclerview removes the items form its view so when we scroll back it fetches it back! But now i want that animation to keep going as I would stop it only when i get data from server!
All I want is the animation that I start in the individual items inside the recylerview shouldn't stop even if the recyclerview is scrolled and the view is out of focus and comes back to focus! I need to stop the animation in the code when I get the server data! I have the code where to stop the animation and it works if the item is not scrolled off the view!
btn.onClick -- this button is the onClick for the recyclerview list
item 1 btn.startAnimation(anim.xml) -- starting the animation
onSuccess -- server returns success btn.clearAnimation();
but before the onSuccess if we scroll the list the animation is stopped!
Please help!
By inspiring from crymson's answer i have made little easy and useful solution using tag method of View instead setting a boolean in complicated logic of your custom adapter.
#Override
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (holder.getItemViewType() == TYPE_AD)
((ViewHolderForAd) holder).ivStory.setTag(false);
}
public class ViewHolderForAd extends RecyclerView.ViewHolder {
private ImageView ivStory;
TextView tvName;
public ViewHolderForAd(View view) {
super(view);
ivStory = (ImageView) view.findViewById(R.id.ivStoryImage);
tvName = (TextView) view.findViewById(R.id.tvAppName);
view.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
int pos = getAdapterPosition();
if (pos < 0) {
pos = (int) v.getTag();
}
customItemClickListener.onItemClicked(v, pos);
}
});
//ivStory.startAnimation(AnimationUtils.loadAnimation(context, R.anim.pulse_story));
ivStory.setTag(false); //Set default tag false to decrease risk of null
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
//...Your code...
if (!(boolean) holder1.ivStory.getTag()) {
holder1.ivStory.setTag(true);
holder1.ivStory.startAnimation(AnimationUtils.loadAnimation(context, R.anim.pulse_story));
}
//...Your code...//
}
You can use setTag(key, object) instead of setTag(object) if you already tagged something(like position) in your imageView.
Hope this helps someone.
Hard to give you a full solution but have you tried saving the animation state inside the ViewHolder that you are using? I'd recommend saving a boolean flag in the ViewHolder class you defined like isAnimating which is initially set to false and in your onBindViewHolder(...) method you can do something like
if (viewHolder.isAnimating) {
// start animation
} else {
// clear animation
}
viewHolder.btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewHolder.isAnimating = true;
// start animation
}
});
Hi am using a SwipeListView that has two buttons on its back view i'm trying to programatically set the SwipeListView so that it doesn't swipe by setting the swipe mode to none. the problem i am having is the front view is now not registering the click. Does any one know why???
Heres what i have tried so far
final SwipeListView messagesList = (SwipeListView)v.findViewById(R.id.list);
if(r_deleteMessages == false || r_markMessages == false) messagesList.setSwipeMode(SwipeListView.SWIPE_MODE_NONE);
if(messageData != null){
ViewGroup.LayoutParams params = messagesList.getLayoutParams();
int size;
size = messageData.size();
params.height = (SM_Global.pxFromDp(context, 80) * size) +3;
messagesList.setLayoutParams(params);
messagesList.requestLayout();
messagesList.setFocusable(false);
final MessagesAdapter messagesAdapter = new MessagesAdapter(context, R.layout.layout_message_item, messageData,messagesList,"profile");
messagesList.setAdapter(messagesAdapter);
Log.v("Auth","CAN READ MESSAGES | " + r_readMessages);
messagesList.setSwipeListViewListener(new BaseSwipeListViewListener() {
#Override
public void onClickFrontView(int position) {
super.onClickFrontView(position);
Log.v("Auth","CLICKED ");
}
});
You could add a Listener to the list view and override onChangeSwipeMode
if(r_deleteMessages == false || r_markMessages == false){
mList.setSwipeListViewListener(new BaseSwipeListViewListener() {
#Override
public int onChangeSwipeMode(int position) {
return SwipeListView.SWIPE_MODE_NONE;
}
});
}
This way you will still get touch events.
You may also need to do mList.setSwipeOpenOnLongPress(false); to disablr the long click
see: https://github.com/47deg/android-swipelistview/issues/9
I tried the suggested ways but didn't work for me. So I made changes in my List Adapter & handled the click event there. Now it is working fine.
I want to draw a check mark for the image view I click on and uncheck the imageview I clicked on before using the following code snip. I store last checked position in mDeviceAdapter. When I try to uncheck old position, the image view always gives null even for the partial visible image view. I am really confused because I thought only invisible one is recycled... Newbie in Android and any comment is appreciated.
public void CheckableImageView#setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
invalidate();
}
}
mDeviceGallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
CheckableImageView viewToCheck = (CheckableImageView) view;
if (!viewToCheck.isChecked()) {
int oldCheckedPosition = mDeviceAdapter
.getCheckedPosition();
mDeviceAdapter.setCheckedPosition(position);
View checkedView = mDeviceGallery
.getChildAt(oldCheckedPosition);
Log.d(TAG, "old position="+oldCheckedPosition + "old view="+checkedView);
if (checkedView != null) {
((CheckableImageView) checkedView)
.setChecked(false);
Log.d(TAG, "uncheck position="
+ oldCheckedPosition);
}
viewToCheck.setChecked(true);
That's not the right approach.
You need to add to your data type a boolean field (i.e mIsChecked).
On the onItemClick method set the value of that variable to true and keep its INDEX as a member of the adapter. When another item is clicked set the value of that item to true and set the value of the saved one to false (change the value of the datatype in you ArrayList in the INDEX you stored in the previous click).
Now, in the getView() method, you must have if/else statement. Something like:
if (item.isChecked())
{
checkedView.setChecked(true);
}
else
{
checkedView.setChecked(false);
}
Example to the onClick method: (just a general direction)
if (item.isChecked())
{
checkedView.setChecked(false);
yourList.get(position).setChecked(true);
yourList.get(mLastCheckedIndex).setChecked(false);
mLastCheckedIndex = position;
notifyDataSetChanged();
}
else
{
//same but opposite.
}
Hope this helps!