I've created a custom ListView by extending AdapterView and I'd like to handle key events inside of this View. I've overridden onKeyDown, but don't ever see it called. I even tried overriding dispatchKeyEvent and that never gets called either. I've also made my View focusable with setFocusable(true).
Also, I'm a bit confused as to which view actually DOES have focus. In HierarchyView, every single view in the tree says hasFocus = false.
How can I receive key events in my View?
Looks like I can answer my own question here.
Taking a look at AdapterView, you can see that it overrides setFocusable with the following implementation:
#Override
public void setFocusable(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableState = focusable;
if (!focusable) {
mDesiredFocusableInTouchModeState = false;
}
super.setFocusable(focusable && (!empty || isInFilterMode()));
}
As you can see from the final call to super.setFocusable there, the View will not be focusable if there's no data bound yet from the adapter (meaning the adapter is either null or its is <= 0).
So to resolve this, simply add setFocusable(true) at the end of your setAdapter() implementation.
Related
Currently, when I
Launch a new Activity by clicking on AutoCompleteTextView's drop down
Close the launched Activity
AutoCompleteTextView's drop down is hidden.
I would like to preserve AutoCompleteTextView's drop-down state which includes
Drop down should not be hidden when gets back from launched Activity
Drop down's scroll position should be preserved.
I'm not exactly sure the reason why AutoCompleteTextView's dropdown will be hidden when I back from launched Activity. Hence, I had tried 2 things
Change windowSoftInputMode of launched Activity from stateAlwaysHidden to stateUnchanged.
In onActivityResult, when the launched Activity is closed, perform mSearchSrcTextView.showDropDown(); explicitly.
However, I am still facing the issue. The previous scroll position of AutoCompleteTextView's dropdown is not preserved. It is reset back to top of the list.
Here's the screen-shot to better illustrate the problem I am facing.
(Current AutoCompleteTextView's dropdown is scrolled to the end. I click on the last item and launch a new Activity)
(New Activity is launched. Now, I click on the BACK soft key twice, to close the keyboard and then close the Activity)
(Due to the explicit call of mSearchSrcTextView.showDropDown(); in onActivityResult, the drop down is shown again. However, its previous scrolled position is not being preserved. Start of list is being shown instead of end of list)
I was wondering, is there any way to preserved the AutoCompleteTextView's DropDown state, when closing a previous launched Activity?
For AutoCompleteTextView, it has a method called dismissDropDown(). I believe when back from newly launched activity, this function is being triggered. So we workaround this problem by extending AutoCompleteTextView & override it's dismissDropDown().
We add a boolean flag temporaryIgnoreDismissDropDown, to indicate whether to temporarily ignore dismissDropDown.
public class MyAutoCompleteTextView extends AutoCompleteTextView {
private boolean temporaryIgnoreDismissDropDown = false;
.....
#Override
public void dismissDropDown() {
if (this.temporaryIgnoreDismissDropDown) {
this.temporaryIgnoreDismissDropDown = false;
return;
}
super.dismissDropDown();
}
public void setTemporaryIgnoreDismissDropDown(boolean flag) {
this.temporaryIgnoreDismissDropDown = flag;
}
}
Before launching new Activity, we set dismissDropDown to true. After coming back from launched activity, dismissDropDown is called. The override method checks if temporaryIgnoreDismissDropDown is true, just set it to false & do nothing. So the real dismissDropDown is skipped.
// myAutoCompleteTextView is instance of MyAutoCompleteTextView
myAutoCompleteTextView.setTemporaryIgnoreDismissDropDown(true);
// launch new Activity
startActivity(....);
Hope this help, good luck!
After an hour of coding, much trying and a lot of googling around, I've put together a solution that does just what you want. It uses reflection to access the ListView within the Dropdown menu and to access the dropdown state when you leave the activity.
The code for this is kinda long, so I'll walk you through all the parts. Firstly, I have some variables we will need:
boolean wasDropdownOpen;
int oldDropdownY;
Handler handler;
The handler will be neccessary for later, as we have to do a little trick in the onResume() method. Initialize it as usual in your onCreate() method:
handler = new Handler(getMainLooper());
Now, let's get to the tricky part.
You need to call the following method before you start any activity. It can't be done in onPause() since the Dropdown menu is already closed when this method is called. In my test code I've overridden the startActivity() and startActivityForResult() method, and called it there, but you can do this however you like.
private void processBeforeStart() {
ListPopupWindow window = getWindow(textView);
if(window == null) return;
wasDropdownOpen = window.isShowing();
ListView lv = getListView(window);
if(lv == null) return;
View view = lv.getChildAt(0);
oldDropdownY = -view.getTop() + lv.getFirstVisiblePosition() * view.getHeight();
}
This will save your dropdown ListView's state for later. Now, we will load it. This is the onResume() method we will need for this:
#Override
protected void onResume() {
super.onResume();
if (wasDropdownOpen)
textView.showDropDown();
handler.postDelayed(new Runnable() {
#Override
public void run() {
ListView lv = getListView(getWindow(textView));
if (lv != null)
scrollToY(lv, oldDropdownY);
}
}, 150);
}
First of all, let me explain this method. We saved the state if the dropdown was open, so we reopen the menu if it was. Simple. The next part is the scrolling. We need to do this in a Handler because the UI is not yet fully loaded when onResume() is called and therefore the ListView is still inaccessible.
The scrollToY() method you see there is a modified version of the code from this post, as Android's ListView does not have an inbuilt method to set the scroll position as precisely as we want it here.
The implementation of this method is as follows:
private void scrollToY(ListView lv, int position) {
int itemHeight = lv.getChildAt(0).getHeight();
int item = (int) Math.floor(position / itemHeight);
int scroll = (item * itemHeight) - position;
lv.setSelectionFromTop(item, scroll);// Important
}
Now, you've probably seen the getWindow() and getListView() methods I've used above. These are the reflection methods, which we have to use because Android does not expose a public API to access the ListView within the ListPopupWindow of the AutoCompleteTextView. Additionally, the DropDownListView, a subclass of ListView that is actually used within this object, is not visible to the oudside as well, so we have to use Reflection once again.
Here is the implementation of my two helper methods:
private ListView getListView(ListPopupWindow window) {
for (Field field : window.getClass().getDeclaredFields()) {
if (field.getType().getName().equals("android.widget.DropDownListView")) {
field.setAccessible(true);
try {
return (ListView) field.get(window);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
private ListPopupWindow getWindow(AutoCompleteTextView tv) {
Class realClass = tv.getClass().getName().contains("support") ? tv.getClass().getSuperclass() : tv.getClass();
for (Field field : realClass.getDeclaredFields()) {
if (field.getType().getName().equals(ListPopupWindow.class.getName())) {
field.setAccessible(true);
try {
return (ListPopupWindow) field.get(tv);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
I've tested this on Android O (API level 26) and it works just as you described you want it to work.
I hope that the effort I put into this answer gets me a chance on the Bounty ;-)
It sounds like you've already figured out how to show the drop-down on demand (via showDropDown()), so I'll only address how to restore the scroll position of the dropdown.
You can access the first visible position of the dropdown like this:
autocomplete.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int firstVisiblePosition = parent.getFirstVisiblePosition();
// save this value somehow
}
});
Save the value of this int however you'd like (in memory, via onSaveInstanceState(), pass it through to the started activity so that it can pass it back via onActivityResult(), etc). Then, wherever you re-show the dropdown, do this:
autocomplete.showDropDown();
autocomplete.setListSelection(firstVisiblePosition);
The shortcoming of this technique is that it makes the item at firstVisiblePosition completely visible, so if it was halfway scrolled out of view, the list position won't be restored perfectly. Unfortunately, I don't believe there's any way to save/restore this partial-view offset.
So I am currently in FragmentA.java, a class which consists of different EditTexts and Checkboxes for users to press and input.
My question is, I have this reset button, how can I reset the entire fragment view? (e.g. an EditText will be set to empty string, or with the value 0, as it is when created).
P.S. of course I can set the editText/Checkboxes one by one programically, however there are quite a lot of them and other views, so I would like to know if there is a good way to reset all of them.
Let's break it down into steps:
1. Getting the references
How this is done depends on what you already have. If the fields are created in-code, it's easy: Just store the references in a List<CommonBaseType>.
If they are loaded from an XML Layout, there are multiple options. If you just want all views of certain type(s) to be reset, you can iterate through the view hierarchy by getting a reference to their holding ViewGroup (the layout) and iterate over the children with getChildCount() and getChildAt(int). Then, check the type of the child. If it's a ViewGroup, check it's children. If it's an EditText or CheckBox, add them to the list.
If you need more control and don't want all views to be reset, you can tag the ones you want with something. You can tag a view in XML by using the android:tag-attribute and find them after inflation using the View.findViewWithTag(Object)-method.
2. Resetting
Now that you have the references, you can reset them by simply iterating over the collection you made in step 1 and handle them depending on their type. Some pseudo code with that:
List<View> form_elements = findViewsToReset();
for (View element : form_elements){
if (element instanceof EditText){
((EditText) element).setText("");
} else if (element instanceof CheckBox){
((CheckBox) element).setChecked(false);
}
// and so forth...
}
Something like this will reset all fields in your form to a default-value, depending on their type.
3. Resetting back to their original values
If you want to reset the views to their original values, you should "index" those when the initial values are set (which might be directly after inflation, if you set the values via XML).
To do this, simply run through your list from step 1 and make a mapping from their ID to their value at that point:
List<View> form_elements = findViewsToReset();
Map<Integer, Object> default_values = new HashMap<>(form_elements.size());
for (View element : form_elements){
if (element.getId() == View.NO_ID){
// We have nothing to identify this view by...
continue;
}
// Store the default values away:
if (element instanceof EditText){
default_values.put(
element.getId(),
((EditText) element).getText()
);
} else if (element instanceof CheckBox){
default_values.put(
element.getId(),
((CheckBox) element).isChecked()
);
}
// and so forth...
}
Later when you want to reset the form-elements, you can just iterate the list again and get the default values from the map. Cast them depending on the type of field (EditText -> String, CheckBox -> Boolean, etz) and set the values.
Bonus: Nasty RadioGroup
Resetting a RadioGroup is simply archived by calling clearCheck() on it, which has the nasty side-effect of triggering the associated OnCheckedChangeListener (which you might not want, depending on what you're doing in the listener).
The simplest way around this is to un-register the listener before calling clearCheck() and re-registering it afterwards. This can be archived by overriding RadioGroup.clearCheck():
/**
* When {#link #clearCheck()} is called, the registered (if any) {#link android.widget.RadioGroup.OnCheckedChangeListener} will <b>not</b> be called.
* #author Lukas Knuth
* #version 1.0
*/
public class CustomRadioGroup extends RadioGroup {
private OnCheckedChangeListener checked_change_listener;
public CustomRadioGroup(Context context) {
super(context);
}
public CustomRadioGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
// We need to store this ourselves, since there is no getter-method for the listener -.-
this.checked_change_listener = listener;
super.setOnCheckedChangeListener(listener);
}
#Override
public void clearCheck() {
// 1. unregister the listener:
super.setOnCheckedChangeListener(null);
// 2. Clear
super.clearCheck();
// 3. restore the listener like it was before:
super.setOnCheckedChangeListener(this.checked_change_listener);
}
}
I know the reason why the getView method of an Adapter is called more than once, but is there a way for knowing which of the returned view will be actually displayed on the activity?
Until now I put all the returned view linked to the same position in a list and, every time I need to modify a shown view I modify all the views corresponding to that position (one of those will be the right one...).
Surely is not the best way...
Here a piece of code of my adapter:
class MyAdapter extends BaseAdapter {
Vector<ImageView> vectorView[] = new Vector<ImageView>[25];
public MyAdapter(Activity context) {
...
}
public doSomeStuffOnAView(int position) {
// needs to know which view corresponds to the given position
// in order to avoid the following for cycle
for (ImageView iv: vector[position]) {
// do something
}
}
public View getView(int position, ...) {
ImageView childView = ...;
if (vector[position]==null) {
vector[position]=new Vector<ImageView>();
}
vector[position].add(childView);
return childView;
}
}
The method getView(...) might be called more than once for each position, but just one returned view per position will be shown on the activity. I need to know which of these. I thought it was the one returned the last time getView has been called for a position, but it is now always true.
one way but it is not standard.. you can directly check with integer variable i.e. hardcoded type. You have list of views in your xml file. jst cross check & compare with integer variable.
I have a ListFragment that uses a header view. Both the header's contents and the list's are fetched from a background task. In order to not re-fetch the data on configuration changes, I am calling setRetainInstance and keeping the data on the fragment.
When the the configuration changes, the view is recreated, so it removes the header view that I previously populated. Since now I already have the data, I should just re-add the header view to the list.
Unfortunately when I try doing this... boom!
java.lang.IllegalStateException: Cannot add header view to list -- setAdapter
has already been called.
Apparently, even tho the view is destroyed and onCreateView is called again, the list's adapter is already set (or the state is retained), making it impossible to add the header view again.
How can I keep the ListView's header or redraw it without recreating the fragment on orientation changes?
This is intended behaviour, take a look at the Android source code here for guidance on API 17, but really any will do. The relevant part is:
Add a fixed view to appear at the top of the list. If addHeaderView is
called more than once, the views will appear in the order they were
added. Views added using this call can take focus if they want. NOTE:
Call this before calling setAdapter. This is so ListView can wrap the
supplied cursor with one that will also account for header and footer
views.
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (mAdapter != null && ! (mAdapter instanceof HeaderViewListAdapter)) {
throw new IllegalStateException(
"Cannot add header view to list -- setAdapter has already been" +
"called."); // Edit: SK9 wrapped this.
}
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mHeaderViewInfos.add(info);
// in the case of re-adding a header view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
The adapter is not null when you come to add the header again and an exception is being raised. To resolve your issue, something along the following lines will do just fine:
setListAdapter(null);
getListView().addHeaderView(mHeader);
setListAdapter(new MyAdapter(getActivity(), items));
I wouldn't even classify this as a workaround. I encountered the same problem and this worked for me.
Apparently footers are treated very differently, see here:
public void addFooterView(View v, Object data, boolean isSelectable) {
// NOTE: do not enforce the adapter being null here, since unlike in
// addHeaderView, it was never enforced here, and so existing apps are
// relying on being able to add a footer and then calling setAdapter to
// force creation of the HeaderViewListAdapter wrapper
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
// in the case of re-adding a footer view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
it's a know issue, but you can resolve it like this:
add header before the set adapter and remove him
Yes, it's a known issue, but can be avoided with the proper approach.
It seems that a solution similar to your problem exists.
These guys found a workaround: setSelected in OnItemClick in ListView
Hope it helps ;)
Others has the problem as doesn't working, I have the problem it is working ( and it shouldn't )
I have a data model, which is saved, and need to loaded back to GUI, Activity. It has a few spinner value.
The data is place to a common accesible class, a reference holder.
The activity's onCreate it will check if is on edit mode or not with:
editMode = getIntent().getBooleanExtra(EDIT_MODE_KEY, false);
It will load the UI elements from xml, and start selecting, filling values.
At editing mode, and at creation mode it should select values what has the data model.
At runtime ( after onResume() ) has some workflow: is something is selected at spinner1, than should refresh the spinner2 adapter content and so on.
It doesn't worked the plain .setSelection(positiontoSelect); so I have added a delayed post, now is working.
My problem is: I would like remove for temp the selection listener, call the selection and add back the listener.
Here is the code, which should be modified:
if (editedTimezonePosition > -1) {
final int positiontoSelect = editedTimezonePosition;
new Handler().postDelayed(new Runnable() {
public void run() {
OnItemSelectedListener listener = spSelectTimezone.getOnItemSelectedListener();
spSelectTimezone.setOnItemSelectedListener(null);
spSelectTimezone.setSelection(positiontoSelect);
spSelectTimezone.setOnItemSelectedListener(listener);
}
}, 250);
}
setting to null the listener has no effect: I am getting callback to my listener method.
If you have any idea how to fix it, please share it!
You could put a counter variable in your onItemSelected method. If it is 0 (meaning the first time the method has been called), do nothing but increment the variable. If it is greater than 0, execute the rest of your code.
private int mSpinnerSelectionCount=0;
public void onItemSelected(AdapterView<?> parent, View view,
int pos, long id) {
if(mSpinnerSelectionCount == 0){
mSpinnerSelectionCount++;
} else {
// Your normal selection code here
}
}