15

I'm having some issues implementing a TimePicker in my application that allows the user to change the time of a database record prior to inserting it.

The problem is that when the AM/PM button is pressed, the onTimeChanged(View, int, int) method isn't invoked. Whenever I change either the hour or minute value of the TimePicker, onTimeChanged() is called, however.

Scenarios:

  • User just clicks the AM/PM button: AM/PM is NOT updated
  • User clicks the Hours/Minutes: Time is updated
  • User clicks the AM/PM button then changes the hours/minutes: Time and AM/PM is updated

Am I wrong in thinking that the AM/PM button should be able to be clicked to update the time without having to also change a time value after the am/pm button?

I've put together a small test project to replicate this and here's the code:

Activity

public class TestActivity extends Activity implements OnTimeChangedListener {

    private Calendar mCalendar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.timepicker);

        mCalendar = Calendar.getInstance();

        TimePicker tp = (TimePicker)findViewById(R.id.timepicker);
        tp.setIs24HourView(false);
        tp.setOnTimeChangedListener(this);
    }

    @Override
    public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
        Log.d("TAG", "In onTimeChanged");
        mCalendar.set(mCalendar.get(Calendar.YEAR),
                      mCalendar.get(Calendar.MONTH),
                      mCalendar.get(Calendar.DAY_OF_MONTH),
                      hourOfDay,
                      minute);

        setCalendarTime();
    }

    private void setCalendarTime() {
        Date date = mCalendar.getTime();

        if (date != null) {
            SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yy '@' h:mm a");
            String dateTime = formatter.format(date);

            Toast.makeText(this, dateTime, Toast.LENGTH_LONG).show();
        }
    }
}

timepicker.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:fillViewport="true">
    <TimePicker android:id="@+id/timepicker"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="5dip"
                android:layout_marginRight="5dip"/>
</LinearLayout>
hooked82
  • 6,336
  • 4
  • 41
  • 46

11 Answers11

33

I have tested the code. Yes, you are right, TimePicker AM/PM button not invoking onTimeChanged method which it should.

Actually, Its a bug. It has been reported to google. You can find it here
http://code.google.com/p/android/issues/detail?id=18982

Please vote, comment & Star the bug report to raise its priority and to get it fixed by development team as soon as possible.

Vivek
  • 11,938
  • 19
  • 92
  • 127
6

I found this answer, but it didn't work for me, so I thought I would update.

In 3.2 (and above?), the time picker does not have a button for am/pm. Instead it has a NumberPicker. The following code worked for me. The last bit is there because I needed to limit the time selected to no earlier than the 'min' date and no greater than the 'max' date, so I had to check the date selected in the matching DatePicker control:

        NumberPicker amPmView  = (NumberPicker)((ViewGroup)tp.getChildAt(0)).getChildAt(3);
        amPmView.setOnValueChangedListener(new OnValueChangeListener() { 
            @Override
            public void onValueChange(NumberPicker arg0, int arg1, int arg2) {
                if(arg0.getValue()== 1){ 
                    if (tp.getCurrentHour() < 12)
                        tp.setCurrentHour(tp.getCurrentHour() + 12); 
                } 
                else{ 
                    if (tp.getCurrentHour() >= 12) 
                        tp.setCurrentHour(tp.getCurrentHour() - 12); 
                } 

                int year = dp.getYear();
                int month = dp.getMonth();
                int dayOfMonth = dp.getDayOfMonth();
                int hourOfDay = tp.getCurrentHour();
                int minute = tp.getCurrentMinute();

                if (year == min.get(Calendar.YEAR) && month == min.get(Calendar.MONTH) && dayOfMonth == min.get(Calendar.DAY_OF_MONTH)){
                    if ((hourOfDay < min.get(Calendar.HOUR_OF_DAY))||
                        (hourOfDay == min.get(Calendar.HOUR_OF_DAY) && (minute < min.get(Calendar.MINUTE)))){
                        tp.setCurrentHour(min.get(Calendar.HOUR_OF_DAY));
                        tp.setCurrentMinute(min.get(Calendar.MINUTE));
                    }
                }else if (year == max.get(Calendar.YEAR) && month == max.get(Calendar.MONTH) && dayOfMonth == max.get(Calendar.DAY_OF_MONTH)){
                    if ((hourOfDay > max.get(Calendar.HOUR_OF_DAY))||
                        (hourOfDay == max.get(Calendar.HOUR_OF_DAY) && (minute > max.get(Calendar.MINUTE)))){
                        tp.setCurrentHour(max.get(Calendar.HOUR_OF_DAY));
                        tp.setCurrentMinute(max.get(Calendar.MINUTE));
                    }
                }
            } 
        }); 
rtbsoft
  • 61
  • 1
  • 2
5

Had the same issue when creating the TimePicker in code and none of the provided solutions worked for me. Below was what I ended up doing. Tested on API levels 18, 21 and 23

final TimePicker tp = new TimePicker(getContext());
tp.setOnTimeChangedListener(this);
try {
    ViewGroup amPmView;
    ViewGroup v1 = (ViewGroup)tp.getChildAt(0);
    ViewGroup v2 = (ViewGroup)v1.getChildAt(0);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        ViewGroup v3 = (ViewGroup)v2.getChildAt(0);
        amPmView = (ViewGroup)v3.getChildAt(3);
    } else {
        amPmView = (ViewGroup)v2.getChildAt(3);
    }
    View.OnClickListener listener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            tp.setCurrentHour((tp.getCurrentHour() + 12) % 24);
        }
    };
    View am = amPmView.getChildAt(0);
    View pm = amPmView.getChildAt(1);
    am.setOnClickListener(listener);
    pm.setOnClickListener(listener);
} catch (Exception e) {
    // DO nothing... just ignore the workaround if this fails.
}
velval
  • 3,072
  • 36
  • 45
  • this snippet managed to put listener on AM/PM but now the AM/PM is not working as you replaced the default listener by your listener. please advise – Hisham Bakr Apr 09 '16 at 18:38
  • I replaced onClickListener with OnTouch to not override original click listener – Hisham Bakr Apr 09 '16 at 20:23
  • 1
    Hi @Hisham, The whole idea for this workaround is to fix the default listener as it has a bug as pointed out by Vivek. The default listener does not call the onTimeChanged callback so you will need to replace it with your custom listener and call tp.setCurrentHour((tp.getCurrentHour() + 12) % 24); to ensure the callback gets triggered with the right time. – velval Apr 10 '16 at 07:12
  • 2
    This answer should have been accepted and should have received that bounty of 100 reputation points. I tested this and it works happily on all Android versions. Note that in Android N+, this bug has already been fixed. – Wrichik Basu Aug 24 '20 at 08:16
  • What a brilliant solution! This Android Bug wasted 2 hours of my time debugging :/ This should have been the accepted answer! This solution totally works! – ONE Oct 26 '20 at 04:31
5

Velval solution is the only solution that works and compatiple with different Android vesrions. I tried it on api 17 and 23. but it has an issue that it prevent original on click listener on Android 6. so here is what worked with me. to use touch instead of click:

public class MyTimePicker extends TimePicker {

    private OnTimeChangedListener onTimeChangedListener;

    public MyTimePicker(Context context) {
        super(context);
        //  init();
    }

    public MyTimePicker(Context context, AttributeSet attrs) {
        super(context, attrs);
        //init();
    }

    public MyTimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // init();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public MyTimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // Stop ScrollView from getting involved once you interact with the View
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            ViewParent p = getParent();
            if (p != null)
                p.requestDisallowInterceptTouchEvent(true);
        }
        return false;
    }

    @Override
    public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
        super.setOnTimeChangedListener(onTimeChangedListener);
        this.onTimeChangedListener = onTimeChangedListener;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        init();
    }

    private void init() {

        try {
            ViewGroup amPmView;
            ViewGroup v1 = (ViewGroup) getChildAt(0);
            ViewGroup v2 = (ViewGroup) v1.getChildAt(0);
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                ViewGroup v3 = (ViewGroup) v2.getChildAt(0);
                amPmView = (ViewGroup) v3.getChildAt(3);
            } else {
                amPmView = (ViewGroup) v2.getChildAt(3);
            }
            View.OnTouchListener listener = new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                        onTimeChangedListener.onTimeChanged(MyTimePicker.this, getCurrentHour(), getCurrentMinute());
                    } else {
                        int hour = getCurrentHour();
                        if (hour >= 12) {
                            hour -= 12;
                        } else {
                            hour += 12;
                        }
                        onTimeChangedListener.onTimeChanged(MyTimePicker.this, hour, getCurrentMinute());
                    }

                    return false;
                }

            };
            View am = amPmView.getChildAt(0);
            View pm = amPmView.getChildAt(1);

            am.setOnTouchListener(listener);
            pm.setOnTouchListener(listener);
        } catch (Exception e) {
            // DO nothing... just ignore the workaround if this fails.
        }


    }
}
Hisham Bakr
  • 559
  • 4
  • 13
3

this link shows that onTimeChanged(); is getting called which triggers the event dispatcher.

If you're not getting the events you need (even though it appears to be sending) you have have to

  • extend the default TimerPicker,

  • override mAmPmButton.setOnClickListener,

  • and include your version in the view.


mAmPmButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            requestFocus();
            if (mIsAm) {

                // Currently AM switching to PM
                if (mCurrentHour < 12) {
                    mCurrentHour += 12;
                }                
            } else {

                // Currently PM switching to AM
                if (mCurrentHour >= 12) {
                    mCurrentHour -= 12;
                }
            }
            mIsAm = !mIsAm;
            mAmPmButton.setText(mIsAm ? mAmText : mPmText);
            onTimeChanged();
        }
    });
swiftBoy
  • 35,607
  • 26
  • 136
  • 135
user123321
  • 12,593
  • 11
  • 52
  • 63
  • Yeah, I've seen that in the source. I've even pulled the source down and recompiled the TimePicker/NumberPicker into local classes and it works as expected. The problem with this is that I don't want to have to pull the source for these and have my own custom ones. I'd rather figure out why the default one isn't working as expected in my application? – hooked82 Jan 30 '12 at 19:32
  • 1
    I don't think you're going to get it to work without doing one of those options. There is apparently a problem with the TimerPicker included in the sdk. – user123321 Jan 30 '12 at 19:38
3

Please use the below code it is working fine for me.

final TimePicker tp=(TimePicker)findViewById(R.id.timePicker1);
    View amPmView  = ((ViewGroup)tp.getChildAt(0)).getChildAt(2);
    if(amPmView instanceof Button)
    {
        amPmView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Log.d("OnClickListener", "OnClickListener called");
                if(v instanceof Button)
                {
                    if(((Button) v).getText().equals("AM"))
                    {
                        ((Button) v).setText("PM");
                         if (tp.getCurrentHour() < 12) {
                             tp.setCurrentHour(tp.getCurrentHour() + 12);
                            }  

                    }
                    else{
                        ((Button) v).setText("AM");
                         if (tp.getCurrentHour() >= 12) {
                             tp.setCurrentHour(tp.getCurrentHour() - 12);
                            }
                    }
                }

            }
        });
    }
ram
  • 3,487
  • 10
  • 33
  • 47
  • Thanks, this works great. I commented out the v.setText("AM/PM") lines and it still gets updated appropriately. I'm not sure if this is b/c tp.setCurrentHour() ends up firing the timeChanged event. – samus Feb 04 '13 at 15:44
  • I have problem with this approach, the instanceof check is failing. – Neon Warge May 15 '15 at 04:51
1

With API 25 Nougat, code of Velval / Hisham is the only one that works but not in landscape mode. I changed the code and it works fine :) :

public class AMPMTimePicker extends TimePicker {

private static final String TAG = "AMPMTimePicker";
private OnTimeChangedListener onTimeChangedListener;

public AMPMTimePicker(Context context) {
    super(context);
}

public AMPMTimePicker(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public AMPMTimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public AMPMTimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // Stop ScrollView from getting involved once you interact with the View
    if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
        ViewParent p = getParent();
        if (p != null)
            p.requestDisallowInterceptTouchEvent(true);
    }
    return false;
}

@Override
public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
    super.setOnTimeChangedListener(onTimeChangedListener);
    this.onTimeChangedListener = onTimeChangedListener;
}

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    init();
}

@SuppressWarnings("deprecation")
private void init() {
    try {
        ViewGroup amPmView;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

            // LinearLayout (LOLLIPOP)
            // GridLayout (M-LANDSCAPE)
            // LinearLayout (M-PORTRAIT)
            ViewGroup v1 = (ViewGroup) getChildAt(0);

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {

                // FrameLayout (LOLLIPOP-LANDSCAPE)
                // FrameLayout - id:time_header (LOLLIPOP-PORTRAIT)
                ViewGroup v2 = (ViewGroup) v1.getChildAt(0);

                // FrameLayout - id:TimeHeader (LOLLIPOP-LANDSCAPE)
                // LinearLayout (LOLLIPOP-PORTRAIT)
                ViewGroup v3 = (ViewGroup) v2.getChildAt(0);

                if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
                    ViewGroup v4 = (ViewGroup) v3.getChildAt(0); // LinearLayout (LOLLIPOP)
                    amPmView = (ViewGroup) v4.getChildAt(3); // LinearLayout - id:ampm_layout (LOLLIPOP)
                } else { // PORTRAIT
                    amPmView = (ViewGroup) v3.getChildAt(3); // LinearLayout - id:ampm_layout (LOLLIPOP)
                }
            } else { // M and after
                if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
                    ViewGroup v2 = (ViewGroup) v1.getChildAt(1); // RelativeLayout (M)
                    amPmView = (ViewGroup) v2.getChildAt(1); // LinearLayout - id:ampm_layout (M)
                } else {
                    ViewGroup v2 = (ViewGroup) v1.getChildAt(0); // RelativeLayout - id:time_header (M)
                    amPmView = (ViewGroup) v2.getChildAt(3); // LinearLayout - id:ampm_layout (M)
                }
            }

            View am = amPmView.getChildAt(0); // AppCompatCheckedTextView - id:am_label
            View pm = amPmView.getChildAt(1); // AppCompatCheckedTextView - id:pm_label

            View.OnTouchListener listener = new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int hour;
                    int minute;
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                        hour = getCurrentHour();
                        minute = getCurrentMinute();
                    } else {
                        hour = getHour();
                        minute = getMinute();
                    }
                    hour = (hour >= 12) ? hour - 12 : hour + 12;
                    onTimeChangedListener.onTimeChanged(AMPMTimePicker.this, hour, minute);
                    return false;
                }
            };
            am.setOnTouchListener(listener);
            pm.setOnTouchListener(listener);
        }
    } catch (Exception e) {
        Log.e(TAG, "TimePicker is not defined for this Android version : " + e.getMessage());
    }
}
}
J-Jamet
  • 847
  • 8
  • 17
0

Here's a quick project that I threw together that supports Android 4.0+. Basically, it shows a TextView that is always in sync with the TimePicker, regardless of which vertical spinner is being manipulated by the user.

https://gist.github.com/danialgoodwin/5694256

Anonsage
  • 8,030
  • 5
  • 48
  • 51
0

i make this method thats return true if the TimePicker select AM and false if the TimePicker select PM. note: the suppresLint is for Api 8 don't work .getvalue() statement

Saludos!! :)

@SuppressLint("NewApi")
private boolean isAM(TimePicker timePicker){
    NumberPicker numberPickerAmPm  = (NumberPicker)((ViewGroup) timePicker.getChildAt(0)).getChildAt(3);
    if(numberPickerAmPm.getValue()==0){
        //sendToast("es am");
        return true;
    }else{
       //sendToast("es pm");
        return false;
    }

}
user3186511
  • 63
  • 2
  • 8
0

I would like to share my solution as well since the other workarounds posted here doesn't work for me. But I am able to figure it out. This line:

    View amPmView  = ((ViewGroup)tp.getChildAt(0)).getChildAt(2);

Doesn't seem to return the picker wheel for AM_PM.

    NumberPicker amPmView  = (NumberPicker ((ViewGroup)tp.getChildAt(0)).getChildAt(3);

Neither this as well, since this one returns a TextView. It seems it seems it is the label designated on the picker.

But using the latter, getting the 4th child element returns me the picker wheel for AM_PM. Here is what I got so far:

    NumberPicker amPmView = (NumberPicker)((ViewGroup) mTimePicker.getChildAt(0)).getChildAt(4);
    amPmView.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() 
    {
        @Override
        public void onValueChange(NumberPicker picker, int oldVal, int newVal) 
        {
            Log.i(NoteApplication.TAG, "AM_PM selected...");
        }
    });

Now I am able to detect changes in AM_PM. I hope this help some other people who can't retrieve it via getChildAt(2) or getChildAt(3) as described by other answers.

Also take note that this is the case for the class TimePicker, I haven't tried this yet on a TimePickerDialog so I am not sure for that one. I am testing at min sdk 8 targeting api 22.

HTH

Neon Warge
  • 1,817
  • 6
  • 29
  • 53
0

I've also faced the same problem using API 21, and the problem hasn't been solved yet.

I replaced the TimePicker view for a TimePickerDialog and it worked.

The following code sets a TextView with the time picked from the TimePickerDialog. You can replace the tv.setText() with any logic :

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final TextView tv = (TextView)findViewById(R.id.textView);

    Calendar calendar = Calendar.getInstance();
    int hour = calendar.get(Calendar.HOUR_OF_DAY);
    int minute = calendar.get(Calendar.MINUTE);
    TimePickerDialog timePickerDialog = new TimePickerDialog(this,
            new TimePickerDialog.OnTimeSetListener() {
                @Override
                public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
                    tv.setText(String.format(Locale.getDefault(),"Hour: %d, Minute: %d ",hourOfDay,minute));
                }
    }, hour, minute, true);
    timePickerDialog.show();
}
tam.teixeira
  • 805
  • 7
  • 10