I am trying to fire a custom AccessibilityEvent using the AccessibilityManager and TalkBack.
The use case for the event is when the user clicks an action bar, the fragment polls a list of objects, and then fashions its AccessibilityEvent content based on the size of a list.
When I try to run this, I do not get the expected TalkBack message. I am pretty sure that I'm missing something basic with instantiating an AccessibilityEvent.
I am also not sure whether I need to use, or how to apply AccessibilityDelegates here because the callback is coming from a MenuItem rather than a View. I know I can call findViewById to get the view for this MenuItem, but I am not very knowledgeable on these APIs.
Any guidance on these two points would be great!
The problem in question is described basically by the following pseudocode:
public class MyFragment extends Fragment {
//...
private List<Pojo> mPojoList;
//...
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.the_id_for_my_menuitem) {
if (booleanCheck() && !mPojoList.isEmpty()) {
//create the Accessibility event
final AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_CLICKED);
event.setContentDescription(String.format("deleting %2d pojos", mPojoList.size()));
//Send a custom accessibility event to let the user know that we're deleting X objects.
final AccessibilityManager mgr = (AccessibilityManager) this.getActivity().getSystemService(Context.ACCESSIBILITY_SERVICE);
//PROBLEM: We're not seeing this event come through in TalkBack.
mgr.sendAccessibilityEvent(event);
//Delete the objects.
myDeleteObjectsFunction();
}
}
}}
Try to fire accessibility events using View object.
AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_CLICKED);
event.setContentDescription(String.format("deleting %2d pojos", mPojoList.size()));
View view = getActivity().findViewById(R.id.child_view);
ViewParent parent = view.getParent();
if (parent != null) {
parent.requestSendAccessibilityEvent(view, event);
}
Although it is an old question, I'm going to publish my answer because the answer gave before did not work for me.
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.the_id_for_my_menuitem) {
if (booleanCheck() && !mPojoList.isEmpty()) {
AccessibilityManager manager = (AccessibilityManager)this.getSystemService(Context.ACCESSIBILITY_SERVICE);
if(manager.isEnabled()){
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.setClassName(getClass().getName());
event.getText().add(*yourString*);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
event.setSource(findViewById(*yourButton*));
}
manager.sendAccessibilityEvent(event);
}
}
}
}
Related
I am trying to write a launcher-like app which can add Widgets to its screen.
I am using Leonardo Fischer's tutorial (http://leonardofischer.com/hosting-android-widgets-my-appwidgethost-tutorial/) which is great.
In order to remove a widget, the user is supposed to longpress the Widget and that's where I am running into some trouble; some Widgets (WhatsApp Messagelist, Evernote List, for instance) allow you to scroll them. For some reason, if you scroll, Android fires a LongClick event which wrongfully removes the widget...
My code:
(creates the widget and set LongClickListener)
public void createWidget(Intent data) {
Bundle extras = data.getExtras();
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
hostView.setAppWidget(appWidgetId, appWidgetInfo);
// relative layout
//RelativeLayout.LayoutParams lp = new RelativeLayout()
//mainlayout.addView(hostView, lp);
mainlayout.addView(hostView);
// [COMMENTED OUT] hostView.setOnLongClickListener(new AppWidgetLongClickListener(hostView));
}
UPDATE
Countless hours later, I think I partially understood what's happening, but I still can't get the correct behaviour.
According to http://balpha.de/2013/07/android-development-what-i-wish-i-had-known-earlier/ , you need to implement an onInterceptTouchEvent in the parent container (mainlayout in my case) to intercept and treat events before they reach the children (widgets in my case).
So I googled up the following code and tried to adapt to my needs:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Consume any touch events for ourselves after longpress is triggered
//Log.i(TAG,"OnIntercept: "+ev.toString());
if (mHasPerformedLongPress) {
Log.i(TAG,"Longpress OK!: "+ev.toString());
mHasPerformedLongPress = false;
return true;
}
// Watch for longpress events at this level to make sure
// users can always pick up this widget
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
postCheckForLongClick();
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
break;
}
// Otherwise continue letting touch events fall through to children
return false;
}
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
Log.i(TAG,"Inside RUN");
if (getParent()!= null) {
Log.i(TAG,"getParent:"+getParent().toString());
}
if ((getParent() != null) && hasWindowFocus()
&& (mOriginalWindowAttachCount == getWindowAttachCount())
&& !mHasPerformedLongPress) {
if (performLongClick()) { // <-- DOESN'T WORK :(
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = getWindowAttachCount();
}
}
private void postCheckForLongClick() {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout());
}
#Override
public void cancelLongPress() {
super.cancelLongPress();
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
The above code does intercept touch events when I click a widget, but its logic seems aimed at intercepting (and direct for further treatment) longclicks to widgets. What I actually need is to intercept a longclick inside the parent view.
The trick seems to lie at if (performLongClick()), which, as far as I could get, fires a LongClick event to the widget...
... so I guess my question now is how to track a longclick inside the parent view.
Sorry for the long (and seemingly basic) question on handling Android UI events, but from what I googled this seems a very convoluted topic..
So it's done...! I am not sure whether this is an elegant solution, but it works.
onInterceptTouchEvent allows a parent view to act on events before they are sent to the final sender. Please note the it won't fire if you touch the actual view. So if you have a Layout with some "blank space" and some elements, onInterceptTouchEvent won't fire if you touch the layout's "blank space" (you will need the layout's onTouchEvent in this case).
Because we can essentially only track ACTION_UP, ACTION_MOVE and ACTION_DOWN events, we need to time the duration of a ACTION_DOWN / ACTION_UPpair of events to decide whether this is a longclick or not, so what I did follows:
public class time_counter {
private long begin_time;
private long end_time;
public time_counter(long i, long f) {
this.begin_time = i;
this.end_time = f;
}
public long getDuration() {
return (this.end_time - this.begin_time);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Consume any touch events for ourselves after longpress is triggered
// Watch for longpress events at this level to make sure
// users can always pick up this widget
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
cnt = new time_counter(ev.getEventTime(), (long)0);
break;
}
case MotionEvent.ACTION_MOVE: {
if (cnt != null) {
cnt.end_time = ev.getEventTime();
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (cnt != null) {
cnt.end_time = ev.getEventTime();
}
Log.i(TAG, "DURATION: " + cnt.getDuration());
if (cnt.getDuration() > ViewConfiguration.getLongPressTimeout()) {
Log.i(TAG, "it's a longpress: " + this.toString());
if (processClick) {
processClick = false;
this.doRemoveWidget();
}
cancelLongPress();
return true;
}
break;
}
// Otherwise continue letting touch events fall through to children
return false;
}
Whenever Android sends an ACTION_DOWN event, the code start to track its duration using a simple "time counter" object. The counter's end timestamp is continually updated throughout ACTION_MOVE events and when Android sends an ACTION_UP or ACTION_CANCEL, the code checks for the final duration. If it's over ViewConfiguration.getLongPressTimeout() (default = 500ms), it triggers the action.
Notice that in my case I needed a boolean variable to prevent multiple event firing, since I wanted to use a LongClick to remove a widget. A second accidental firing, which would almost always happen, would trigger a null pointer exception since the widget had already been removed.
I tested it with several widgets (big, small, configurable, with and without scrolling views, etc, etc, etc.) and I haven't found a glitch.
Again, not sure if this an elegant or "android-wise" solution, but it solved my problem.
Hope this helps!
REFERENCE:
If you need an excellent article on touch events, please check http://balpha.de/2013/07/android-development-what-i-wish-i-had-known-earlier/. It gave me the correct "frame of mind" to address my problem.
If you really are seeing an event for starting a scroll being followed by an event for a long click, you could deal with that by setting a flag in your event handling class that tracks when scroll begins and ends, and choose to ignore the long click if a scroll is in progress.
I have been trying to style some elements of a ListView. Mainly, on top of the color & background made by selectors, I would like to also change the font of the pressed item. This is not doable with a selector, so I have to do this by code.
I have tried many approaches, with no success.
simplified sample code to illustrate the issue :
in the adapter of the listview :
getView(..) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.mylayout);
viewHolder = new MyViewHolder();
...
convertView.setOnTouchListener(new MyTouchListener());
}
}
private class MyTouchListener implements onTouchListener {
#Override
public boolean onTouchEvent(MotionEvent event) {
..
switch (maskedAction) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
textView.setTypeface(BOLD);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
textView.setTypeface(NORMAL);
break;
}
return false; //// PROBLEM
}
Now, my issue is that if my touch listener returns false for the pointer down event, it means I did not consume the event, so I don't receive the subsequent event (move, up, ..). So the textview is stuck in the BOLD font.
If I return true for the onDown event, the framework considers that the event has already been handled for the view, so it ignores the selectors in my xml. Annoying, but I could always do all the styling in the code, even if it means losing the advantage of selectors for the proprieties that can use it. It also means that the item does not receive the click event, which is a deal breaker.
So, I am in a dead end here. I have tried many alternatives ways to do this (custom view which calls both my listener and the default one, different returns values, ...) with no success.
Does anybody have an idea on how to solve this ?
I have figured it out :
By design, this is not doable with an OnTouchListener. However, there are several other options.
One is to override dispatchTouchEvent or onInterceptTouchEvent in the parent ViewGroup.
Another is to override onCreateDrawableState.
Since I was already overloading this method (in order to create new states), I opted for that one :
#Override
protected int[] onCreateDrawableState(int extraSpace) {
{...}
if (mListener != null) {
if (isPressed() != isPressed) {
isPressed = isPressed();
mListener.onPressedStateChange(isPressed);
}
}
return super.onCreateDrawableState(extraSpace);
}
public interface PressedStateListener {
public void onPressedStateChange(boolean isPressed);
}
PressedStateListener mListener;
public void setOnPressedStateChangedListener(PressedStateListener listener) {
mListener = listener;
}
in the viewholder :
private class DropDownOnPressedStateChangedListener implements PressedStateListener {
#Override
public void onPressedStateChange(boolean isPressed) {
if (isPressed) {
getText().setTypeface(MEDIUM_TYPEFACE, Typeface.NORMAL);
} else {
getText().setTypeface(REGULAR_TYPEFACE, Typeface.NORMAL);
}
}
in the adapter :
if (convertView == null) {
view = mInflater.inflate(ENTRY_RESOURCE_ID, parent, false);
viewHolder = new HeaderDropDownViewHolder(view);
((CustomLayout)view).setOnPressedStateChangedListener(viewHolder.getOnListener());
That way I have a clean implementation that allows me to edit any view property when it is pressed or not without any conflict with its existing customization or behavior.
I use ActionMode to select items in a grid. The problem is that I cannot recognize whether exactly the Done button is clicked. The only I can is to know that ActionMode is finished. But pressing Back finishes the ActionMode too.
The desired behavior is to accept selection on Done click, and exit ActionMode on Back press.
I tried to use ActionMode.setCustomView() but it doesn't affect the Done button. The Activity.onBackPressed() is not called when ActionMode is started.
The one solution I've found is to use ActionBarSherlock and get the Done button manually:
View closeButton = findViewById(R.id.abs__action_mode_close_button);
But it works on Android 2.x-3.x only, because on 4.x a native action bar is used.
Please don't do that as it's implementation specific and extremely non-standard.
You can use the onDestroyActionMode callback for when an action mode is dismissed.
Here is the solution:
ActionMode mMode = MyActivityClass.this.startActionMode(some implementation);
int doneButtonId = Resources.getSystem().getIdentifier("action_mode_close_button", "id", "android");
View doneButton = MyActivityClass.this.findViewById(doneButtonId);
doneButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// do whatever you want
// in android source code it's calling mMode.finish();
}
});
Here is my implementation, and it's a proper hack but it works and I can't really find an alternative to doing something specific when the ActionMode DONE is clicked. I find it really weird that you can't capture this event more elegantly.
Any suggestions to making this slightly less ugly would be greatly appreciated...
In my activity..
boolean mActionModeIsActive = false;
boolean mBackWasPressedInActionMode = false;
#Override
public boolean dispatchKeyEvent(KeyEvent event)
{
mBackWasPressedInActionMode = mActionModeIsActive && event.getKeyCode() == KeyEvent.KEYCODE_BACK;
return super.dispatchKeyEvent(event);
}
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
mActionModeIsActive = true;
return true;
}
#Override
public void onDestroyActionMode(ActionMode mode)
{
mActionModeIsActive = false;
if (!mBackWasPressedInActionMode)
onActionModeDoneClick();
mBackWasPressedInActionMode = false;
}
public void onActionModeDoneClick();
{
// Do something here.
}
If you are using Fragments with your Activity then some of this code will probably need to be in the Fragment, and the other bits in the Activity.
#JakeWharton (and other ActionBarSherlock users) if you see this on your travels. I'd be interested to know if the above is compatible with ABS as I have yet to integrate ABS with my current project.
I call
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
when my app starts to make my app able to display the full screen.
I want my app's UI to pop up when screen is touched, but Activity.onTouchEvent() is not triggered until the screen is touched a second time. At first touch, only the Virtual Keys are shown.
So, I have to trigger my app's UI to pop up on
public void onSystemUiVisibilityChange(int visibility) {
if (visibility == View.SYSTEM_UI_FLAG_VISIBLE) {
// show my APP UI
}
}
but onSystemUiVisibilityChange with View.SYSTEM_UI_FLAG_VISIBLE will be invoked NOT once per touch (3 times on my Galaxy Nexus) by system, especially if the user touches the screen very fast/often.
project lib 4.0 or 4.03.
Samsung galaxy(9250) with 4.03.
Android 4.4 (API Level 19) introduces a new SYSTEM_UI_FLAG_IMMERSIVE flag for setSystemUiVisibility() that lets your app go truly "full screen." This flag, when combined with the SYSTEM_UI_FLAG_HIDE_NAVIGATION and SYSTEM_UI_FLAG_FULLSCREEN flags, hides the navigation and status bars and lets your app capture all touch events on the screen.
This did work for me:
setOnSystemUiVisibilityChangeListener(new OnSystemUiVisibilityChangeListener() {
#Override
public void onSystemUiVisibilityChange(int visibility) {
if ((visibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
// show my app UI
}
}
});
What I've done is first imported android.view.GestureDetector so I can use it to detect gestures. Android has a number of default gestures that are automatically detected in the GestureDector class. Most of this info is found here, but below is code in a form that I've used in an actual project that works.
First I've made an anonymous class in my Activity (this can be nested wherever, but I tend to make my anonymous classes at the bottom, right before the closing bracket). NOTE: You can also implement OnGestureListener as part of your class, also.
The code below is for using gesture detection to give a simple hide/show.
I've declared and defined my action bar (my UI, which is initially hidden) as an instance variable, so I can access it here, and wherever else, but you can substitute it for a getActionBar().show() and getActionBar().hide() in the case you don't want to declare it as an instance variable. Substitute your UI in the place of the actionBar here:
public class Example extends ActionBarActivity {
// declared in onCreate() method
private android.support.v7.app.ActionBar actionBar;
private GestureDetectorCompat mDetector;
private YourView view1;
private YourView view2;
private YourView view3;
private YourView view4;
// some other code
class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final String DEBUG_TAG = "Gestures in Example Class";
#Override
public boolean onDoubleTap(MotionEvent event) {
Log.d(DEBUG_TAG, "onDoubleTap: " + event.toString());
// if there is a double tap, show the action bar
actionBar.show();
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent event) {
Log.d(DEBUG_TAG, "onSingleTapConfirmed: " + event.toString());
// if the tap is below the action bar, hide the action bar
if (event.getRawY() > getResources().getDimension(R.dimen.abc_action_bar_default_height)) {
actionBar.hide();
return true;
}
return false;
}
#Override
public boolean onDown(MotionEvent event) {
return true;
}
} // end-of-Example Class
Then in my onCreate() I've declared my GestureDetector and also (optionally) set my GestureListeners:
private GestureDetectorCompat mDetector;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// some code here
mDetector = new GestureDetectorCompat(this, new GestureListener());
// this code is for more advanced view logic not needed for a basic set-up
//setGestureListeners();
} // end-of-method onCreate()
Then in order to actually send gestures to be processed we provide the instructions for doing that, there are two ways I know about, first the simplest:
/**
* This method recognizes a touchEvent and passes it to your custom GestureListener
* class.
*/
#Override
public boolean onTouchEvent(MotionEvent event){
this.mDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
The second way is more complex, but if you want to only recognize touch events on certain Views in your layout as in the case where you have overlapping views and can only access the top View, you can create a custom class to pass the event around or up:
class MyOnTouchListener implements View.OnTouchListener {
public boolean onTouch(View v, MotionEvent event) {
if (v.equals(view4)) {
return mDetector.onTouchEvent(event);
} else return false;
}
} // end-of-class MyOnTouchListener
and then use it here:
public void setGestureListeners() {
/* when we return false for any of these onTouch methods
* it means that the the touchEvent is passed onto the next View.
* The order in which touchEvents are sent to are in the order they
* are declared.
*/
view1.setOnTouchListener(new MyOnTouchListener());
view2.setOnTouchListener(new MyOnTouchListener());
view3.setOnTouchListener(new MyOnTouchListener());
view4.setOnTouchListener(new MyOnTouchListener());
} // end-of-method setGestureListeners()
In my setGestureListeners method, I gave them all the same set of commands, that essentially only recognizes touchEvents on view4. Otherwise, it just passes the touchEvent to the next view.
This is code using AppCompat, but if you are not building for older versions, you can use the regular GestureDetector and ActionBar.
Have you tried adding code to only show your UI when the state has changed? You have to maintain the last known visibility and only show your UI when you first come into being visible:
int mLastSystemUiVis;
#Override
public void onSystemUiVisibilityChange(int visibility) {
int diff = mLastSystemUiVis ^ visibility;
mLastSystemUiVis = visibility;
if ((diff&SYSTEM_UI_FLAG_VISIBLE) != 0
&& (visibility&SYSTEM_UI_FLAG_VISIBLE) == 0) {
// DISPLAY YOUR UI
}
}
Code sample adopted from the Android docs
The method Activity.onTouchEvent() gets called at the end of the responder chain (meaning after all other views have had a chance to consume the event). If you tap on a view that is interested in touch (i.e. a Button or EditText) there's a good chance your Activity will never see that event.
If you want to have access to touches before they every get dispatched to your view(s), override Activity.dispatchTouchEvent() instead, which is the method called at the beginning of the event chain:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
//Check the event and do magic here, such as...
if (event.getAction() == MotionEvent.ACTION_DOWN) {
}
//Be careful not to override the return unless necessary
return super.dispatchTouchEvent(event);
}
Beware not to override the return value of this method unless you purposefully want to steal touches from the rest of the views, an unnecessary return true; in this spot will break other touch handling.
I got this problem too, and I found this http://developer.android.com/reference/android/view/View.html#SYSTEM_UI_FLAG_HIDE_NAVIGATION
So, no way to help. Even the android system packaged Gallery app used SYSTEM_UI_FLAG_LOW_PROFILE instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION in photo page view. This is at least what we can do.
I had a very similar issue with trying to update the UI from an onTouchEvent() requiring two touches to work, and I tried a bunch of elaborate stuff before finally getting it to work on the first click.
In my case, I was showing a previously hidden item, then getting its height, then moving a button down by that height. The problem I ran into is that the height was showing as 0 until after the first touch event finished. I was able to solve this by calling show() during ACTION_UP for the onTouchEvent() instead of its ACTION_DOWN. Maybe it'd work if you did something similar?
Try to use:
getWindow().getDecorView().setSystemUiVisibility(View.GONE);
instead:
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
After that you can use normal activity in fullscreen and if you want nav keys you need to swipe from bottom to up. Working for me at Galaxy Tab 2 with android 4.1.2
How do you disable multiple clicks on a menu option, before the first click is processed?
You can set the visibility or enable/disable the item by code.
MenuItem item = menu.findItem(R.id.your_item);
item.setVisible(true);
item.setEnabled(false);
Of course you have to check somewhere whether to enable oder disable the icon.
Psuedo/Android answer:
private boolean clicked = false;
#Override
public onClick(View v){
if(!clicked){
clicked = true;
// do your processing - one click only
super.onClick();
}
}
EDIT
or even better after the first click you can call yourView.setOnClickListener(null); to remove the onClick
I know that this is an old question but I want to share a reactive approach.
Fragment/Activity
onOptionsItemSelected:
if (item.getItemId() == yourId) {
viewModel.showTopUp()
return true;
}
return super.onOptionsItemSelected(item);
In the ViewModel create a PublishSubject and throttle the requests to prevent multiple clicks:
private PublishSubject<Context> topUpClicks = PublishSubject.create();
public void showTopUp(Context context) {
topUpClicks.onNext(context);
}
private void handleTopUpClicks() {
disposables.add(topUpClicks.throttleFirst(1, TimeUnit.SECONDS)
.doOnNext(transactionViewNavigator::openTopUp)
.subscribe());
}