My app has a ListView and an EditText sitting below it. For some reason, the TAB key doesn't trigger the onKeyListener. All other keys I'm handling (DEL, ENTER, DPAD_UP/DOWN/CENTER) are received just fine. I added a breakpoint in dispatchKeyEvent, again no luck receiving TAB events.
My app previously had a large TextView for displaying text and during this time, TAB events were received fine. The ListView has now replaced the TextView.
I'm completely mystified as to why the TAB event is no longer being received. This is on a stock Xoom, running ICS 4.0.4 & stock N1, with 2.3.6.
I've compared my current code against the version using a TextView and much of the code is just to handle the ListView in the place of the TextView. Apart from nextFocusLeft and nextFocusRight attributes, nothing else has changed for the EditText.
Edit: I just tried with Go Keyboard and Hacker's Keyboard and TAB is received fine. It appears this is with just some virtual keyboards
I think I may see the problem. Looking at the source for ListView.java, there is a mechanism to consume key events that shift focus within a list item. Check out the comments preceding this method as well as the block of comments in the middle of the method.
/**
* To avoid horizontal focus searches changing the selected item, we
* manually focus search within the selected item (as applicable), and
* prevent focus from jumping to something within another item.
* #param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}
* #return Whether this consumes the key event.
*/
private boolean handleHorizontalFocusWithinListItem(int direction) {
if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) {
throw new IllegalArgumentException("direction must be one of"
+ " {View.FOCUS_LEFT, View.FOCUS_RIGHT}");
}
final int numChildren = getChildCount();
if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
final View selectedView = getSelectedView();
if (selectedView != null && selectedView.hasFocus() &&
selectedView instanceof ViewGroup) {
final View currentFocus = selectedView.findFocus();
final View nextFocus = FocusFinder.getInstance().findNextFocus(
(ViewGroup) selectedView, currentFocus, direction);
if (nextFocus != null) {
// do the math to get interesting rect in next focus' coordinates
currentFocus.getFocusedRect(mTempRect);
offsetDescendantRectToMyCoords(currentFocus, mTempRect);
offsetRectIntoDescendantCoords(nextFocus, mTempRect);
if (nextFocus.requestFocus(direction, mTempRect)) {
return true;
}
}
// we are blocking the key from being handled (by returning true)
// if the global result is going to be some other view within this
// list. this is to acheive the overall goal of having
// horizontal d-pad navigation remain in the current item.
final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
(ViewGroup) getRootView(), currentFocus, direction);
if (globalNextFocus != null) {
return isViewAncestorOf(globalNextFocus, this);
}
}
}
return false;
}
Are there multiple focusable items within a single list element? If so, this code will consume the tab key. If that is the case, then you may want to make some of the items unfocusable or consider another design option.
Related
In my project, Expandable list view is common for all logins, but in new update, based on role of login user, i want to stop showing children of certain groupviews. I managed to put an "Access Denied" dialog, but I cannot stop, the expansion of groupview or showing child view. Is there a way for that?
I tried to pass role to the adapter of the expandableListAdapter class and write some cases in getChildView like if (groupPosition==1 && role == 1) I inflated an empty layout. This way I cannot prevent expansion, the child view is still being shown but it is empty and on clicking of that, it is throwing null pointer exception.
I found a way for that, I returned the child count for my specific cases 0
#Override
public int getChildrenCount(int groupPosition) {
Log.e("getChildrenCount", "getChildrenCount");
if ((this.listDataChild.get(this.listDataHeader.get(groupPosition)) == null)
|| (roleId.equalsIgnoreCase("2") && groupPosition!=1)
|| (roleId.equalsIgnoreCase("3") && groupPosition!=2)
|| (roleId.equalsIgnoreCase("5") && groupPosition!=4) )
return 0;
else
return this.listDataChild.get(this.listDataHeader.get(groupPosition)).size();
}
This worked for me
Thanks
What is so special about Edittext that it can retain the value but not Textview and some other widgets and we have to use onSavedInstance() method for them.
What is the magic behind EditText specially that it can retain the values?
If someone can tell how it works internally.
<----Update---->
How it works internally, Please point to that part of the code which explains this scenario.
What is the magic behind EditText specially that it can retain the
values? How it works internally, Please point to that part of the code
which explains this scenario.
It is the selectable property which brings the difference. The following if condition in TextView.onSaveInstanceState takes care of it.
#Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
boolean save = mFreezesText;
int start = 0;
int end = 0;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
save = true;
}
}
...
}
By default TextView can't be selected. Hence getSelectionStart() and getSelectionEnd() returns -1, there by save variable holds false value. To make it retain it's content on orientation change, set the attribute textIsSelectable to true.
Since EditText is selectable by default, getSelectionStart() and getSelectionEnd() always return value >=0, there by save variable holds true value and the content gets saved.
Note : By default freezesText is disabled. Hence mFreezesText value is false.
By default, EditText view saves its state - This is accomplished by setting flags in code telling the view to save state when view is not visible or lost focus. The text is automatically saved and restored after rotating device. The automatic saving of state of EditText view can be disabled in the XML layout file by setting the android:saveEnabled property to false:
android:saveEnabled="false"
Or programmatically, call view.setSaveEnabled(false).
saveEnabled controls whether the saving of this view's state is enabled (that is, whether its onSaveInstanceState() method will be called). Note that even if freezing is enabled, the view still must have an id assigned to it (via setId()) for its state to be saved. This flag can only disable the saving of this view; any child views may still have their state saved. saveEnabled attribute is part of android View - View Code. Here is related parts of code:
public boolean isSaveEnabled() {
return (mViewFlags & SAVE_DISABLED_MASK) != SAVE_DISABLED;
}
...
public void setSaveEnabled(boolean enabled) {
setFlags(enabled ? 0 : SAVE_DISABLED, SAVE_DISABLED_MASK);
}
...
void setFlags(int flags, int mask) {
int old = mViewFlags;
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;
if (changed == 0) {
return;
}
int privateFlags = mPrivateFlags;
/* Check if the FOCUSABLE bit has changed */
if (((changed & FOCUSABLE_MASK) != 0) &&
((privateFlags & HAS_BOUNDS) !=0)) {
if (((old & FOCUSABLE_MASK) == FOCUSABLE)
&& ((privateFlags & FOCUSED) != 0)) {
/* Give up focus if we are no longer focusable */
clearFocus();
} else if (((old & FOCUSABLE_MASK) == NOT_FOCUSABLE)
&& ((privateFlags & FOCUSED) == 0)) {
/*
* Tell the view system that we are now available to take focus
* if no one else already has it.
*/
if (mParent != null) mParent.focusableViewAvailable(this);
}
}
....
Quoting from this article:https://tekeye.uk/android/examples/saving-activity-state
"Switching the device from portrait to landscape causes Android to stop and restart the Activity, allowing Activities the opportunity to redraw a screen for the different dimensions. With stopping and starting an Activity a common occurrence users would be annoyed if input kept being lost. Android activites have a pair of methods called onSaveInstanceState(Bundle) and onRestoreInstanceState(Bundle) which are automatically used by input Views to save their data. These methods only work if the Views that take data can be identified, hence the need for the EditText (and all screen items) to have an id. A bonus is that this method pair can be overridden in an Activity so that state variables not associated with input fields can also be saved and restored."
Remove the id from an edittext and try it :)
Try this for your textview I think it may help you
<TextView
...
android:freezesText="true" />
Android View class have protected methods
protected Parcelable onSaveInstanceState ()
protected void onRestoreInstanceState (Parcelable state)
so any view can override those methods and save View's state information with them.
TextView extends View class and have implementation of protected Parcelable onSaveInstanceState() and protected void onRestoreInstanceState (Parcelable state) within it.
lets look at implementation of onSaveInstanceState()
#Override
public Parcelable onSaveInstanceState()
{
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
boolean save = mFreezesText;
int start = 0;
int end = 0;
if (mText != null)
{
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0)
{
// Or save state if there is a selection
save = true;
}
}
...
}
as you can see here save feature is depending on single flag "save"
so if user explicitly specify mFreezesText = true then it will save
the text
another possiblity if there is selection cursor then it will at least return getSelectionStart() = 0 and not -1 that will cause TextView to make save=true to save state of TextView and thats what happening with EditText as EditText have selection cursor and it does extends TextView.
I have a huge list of items that I present in an AlertDialog. I would like to present the user the list scrolled to the most likely area they will select one item from. I'm using
AlertDialog.Builder.setSingleChoiceItems(myAdapter, ...).
ArrayAdapter<MyType> myAdapter;
The problem I'm having a hard time with is how to scroll to an item when it's not logically correct to present the item as selected.
I tried getting the ListView from the resulting AlertDialog. But it's empty (even after the Builder creates and shows it).
I tried forcing a populated ListView by inflating a plane ListView in res/layout. listView.scrollTo(x, y) didn't seem to have an effect.
I tried up setting the OnShowListener for the AlertDialog. onShow() is never invoked.
Does anyone know of a work around?
You could use the functions which are part of the ListView class:
smoothScrollByOffset(int offset);
or
smoothScrollToPosition(int position);
Or
if you want to scroll one by one you could use functions like:
private void scrollToNext() {
int currentPosition = getListView().getFirstVisiblePosition();
if (currentPosition == getListView().getCount() - 1)
return;
getListView().setSelection(currentPosition + 1);
getListView().clearFocus();
}
private void scrollToPrevious() {
int currentPosition = getListView().getFirstVisiblePosition();
if (currentPosition == 0)
return;
getListView().setSelection(currentPosition - 1);
getListView().clearFocus();
}
On the default ListViews if you long press on an item the background fades from dark blue to light blue(on Galaxy Tab. Its orange to light orange on some other devices) during the time it takes to register a LongClick. I am assuming this is done somehow with a selector but thus far I've only seen how to use selectors for state:selected, state:focused, and state:pressed.
This page doesn't seem to show anything about a LongClick state so perhaps my assumption that this is accomplished with a selector is incorrect?
Can anyone fill me in on how the default ListView gets this effect and how I can apply it to other views?
So it turned out to be a little bit more difficult than I had thought but I have it working almost correctly now.
Here is the OnTouchListener I ended up using:
listOnTouchListener = new OnTouchListener() {
public boolean onTouch(View v, MotionEvent me){
if (me.getAction() == MotionEvent.ACTION_DOWN){
//This means a finger has come down on top of our view
//We are going to start the animation now.
Log.i(myTag, "Action Down");
Context mContext = getApplicationContext();
Resources res = mContext.getResources();
TransitionDrawable transition = (TransitionDrawable) res.getDrawable(R.drawable.listtransition);
v.setBackgroundDrawable(transition);
//LongClick took approx. 510-530 milliseconds to register after OnTouch. So I set the duration at 500 millis.
transition.startTransition(500);
}else if (me.getAction() == MotionEvent.ACTION_UP){
//This means we didn't hold long enough to get a LongClick
//Set the background back to the normal one.
v.setBackgroundResource(R.drawable.bubblelight);
}else if (me.getAction() == MotionEvent.ACTION_MOVE){
//Do Nothing
}else if (me.getAction() == MotionEvent.ACTION_CANCEL){
Log.i(myTag, "Action Cancel");
//This means we are scrolling on the list, not trying to longpress
//So set the background back to the normal one.
v.setBackgroundResource(R.drawable.bubblelight);
}
return false;
}
};
I also used an OnLongClickListener inside this I set the background back to the normal one.
Here is the Transition XML:
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#drawable/bubblelight1" />
<item android:drawable="#drawable/bubbledark" />
</transition>
You may be asking whats with the bubblelight1? More on that in a moment.
Here is the getView() Method I use inside my adapter to return the views that get displayed in the List:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.row, null);
}
Status s = items.get(position);
if (s != null) {
v.setOnTouchListener(listOnTouchListener);
v.setOnLongClickListener(listOnLongClickListener);
tweetTxt = (TextView) v.findViewById(R.id.tweetTxt);
timeTxt = (TextView) v.findViewById(R.id.timeTxt);
if (tweetTxt != null) {
tweetTxt.setText(s.getText());
tweetTxt.setOnTouchListener(gestureListener);
}
if(timeTxt != null){
timeTxt.setText(getTimeFromNow(s.getCreatedAt().getTime()));
//timeTxt.setText(s.getCreatedAt().toLocaleString());
}
}
LinkifyWithTwitter.addLinks(tweetTxt, Linkify.ALL);
return v;
}
v.setOnTouchListener(listOnTouchListener); and
v.setOnLongClickListener(listOnLongClickListener); are the lines that set up the view with the Listeners that I've shown above.
Now about the bubblelight1. bubblelight and bubbledark are the nine patch images I am using when I tried this the first time whenever the transition started instead of the background transitioning from bubblelight to bubbledark, bubblelight would grow bigger and bubbledark would appear inside of bubblelight. So I had a big bubble that was light, a smaller bubble that was dark, then the text inside that. To fix this issue I made a copy of bubblelight and made the bottom and right edges of the ninepatch completely filled in. I just had to do the first image in the transition though, not the second. If I did the second image that way then my text would jump out of the bubble and some it would get shown over the top and along the sides of the bubble. I am not entirely sure why this was happening, or why this fix happened to work. But It does the job for now.
http://developer.android.com/guide/topics/ui/menus.html#context-menu
When the user performs a long-press on an item in a ListView and the list is registered to provide a context menu, the list item signals to the user that a context menu is available by animating its background color—it transitions from orange to white before opening the context menu. (The Contacts application demonstrates this feature.)
So its an animation that is used. I believe it uses the View's own default On...() methods to display it. You may need to worry about giving it clickable="true" or longClickable="true" attributes.
In addition to your OnTouchListener, you can use a Runnable to revert back the background to it's original state so that you don't need to explicitly do it in the OnLongClick handler.
Handler myHandler = new Handler();
...
transition.startTransition(ViewConfiguration.getLongPressTimeout());
ResetBG r = new ResetBG(transition);
myHandler.postDelayed(r, ViewConfiguration.getLongPressTimeout());
where ResetBG is:
class ResetBG implements Runnable {
protected TransitionDrawable myTran;
public Runnable(TransitionDrawable tran) {
myTran = tran;
}
public void run() {
myTran.resetTransition();
}
}
I have 20 cards of which there are 10 pair of images. The player is meant to find a match of each card/image.
The problem is that if a player taps or clicks each card twice then that card gets decremented from the remaining ones . I need to disable the click listener of the ImageView. How can do that?
ivOne = (ImageView)findViewById(R.id.ivOne);
ivOne.setId(a[0]);
//final ImageView ivOne = (ImageView)findViewById(R.id.ivOne);
//ivOne.setEnabled(false);
ivOne.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
ivOne.setBackgroundResource(images[a[0]]);
Log.e("r[0]:", Integer.toString(a[0]));
if (firstTap)
{
firstId = v.getId();
firstTap = false;
}
else
{
//ivOne.setEnabled(false);
int secondId = v.getId();
Log.e("secondId", Integer.toString(secondId));
if ((secondId == firstId) && (score != 0))
{
//ivOne.setEnabled(false);
if (ivOneScored == false)
{
score--;
ivOneScored = true;
}
}
firstTap = true;
}
tvScore.setText("Remaining:" + Integer.toString(score));
}
});
I have a memory matching game on the market that works very similar. What I did was make a class that represents the card (with image and id properties) and assign an id (0-9) to the cards and the image as I loaded them into a temporary arraylist of just the 10 image objects.
public class GameTile {
public int id;
public Drawable image;
}
Then I iterated over the list twice and added the cards to the main arraylist that I use for my adapter... after which I used Collections.shuffle to shuffle the cards... Now... on to the selection. AS you know, in a memory game, you only want 2 cards flipped at any time. I am using a gridview to hold my cards... so what I did was use the OnItemClickListener from the gridview and not the imageview. Secondly, when a card is clicked, I add the position to another arraylist called "selected" that never contains more than 2 items... the position of the items we are attempting to match... In the OnItemClickListener, when the event is fired, I check to see if the item already exists in "selected" and return if it does... in effect ignoring the click.
if (selected.contains(position)) {
return;
}
When "selected" contains 2 items, I ignore all clicks until the handler finishes checking for a match.
if (selected.size() > 1) {
return;
}
So when a user has selected 2 items, I set a handler to call a runnable that checks for a match. If a match is made (by comparing the id fields I set when I first loaded the images), I add the two positions to another arraylist that contains only matched items and that handler also clears "selected" and, if there were matches, I change the images to blanks. When all 20 cards have been matched I fire a win which does all of the win stuff and resets the gameboard.
You have to make sure that those cards have the same symbol, but are not the same card.
So your line
if ((secondId == firstId) && (score != 0))
must rather look like something in the lines of
if ((firstCard != secondCard) && (secondId == firstId) && (score != 0))
where firstCard and secondCard identify the card on the table (not the symbol on the card)
If I understand you right, you just need to say view.setOnClickListener(null) if those two match? If you want to disable both of them, you just store a map to link an ID to a View, and then clear the click listener for both "view" and whichever view belongs to the second ID.