I know that this question has been answered multiple times, but I have not found any of them satisfactory and certainly not elegant.
The issue is that OnItemSelected gets fired not only when the user selects an item, but also when the selection is programmatically set.
Some answers propose setting a flag for times when the programmer is setting a value for the spinner. However sometimes other android code will set the value outside of your code.
A common place for android to set the value is upon instantiation of the spinner. Some answers address that particular issue. However, there are numerous places where Android will break down and reinstantiate a spinner. It is not elegant to track down all of them.
So the question is: how does one attach their OnItemSelectedListener code ONLY to selections made by user interaction with the UI?
Spinner.setOnItemClickListener seems like it would answer this issue perfectly, but Google has it disabled
AdapterView.setOnClickListener also seems like a natural candidate but it also generates a runtime error
Next place is to extend Spinner and start overriding methods but that of course means messing with the APIs (which I'd rather not do, or I'd at least like to have other users working on it with me)
So I am posting an answer, but any criticisms, improvements, or other more elegant answers are welcome.
The key is overriding onClick to set a flag which ties the onItemSelectedListener to user interaction and fires the onItemClickedListener.
If you don't feel comfortable using the API setOnItemClickedListener (for future compatibility perhaps), you can of course substitute your own method. I just felt like onItemClickedListener should have been implemented to this effect the whole time, so that is my subtle protest.
Also, if anyone can think of a reason that the spinnerTouched flag gets short circuited (stays true for longer than it should), please let us know so that it can be addressed. It seems to work pretty well so far though.
public class OnItemClickSpinner extends Spinner implements AdapterView.OnItemSelectedListener {
boolean spinnerTouched = false;
OnItemClickListener onItemClickListener = null;
OnItemSelectedListener onItemSelectedListener = null;
public OnItemClickSpinner(Context context) {
super(context);
super.setOnItemSelectedListener(this);
}
public OnItemClickSpinner(Context context, AttributeSet attrs) {
super(context, attrs);
super.setOnItemSelectedListener(this);
}
public OnItemClickSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setOnItemSelectedListener(this);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
spinnerTouched = true;
return super.onTouchEvent(event);
}
#Override
public void setOnItemClickListener(OnItemClickListener listener) {
onItemClickListener = listener;
}
#Override
public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
this.onItemSelectedListener = onItemSelectedListener;
super.setOnItemSelectedListener(this);
}
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(spinnerTouched && this.onItemClickListener!=null) this.onItemClickListener.onItemClick(parent,view,position,id);
if(this.onItemSelectedListener!=null) this.onItemSelectedListener.onItemSelected(parent,view,position,id);
spinnerTouched=false;
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
if(this.onItemSelectedListener!=null) this.onItemSelectedListener.onNothingSelected(parent);
spinnerTouched=false;
}
}
Had a similar issue with some UI component in iOS. I decided to use a similar hack.
So somewhere in the object that owns the spinner--maybe an activity--declare a member variable: private var isSelectionFromTouch: Boolean = false
The rest of the relevant code:
init {
spinner.onItemSelectedListener = this
spinner.setOnTouchListener { _, _ ->
this.isSelectionFromTouch = true
false
}
}
// On Item Selected
override
fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (!isSelectionFromTouch) { return }
Log.d(TAG, "item selected. do something.")
isSelectionFromTouch = false
}
override
fun onNothingSelected(parent: AdapterView<*>?) {
isSelectionFromTouch = false
}
Related
Basically I'd like to attach a single OnClickListener to multiple views inside a ConstraintLayout.
Before migrating to the ConstraintLayout the views where inside one layout onto which I could add a listener. Now they are on the same layer with other views right under the ConstraintLayout.
I tried adding the views to a android.support.constraint.Group and added a OnClickListener to it programmatically.
group.setOnClickListener {
Log.d("OnClick", "groupClickListener triggered")
}
However this does not seem to work as of the ConstraintLayout version 1.1.0-beta2
Have I done something wrong, is there a way to achieve this behaviour or do I need to attach the listener to each of the single views?
The Group in ConstraintLayout is just a loose association of views AFAIK. It is not a ViewGroup, so you will not be able to use a single click listener like you did when the views were in a ViewGroup.
As an alternative, you can get a list of ids that are members of your Group in your code and explicitly set the click listener. (I have not found official documentation on this feature, but I believe that it is just lagging the code release.) See documentation on getReferencedIds here.
Java:
Group group = findViewById(R.id.group);
int refIds[] = group.getReferencedIds();
for (int id : refIds) {
findViewById(id).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// your code here.
}
});
}
In Kotlin you can build an extension function for that.
Kotlin:
fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
Then call the function on the group:
group.setAllOnClickListener(View.OnClickListener {
// code to perform on click event
})
Update
The referenced ids are not immediately available in 2.0.0-beta2 although they are in 2.0.0-beta1 and before. "Post" the code above to grab the reference ids after layout. Something like this will work.
class MainActivity : AppCompatActivity() {
fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Referenced ids are not available here but become available post-layout.
layout.post {
group.setAllOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
val text = (v as Button).text
Toast.makeText(this#MainActivity, text, Toast.LENGTH_SHORT).show()
}
})
}
}
}
This should work for releases prior to 2.0.0-beta2, so you can just do this and not have to do any version checks.
The better way to listen to click events from multiple views is to add a transparent view as a container on top of all required views. This view has to be at the end (i.e on top) of all the views you need to perform a click on.
Sample container view :
<View
android:id="#+id/view_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="#+id/view_bottom"
app:layout_constraintEnd_toEndOf="#+id/end_view_guideline"
app:layout_constraintStart_toStartOf="#+id/start_view_guideline"
app:layout_constraintTop_toTopOf="parent"/>
Above sample contains all four constraint boundaries within that, we can add views that to listen together and as it is a view, we can do whatever we want, such as ripple effect.
To complement the accepted answer for Kotlin users create an extension function and accept a lambda to feel more like the API group.addOnClickListener { }.
Create the extension function:
fun Group.addOnClickListener(listener: (view: View) -> Unit) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
usage:
group.addOnClickListener { v ->
Log.d("GroupExt", v)
}
The extension method is great but you can make it even better by changing it to
fun Group.setAllOnClickListener(listener: (View) -> Unit) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
So the calling would be like this
group.setAllOnClickListener {
// code to perform on click event
}
Now the need for explicitly defining View.OnClickListener is now gone.
You can also define your own interface for GroupOnClickLitener like this
interface GroupOnClickListener {
fun onClick(group: Group)
}
and then define an extension method like this
fun Group.setAllOnClickListener(listener: GroupOnClickListener) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener { listener.onClick(this)}
}
}
and use it like this
groupOne.setAllOnClickListener(this)
groupTwo.setAllOnClickListener(this)
groupThree.setAllOnClickListener(this)
override fun onClick(group: Group) {
when(group.id){
R.id.group1 -> //code for group1
R.id.group2 -> //code for group2
R.id.group3 -> //code for group3
else -> throw IllegalArgumentException("wrong group id")
}
}
The second approach has a better performance if the number of views is large since you only use one object as a listener for all the views!
While I like the general approach in Vitthalk's answer I think it has one major drawback and two minor ones.
It does not account for dynamic position changes of the single views
It may register clicks for views that are not part of the group
It is not a generic solution to this rather common problem
While I'm not sure about a solution to the second point, there clearly are quite easy ones to the first and third.
1. Accounting position changes of element in the group
This is actually rather simple. One can use the toolset of the constraint layout to adjust the edges of the transparent view.
We simply use Barriers to receive the leftmost, rightmost etc. positions of any View in the group.
Then we can adjust the transparent view to the barriers instead of concrete views.
3. Generic solution
Using Kotlin we can extend the Group-Class to include a method that adds a ClickListener onto a View as described above.
This method simply adds the Barriers to the layout paying attention to every child of the group, the transparent view that is aligned to the barriers and registers the ClickListener to the latter one.
This way we simply need to call the method on the Group and do not need to add the views to the layout manually everytime we need this behaviour.
in Constraintlayout 2.0.0,you can use Layer to resolve multiple views click event,and also support scale animation
For the Java people out there like me:
public class MyConstraintLayoutGroup extends Group {
public MyConstraintLayoutGroup(Context context) {
super(context);
}
public MyConstraintLayoutGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyConstraintLayoutGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnClickListener(OnClickListener listener) {
for (int id : getReferencedIds()) {
getRootView().findViewById(id).setOnClickListener(listener);
}
}
}
This is not propagating click states to all other children however.
fun ConstraintLayout.setAllOnClickListener(listener: (View) -> Unit) {
children.forEach { view ->
rootView.findViewById<View>(view.id).setOnClickListener { listener.invoke(this) }
}
}
And then
.setAllOnClickListener {
do something
}
I'm looking in the documentation for some kind of an event that would allow me to detect when either a view is created or when the view is attached to the activity, anywhere in the view hierarchy, whether it's one level deep or multiple levels deep.
A method like this would be ideal, either at the Activity level, Window level, or Window.DecorView level:
void ViewAttachedToActivity(View view)
{
... //triggered each time an individual view is added to activity
}
The important part is that I want to be able to detect this event from the context of the Activity, not from the context of the child view itself.
Below is a rough demo of what I'm trying to accomplish. I'm wondering if a more efficient method exists:
P.S. I know I can accomplish the custom font part by subclassing all the text controls like TextView, EditText, Button, etc, and use them instead of the stock controls, but I'm looking for a simple workaround that might help me to avoid that.
(Please excuse the fact that this code is written in C# using Mono for Android, it should be simple to understand and mentally convert to Java)
public class BaseActivity : SherlockFragmentActivity
{
public Typeface Voltaire { get; set; }
bool pendingLayout = false;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Voltaire = Typeface.CreateFromAsset(Assets, "fonts/voltaire-regular.ttf");
Window.DecorView.ViewTreeObserver.GlobalLayout += new EventHandler(ViewTreeObserver_GlobalLayout);
Window.DecorView.ViewTreeObserver.PreDraw += new EventHandler<ViewTreeObserver.PreDrawEventArgs>(ViewTreeObserver_PreDraw);
}
void ViewTreeObserver_GlobalLayout(object sender, EventArgs e)
{
pendingLayout = true;
}
void ViewTreeObserver_PreDraw(object sender, ViewTreeObserver.PreDrawEventArgs e)
{
if (pendingLayout)
{
pendingLayout = false;
SetTypeFace(Window.DecorView, Voltaire);
}
}
public void SetTypeFace(View view, Typeface typeface)
{
if (view is TextView)
{
((TextView)view).Typeface = typeface;
}
if (view is ViewGroup)
{
ViewGroup viewgroup = (ViewGroup)view;
for (int i = 0; i < viewgroup.ChildCount; i++)
{
SetTypeFace(viewgroup.GetChildAt(i), typeface);
}
}
}
}
I dont think an event like this exists
I am interested in populating a screen/activity with a user defined number of same views. Each view will have the exact same layout: couple TextViews and few Buttons. The thing is that each button will control what each TextView will display.
The way I was thinking to implement it was to have one XML and one Java class. Then dependimg on the number the user inputs, populate the screen with that many same views (using a for loop). The question is, can it be done? how? am I thinking about it in the right way?
Please help with any input or thoughts, code examples will be great too.
of course it can be done.
I think the easiest for your situation, plus you can then easily extend, is to create some helper functions that take care of:
1) creating a empty screen
2) create a button for a screen
3) create a textview for a screen
and finally
4) create a screen and populate it
You have to decide the proper Root element for your Views, depending on the child arragement you need. For simplicity let's choose a LinearLayout, but for a RelativeLayout or TableLayout the example is the same, it only changes that when you add the elements, you have to use additional parameters to properly place them.
Note that the function to create an empty custom view returns a ViewGroup ("where all layouts derive from"). This way, you always work with ViewGroups and just define the screen layout type once, inside createCustomView. So you can change the type of screens just there, and the rest of code will work ...
Here is some code for your inspiration:
private ViewGroup createCustomView(Context context) {
LinearLayout myCoolNewView=new LinearLayout(context); // or RelativeLayout, etc..
return myCoolNewView;
}
private Button createButton(Context context, String buttonText) {
Button newButton=new Button(context);
newButton.setText(buttonText);
return newButton;
}
private TextView createText(Context context, String initialText) {
TextView newText=new TextView(context);
newText.setText(buttonText);
return newText;
}
private ViewGroup createScreen(Context context, int numberOfButtons, int numberOfTextfields) {
ViewGroup newScreen=createCustomView(context);
TextView[] textViews=new TextView[numberOfTextFields];
for (int i=0; i<numberOfTextfields; i++) {
textViews[i]=createText(context, "hi i am text "+i);
newScreen.addView(textViews[i]); // you ideally provide here layoutparams to properly place your buttons
}
for (int j=0; i<numberOfButtons; j++) {
Button button=createButton(context, "hi i am button "+j);
button.setOnClickListener(new OnClickListener() {
public void onClick (View clickedView) {
// here you have a button keypress and you know all the textviews
textView[i%j].setText("hey you pressed me");
}
});
newScreen.addView(button);
}
return newScreen;
}
So now you can:
ViewGroup screen1=createScreen(context, 10, 10);
ViewGroup screen2=createScreen(context, 5, 3);
ViewGroup screen3=createScreen(context, 2, 5);
and add the screens to a parent layout, to a ViewFlipper, to a ViewSwitcher, etc... like this:
ViewGroup parentLayoutOfAllScreens=findViewById(R.id.root_of_screens);
parentLayoutOfAllScreens.addView(screen1);
parentLayoutOfAllScreens.addView(screen2);
parentLayoutOfAllScreens.addView(screen3);
In the XML you just have to create the root layout, and name it root_of_screens...
good coding !!! I suppose there'll be some errors in the code above, just typed it here, but I hope you get the idea and tweak it to suit your needs!
EDIT : v2.0 : Extending a View
Create a new .java named "MyCoolScreen.java" or whatever name, in the same folder where your activity is (for simplicity):
package ........
public class MyCoolScreen extends LinearLayout {
/** Now every view holds its own buttons, and they are private, it's good for encapsulating */
private TextView[] mTextViews; // <-- as a convention, members should start with "m"
private Button[] mButtons;
private UserPressedButtons mUserPressedButtonsListener; // See below
/** The following constructors must always be present for a custom view, and must always call super */
public MyCoolScreen(Context context) {
// This is the constructor you will use when creating your view programmatically
super(context);
}
public MyCoolScreen(Context context, AttributeSet attrs) {
// This is the constructor Android calls when you include your custom view in an XML
// You can do this too!!
// The ATTRS will then include your numberofbuttons and numberoftextfields from the XML
// this is beyond the example, but read about it, it's interesting
super(context, attrs); // this MUST ALWAYS be here for custom views, or they will not work.
// it tells the parent view to continue the construction.
}
public MyCoolScreen(Context context, AttributeSet attrs, int defStyle) {
// Another constructor Android calls from the XML
super(context, attrs, defStyle);
}
/** We create an "init" method to initialize this view from outside */
public void init(int numberOfTextViews, int numberOfButtons) {
createScreen(numberOfTextViews, numberOfButtons);
}
/** This is the same */
private Button createButton(Context context, String buttonText) {
Button newButton=new Button(context);
newButton.setText(buttonText);
return newButton;
}
/** This is the same */
private TextView createText(Context context, String initialText) {
TextView newText=new TextView(context);
newText.setText(buttonText);
return newText;
}
/** We tweak this function so it doesnt return a view, but rather fills up this one :) */
private void createScreen(int numberOfButtons, int numberOfTextfields) {
ViewGroup newScreen=this; // It's this view the one we gonna fill up!
mTextViews=new TextView[numberOfTextfields];
mButtons=new Button[numberOfButtons];
Context context=getContext(); // Views always know their context after constructed
for (int i=0; i<numberOfTextfields; i++) {
mTextViews[i]=createText(context, "hi i am text "+i);
newScreen.addView(textViews[i]); // you ideally provide here layoutparams to properly place your buttons
}
for (int j=0; i<numberOfButtons; j++) {
Button button=createButton(context, "hi i am button "+j);
button.setId(j);
button.setOnClickListener(new OnClickListener() {
public void onClick (View clickedView) {
// here you have a button keypress and you know all the textviews
if (mUserPressedButtonsListener!=null) mUserPressedButtonsListener.OnButtonPressed(j);
textView[i%j].setText("hey you pressed me");
}
});
mButtons[j]=button;
newScreen.addView(button);
}
}
public interface UserPressedButtons {
public void OnButtonPressed(int buttonNumber);
}
public void setUserPressedButtonsListener (UserPressedButtons listener) {
mUserPressedButtonsListener=listener;
}
}
Ok, so now to use this, in your Activity you can do:
import ....... .MyCoolScreen;
import ....... .MyCoolScreen.UserPressedButtons;
.
.
.
MyCoolScreen screen1=new MyCoolScreen(context);
screen1.init(5,5); // initializes the screen.
myRootLayout.addView(screen1);
What's cool about this, is now functionality is totally encapsulated in your custom view. And it resides in another .java, so your activity code is very clean, and you can even expand the View functionality without making it ugly.
It's also a common practice to create interfaces and listeners for your views to communicate with the outside world, so for example, we can do:
screen1.setUserPressedButtonsListener(new MyCoolScreen.UserPressedButtons() {
#Override
public void OnButtonPressed (int number) {
// you know the user pressed button "number", and you can do stuff about it without
// having to include it inside the MyCoolScreen class. Of course in your example you
// don't need this at the moment, because the View will modify its textfield, but suppose
// one of the buttons is "rocket launch" , that is something you will handle at the activity level, ie.
if (number==ROCKET_LAUNCH) RocketLauncher.setTarget(10,10).launch(); // Your MyCoolScreen doesnt know how to launch rockets, but your activity maybe yes...
}
});
You can do all kinds of cool things with your new custom view. For example, you could define:
#Override
public void OnDraw(Canvas c) {
c.drawEllipse ...
c.drawRectangle ....
}
And you can paint circles, lines, etc... over your textfields & buttons :) For this to work, you have to put
setWillNotDraw(false) on the constructor.
There might be errors, just typed the code here, but I hope it helps you!
Add and Remove Views in Android Dynamically?
this will helps to you most...
How do I show some default suggestions for AutoCompleteTextView before the user type anything? I cannot find a way to do this even with creating a custom class that extends AutoCompleteTextView.
I want to show suggestions for common input values to save the user from typing.
Any suggestions?
You should subclass AutoCompleteTextView and override enoughToFilter() to return true all the time. After that you can call performFiltering("",0) (it's a protected function, so you can export this call via a public function in your class).
Something like that:
public class ContactsAutoCompleteTextView extends AutoCompleteTextView {
public ContactsAutoCompleteTextView(Context context) {
super(context);
}
public ContactsAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ContactsAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean enoughToFilter() {
return true;
}
public void temp() {
performFiltering("",0);
}
}
Itay Kahana's answer is indeed correct. The only thing I would add is that instead of creating a temp() function, to override the onFocusChanged function. Personally I used the following:
#Override
protected void onFocusChanged (boolean focused, int direction, Rect previouslyFocusedRect) {
if(focused)
performFiltering("", 0);
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
If you dont need it to be dynamic I would go by having a string array in the resources, and then just load the array when the AutoCompleteTextView is about to be viewed. Like:
public class CountriesActivity extends Activity {
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.countries);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_dropdown_item_1line, COUNTRIES);
AutoCompleteTextView textView = (AutoCompleteTextView)
findViewById(R.id.countries_list);
textView.setAdapter(adapter);
}
private static final String[] COUNTRIES = new String[] {
"Belgium", "France", "Italy", "Germany", "Spain"
};
}
Which can be found on http://developer.android.com/reference/android/widget/AutoCompleteTextView.html
Another way which I have done a couple of times which allows it to learn from the user is o use a database connection with IE a simple cursor. When you create the db you could insert some default values.
HereĀ“s an example with using simple cursor adapter: http://androidcommunity.com/forums/f4/how-to-use-autocompletetextview-with-simplecursoradapter-15875/
Edit 1:
One idea to show the list before the user starts type is to have a simple listview below the EditText. Not sure if you could call the autocompletetextview to show the suggestions, should be possible somehow. Perhaps you need to create your own autocompletetextiew class.
The Android autocomplete only starts after two letters. How can I make it so the list appears when the field is just selected?
To get the autocomplete to show on focus add focus listener and show the drop down when the field gets focus, like this:
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View view, boolean hasFocus) {
if(hasFocus){
editText.showDropDown();
}
}
});
Or just call editText.showDropDown() if you don't need the focus part.
Have a look to setThreshold method:
public void setThreshold (int
threshold)
Since: API Level 1
Specifies the minimum number of
characters the user has to type in the
edit box before the drop down list is
shown.
When threshold is less than or equals 0, a threshold of 1 is applied.
Extend the AutoCompleteTextView, overriding the enoughToFilter() methods and the threshold methods so that it doesn't replace the 0 threshold with a 1 threshold:
public class MyAutoCompleteTextView extends AutoCompleteTextView {
private int myThreshold;
public MyAutoCompleteTextView(Context context) {
super(context);
}
public MyAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public void setThreshold(int threshold) {
if (threshold < 0) {
threshold = 0;
}
myThreshold = threshold;
}
#Override
public boolean enoughToFilter() {
return getText().length() >= myThreshold;
}
#Override
public int getThreshold() {
return myThreshold;
}
}
For people who want to change threshold using SearchView you have to use:
SearchView.SearchAutoComplete complete = (SearchView.SearchAutoComplete)search.findViewById(R.id.search_src_text);
complete.setThreshold(0);
Pad your adapter with one/two white character on left depending on the threshold setting.
Alternate method of changing the setting in your XML:
As mentioned by others you need to set your 'Auto Completion Threshold' to 1
Other than what #systempuntoout mentioned.
You can also do that in your XML file as shown
<AutoCompleteTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/edittext_id"
android:inputType="textAutoComplete"
android:completionThreshold="1"
/>
Note the line : android:completionThreshold="1"