diff --git a/app/src/main/assets/timezones.json b/app/src/main/assets/timezones.json new file mode 100644 index 0000000000000000000000000000000000000000..628f21c12705393a3b63bad2710ec39f2df6e873 --- /dev/null +++ b/app/src/main/assets/timezones.json @@ -0,0 +1,1373 @@ +[ + { + "value": "Dateline Standard Time", + "abbr": "DST", + "offset": -12, + "isdst": false, + "text": "(UTC-12:00) International Date Line West", + "utc": [ + "Etc/GMT+12" + ] + }, + { + "value": "UTC-11", + "abbr": "U", + "offset": -11, + "isdst": false, + "text": "(UTC-11:00) Coordinated Universal Time-11", + "utc": [ + "Etc/GMT+11", + "Pacific/Midway", + "Pacific/Niue", + "Pacific/Pago_Pago" + ] + }, + { + "value": "Hawaiian Standard Time", + "abbr": "HST", + "offset": -10, + "isdst": false, + "text": "(UTC-10:00) Hawaii", + "utc": [ + "Etc/GMT+10", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Rarotonga", + "Pacific/Tahiti" + ] + }, + { + "value": "Alaskan Standard Time", + "abbr": "AKDT", + "offset": -8, + "isdst": true, + "text": "(UTC-09:00) Alaska", + "utc": [ + "America/Anchorage", + "America/Juneau", + "America/Nome", + "America/Sitka", + "America/Yakutat" + ] + }, + { + "value": "Pacific Standard Time (Mexico)", + "abbr": "PDT", + "offset": -7, + "isdst": true, + "text": "(UTC-08:00) Baja California", + "utc": [ + "America/Santa_Isabel" + ] + }, + { + "value": "Pacific Standard Time", + "abbr": "PDT", + "offset": -7, + "isdst": true, + "text": "(UTC-08:00) Pacific Time (US & Canada)", + "utc": [ + "America/Dawson", + "America/Los_Angeles", + "America/Tijuana", + "America/Vancouver", + "America/Whitehorse", + "PST8PDT" + ] + }, + { + "value": "US Mountain Standard Time", + "abbr": "UMST", + "offset": -7, + "isdst": false, + "text": "(UTC-07:00) Arizona", + "utc": [ + "America/Creston", + "America/Dawson_Creek", + "America/Hermosillo", + "America/Phoenix", + "Etc/GMT+7" + ] + }, + { + "value": "Mountain Standard Time (Mexico)", + "abbr": "MDT", + "offset": -6, + "isdst": true, + "text": "(UTC-07:00) Chihuahua, La Paz, Mazatlan", + "utc": [ + "America/Chihuahua", + "America/Mazatlan" + ] + }, + { + "value": "Mountain Standard Time", + "abbr": "MDT", + "offset": -6, + "isdst": true, + "text": "(UTC-07:00) Mountain Time (US & Canada)", + "utc": [ + "America/Boise", + "America/Cambridge_Bay", + "America/Denver", + "America/Edmonton", + "America/Inuvik", + "America/Ojinaga", + "America/Yellowknife", + "MST7MDT" + ] + }, + { + "value": "Central America Standard Time", + "abbr": "CAST", + "offset": -6, + "isdst": false, + "text": "(UTC-06:00) Central America", + "utc": [ + "America/Belize", + "America/Costa_Rica", + "America/El_Salvador", + "America/Guatemala", + "America/Managua", + "America/Tegucigalpa", + "Etc/GMT+6", + "Pacific/Galapagos" + ] + }, + { + "value": "Central Standard Time", + "abbr": "CDT", + "offset": -5, + "isdst": true, + "text": "(UTC-06:00) Central Time (US & Canada)", + "utc": [ + "America/Chicago", + "America/Indiana/Knox", + "America/Indiana/Tell_City", + "America/Matamoros", + "America/Menominee", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Resolute", + "America/Winnipeg", + "CST6CDT" + ] + }, + { + "value": "Central Standard Time (Mexico)", + "abbr": "CDT", + "offset": -5, + "isdst": true, + "text": "(UTC-06:00) Guadalajara, Mexico City, Monterrey", + "utc": [ + "America/Bahia_Banderas", + "America/Cancun", + "America/Merida", + "America/Mexico_City", + "America/Monterrey" + ] + }, + { + "value": "Canada Central Standard Time", + "abbr": "CCST", + "offset": -6, + "isdst": false, + "text": "(UTC-06:00) Saskatchewan", + "utc": [ + "America/Regina", + "America/Swift_Current" + ] + }, + { + "value": "SA Pacific Standard Time", + "abbr": "SPST", + "offset": -5, + "isdst": false, + "text": "(UTC-05:00) Bogota, Lima, Quito", + "utc": [ + "America/Bogota", + "America/Cayman", + "America/Coral_Harbour", + "America/Eirunepe", + "America/Guayaquil", + "America/Jamaica", + "America/Lima", + "America/Panama", + "America/Rio_Branco", + "Etc/GMT+5" + ] + }, + { + "value": "Eastern Standard Time", + "abbr": "EDT", + "offset": -4, + "isdst": true, + "text": "(UTC-05:00) Eastern Time (US & Canada)", + "utc": [ + "America/Detroit", + "America/Havana", + "America/Indiana/Petersburg", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Iqaluit", + "America/Kentucky/Monticello", + "America/Louisville", + "America/Montreal", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Pangnirtung", + "America/Port-au-Prince", + "America/Thunder_Bay", + "America/Toronto", + "EST5EDT" + ] + }, + { + "value": "US Eastern Standard Time", + "abbr": "UEDT", + "offset": -4, + "isdst": true, + "text": "(UTC-05:00) Indiana (East)", + "utc": [ + "America/Indiana/Marengo", + "America/Indiana/Vevay", + "America/Indianapolis" + ] + }, + { + "value": "Venezuela Standard Time", + "abbr": "VST", + "offset": -4.5, + "isdst": false, + "text": "(UTC-04:30) Caracas", + "utc": [ + "America/Caracas" + ] + }, + { + "value": "Paraguay Standard Time", + "abbr": "PYT", + "offset": -4, + "isdst": false, + "text": "(UTC-04:00) Asuncion", + "utc": [ + "America/Asuncion" + ] + }, + { + "value": "Atlantic Standard Time", + "abbr": "ADT", + "offset": -3, + "isdst": true, + "text": "(UTC-04:00) Atlantic Time (Canada)", + "utc": [ + "America/Glace_Bay", + "America/Goose_Bay", + "America/Halifax", + "America/Moncton", + "America/Thule", + "Atlantic/Bermuda" + ] + }, + { + "value": "Central Brazilian Standard Time", + "abbr": "CBST", + "offset": -4, + "isdst": false, + "text": "(UTC-04:00) Cuiaba", + "utc": [ + "America/Campo_Grande", + "America/Cuiaba" + ] + }, + { + "value": "SA Western Standard Time", + "abbr": "SWST", + "offset": -4, + "isdst": false, + "text": "(UTC-04:00) Georgetown, La Paz, Manaus, San Juan", + "utc": [ + "America/Anguilla", + "America/Antigua", + "America/Aruba", + "America/Barbados", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Curacao", + "America/Dominica", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guyana", + "America/Kralendijk", + "America/La_Paz", + "America/Lower_Princes", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Montserrat", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Santo_Domingo", + "America/St_Barthelemy", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Tortola", + "Etc/GMT+4" + ] + }, + { + "value": "Pacific SA Standard Time", + "abbr": "PSST", + "offset": -4, + "isdst": false, + "text": "(UTC-04:00) Santiago", + "utc": [ + "America/Santiago", + "Antarctica/Palmer" + ] + }, + { + "value": "Newfoundland Standard Time", + "abbr": "NDT", + "offset": -2.5, + "isdst": true, + "text": "(UTC-03:30) Newfoundland", + "utc": [ + "America/St_Johns" + ] + }, + { + "value": "E. South America Standard Time", + "abbr": "ESAST", + "offset": -3, + "isdst": false, + "text": "(UTC-03:00) Brasilia", + "utc": [ + "America/Sao_Paulo" + ] + }, + { + "value": "Argentina Standard Time", + "abbr": "AST", + "offset": -3, + "isdst": false, + "text": "(UTC-03:00) Buenos Aires", + "utc": [ + "America/Argentina/La_Rioja", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Buenos_Aires", + "America/Catamarca", + "America/Cordoba", + "America/Jujuy", + "America/Mendoza" + ] + }, + { + "value": "SA Eastern Standard Time", + "abbr": "SEST", + "offset": -3, + "isdst": false, + "text": "(UTC-03:00) Cayenne, Fortaleza", + "utc": [ + "America/Araguaina", + "America/Belem", + "America/Cayenne", + "America/Fortaleza", + "America/Maceio", + "America/Paramaribo", + "America/Recife", + "America/Santarem", + "Antarctica/Rothera", + "Atlantic/Stanley", + "Etc/GMT+3" + ] + }, + { + "value": "Greenland Standard Time", + "abbr": "GDT", + "offset": -3, + "isdst": true, + "text": "(UTC-03:00) Greenland", + "utc": [ + "America/Godthab" + ] + }, + { + "value": "Montevideo Standard Time", + "abbr": "MST", + "offset": -3, + "isdst": false, + "text": "(UTC-03:00) Montevideo", + "utc": [ + "America/Montevideo" + ] + }, + { + "value": "Bahia Standard Time", + "abbr": "BST", + "offset": -3, + "isdst": false, + "text": "(UTC-03:00) Salvador", + "utc": [ + "America/Bahia" + ] + }, + { + "value": "UTC-02", + "abbr": "U", + "offset": -2, + "isdst": false, + "text": "(UTC-02:00) Coordinated Universal Time-02", + "utc": [ + "America/Noronha", + "Atlantic/South_Georgia", + "Etc/GMT+2" + ] + }, + { + "value": "Mid-Atlantic Standard Time", + "abbr": "MDT", + "offset": -1, + "isdst": true, + "text": "(UTC-02:00) Mid-Atlantic - Old", + "utc": [] + }, + { + "value": "Azores Standard Time", + "abbr": "ADT", + "offset": 0, + "isdst": true, + "text": "(UTC-01:00) Azores", + "utc": [ + "America/Scoresbysund", + "Atlantic/Azores" + ] + }, + { + "value": "Cape Verde Standard Time", + "abbr": "CVST", + "offset": -1, + "isdst": false, + "text": "(UTC-01:00) Cape Verde Is.", + "utc": [ + "Atlantic/Cape_Verde", + "Etc/GMT+1" + ] + }, + { + "value": "Morocco Standard Time", + "abbr": "MDT", + "offset": 1, + "isdst": true, + "text": "(UTC) Casablanca", + "utc": [ + "Africa/Casablanca", + "Africa/El_Aaiun" + ] + }, + { + "value": "UTC", + "abbr": "UTC", + "offset": 0, + "isdst": false, + "text": "(UTC) Coordinated Universal Time", + "utc": [ + "America/Danmarkshavn", + "Etc/GMT" + ] + }, + { + "value": "GMT Standard Time", + "abbr": "GDT", + "offset": 1, + "isdst": true, + "text": "(UTC) Dublin, Edinburgh, Lisbon, London", + "utc": [ + "Atlantic/Canary", + "Atlantic/Faeroe", + "Atlantic/Madeira", + "Europe/Dublin", + "Europe/Guernsey", + "Europe/Isle_of_Man", + "Europe/Jersey", + "Europe/Lisbon", + "Europe/London" + ] + }, + { + "value": "Greenwich Standard Time", + "abbr": "GST", + "offset": 0, + "isdst": false, + "text": "(UTC) Monrovia, Reykjavik", + "utc": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Bamako", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Freetown", + "Africa/Lome", + "Africa/Monrovia", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Sao_Tome", + "Atlantic/Reykjavik", + "Atlantic/St_Helena" + ] + }, + { + "value": "W. Europe Standard Time", + "abbr": "WEDT", + "offset": 2, + "isdst": true, + "text": "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna", + "utc": [ + "Arctic/Longyearbyen", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Berlin", + "Europe/Busingen", + "Europe/Gibraltar", + "Europe/Luxembourg", + "Europe/Malta", + "Europe/Monaco", + "Europe/Oslo", + "Europe/Rome", + "Europe/San_Marino", + "Europe/Stockholm", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Zurich" + ] + }, + { + "value": "Central Europe Standard Time", + "abbr": "CEDT", + "offset": 2, + "isdst": true, + "text": "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague", + "utc": [ + "Europe/Belgrade", + "Europe/Bratislava", + "Europe/Budapest", + "Europe/Ljubljana", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Tirane" + ] + }, + { + "value": "Romance Standard Time", + "abbr": "RDT", + "offset": 2, + "isdst": true, + "text": "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris", + "utc": [ + "Africa/Ceuta", + "Europe/Brussels", + "Europe/Copenhagen", + "Europe/Madrid", + "Europe/Paris" + ] + }, + { + "value": "Central European Standard Time", + "abbr": "CEDT", + "offset": 2, + "isdst": true, + "text": "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb", + "utc": [ + "Europe/Sarajevo", + "Europe/Skopje", + "Europe/Warsaw", + "Europe/Zagreb" + ] + }, + { + "value": "W. Central Africa Standard Time", + "abbr": "WCAST", + "offset": 1, + "isdst": false, + "text": "(UTC+01:00) West Central Africa", + "utc": [ + "Africa/Algiers", + "Africa/Bangui", + "Africa/Brazzaville", + "Africa/Douala", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Luanda", + "Africa/Malabo", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Porto-Novo", + "Africa/Tunis", + "Etc/GMT-1" + ] + }, + { + "value": "Namibia Standard Time", + "abbr": "NST", + "offset": 1, + "isdst": false, + "text": "(UTC+01:00) Windhoek", + "utc": [ + "Africa/Windhoek" + ] + }, + { + "value": "GTB Standard Time", + "abbr": "GDT", + "offset": 3, + "isdst": true, + "text": "(UTC+02:00) Athens, Bucharest", + "utc": [ + "Asia/Nicosia", + "Europe/Athens", + "Europe/Bucharest", + "Europe/Chisinau" + ] + }, + { + "value": "Middle East Standard Time", + "abbr": "MEDT", + "offset": 3, + "isdst": true, + "text": "(UTC+02:00) Beirut", + "utc": [ + "Asia/Beirut" + ] + }, + { + "value": "Egypt Standard Time", + "abbr": "EST", + "offset": 2, + "isdst": false, + "text": "(UTC+02:00) Cairo", + "utc": [ + "Africa/Cairo" + ] + }, + { + "value": "Syria Standard Time", + "abbr": "SDT", + "offset": 3, + "isdst": true, + "text": "(UTC+02:00) Damascus", + "utc": [ + "Asia/Damascus" + ] + }, + { + "value": "E. Europe Standard Time", + "abbr": "EEDT", + "offset": 3, + "isdst": true, + "text": "(UTC+02:00) E. Europe", + "utc": [ + "Asia/Nicosia", + "Europe/Athens", + "Europe/Bucharest", + "Europe/Chisinau", + "Europe/Helsinki", + "Europe/Mariehamn", + "Europe/Nicosia", + "Europe/Riga", + "Europe/Sofia", + "Europe/Tallinn", + "Europe/Uzhgorod", + "Europe/Vilnius" + ] + }, + { + "value": "South Africa Standard Time", + "abbr": "SAST", + "offset": 2, + "isdst": false, + "text": "(UTC+02:00) Harare, Pretoria", + "utc": [ + "Africa/Blantyre", + "Africa/Bujumbura", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Kigali", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Etc/GMT-2" + ] + }, + { + "value": "FLE Standard Time", + "abbr": "FDT", + "offset": 3, + "isdst": true, + "text": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius", + "utc": [ + "Europe/Helsinki", + "Europe/Kiev", + "Europe/Mariehamn", + "Europe/Riga", + "Europe/Sofia", + "Europe/Tallinn", + "Europe/Uzhgorod", + "Europe/Vilnius", + "Europe/Zaporozhye" + ] + }, + { + "value": "Turkey Standard Time", + "abbr": "TDT", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Istanbul", + "utc": [ + "Europe/Istanbul" + ] + }, + { + "value": "Israel Standard Time", + "abbr": "JDT", + "offset": 3, + "isdst": true, + "text": "(UTC+02:00) Jerusalem", + "utc": [ + "Asia/Jerusalem" + ] + }, + { + "value": "Libya Standard Time", + "abbr": "LST", + "offset": 2, + "isdst": false, + "text": "(UTC+02:00) Tripoli", + "utc": [ + "Africa/Tripoli" + ] + }, + { + "value": "Jordan Standard Time", + "abbr": "JST", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Amman", + "utc": [ + "Asia/Amman" + ] + }, + { + "value": "Arabic Standard Time", + "abbr": "AST", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Baghdad", + "utc": [ + "Asia/Baghdad" + ] + }, + { + "value": "Kaliningrad Standard Time", + "abbr": "KST", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Kaliningrad, Minsk", + "utc": [ + "Europe/Kaliningrad", + "Europe/Minsk" + ] + }, + { + "value": "Arab Standard Time", + "abbr": "AST", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Kuwait, Riyadh", + "utc": [ + "Asia/Aden", + "Asia/Bahrain", + "Asia/Kuwait", + "Asia/Qatar", + "Asia/Riyadh" + ] + }, + { + "value": "E. Africa Standard Time", + "abbr": "EAST", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Nairobi", + "utc": [ + "Africa/Addis_Ababa", + "Africa/Asmera", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Mogadishu", + "Africa/Nairobi", + "Antarctica/Syowa", + "Etc/GMT-3", + "Indian/Antananarivo", + "Indian/Comoro", + "Indian/Mayotte" + ] + }, + { + "value": "Moscow Standard Time", + "abbr": "MSK", + "offset": 3, + "isdst": false, + "text": "(UTC+03:00) Moscow, St. Petersburg, Volgograd", + "utc": [ + "Europe/Kirov", + "Europe/Moscow", + "Europe/Simferopol", + "Europe/Volgograd" + ] + }, + { + "value": "Samara Time", + "abbr": "SAMT", + "offset": 4, + "isdst": false, + "text": "(UTC+04:00) Samara, Ulyanovsk, Saratov", + "utc": [ + "Europe/Astrakhan", + "Europe/Samara", + "Europe/Ulyanovsk" + ] + }, + { + "value": "Iran Standard Time", + "abbr": "IDT", + "offset": 4.5, + "isdst": true, + "text": "(UTC+03:30) Tehran", + "utc": [ + "Asia/Tehran" + ] + }, + { + "value": "Arabian Standard Time", + "abbr": "AST", + "offset": 4, + "isdst": false, + "text": "(UTC+04:00) Abu Dhabi, Muscat", + "utc": [ + "Asia/Dubai", + "Asia/Muscat", + "Etc/GMT-4" + ] + }, + { + "value": "Azerbaijan Standard Time", + "abbr": "ADT", + "offset": 5, + "isdst": true, + "text": "(UTC+04:00) Baku", + "utc": [ + "Asia/Baku" + ] + }, + { + "value": "Mauritius Standard Time", + "abbr": "MST", + "offset": 4, + "isdst": false, + "text": "(UTC+04:00) Port Louis", + "utc": [ + "Indian/Mahe", + "Indian/Mauritius", + "Indian/Reunion" + ] + }, + { + "value": "Georgian Standard Time", + "abbr": "GST", + "offset": 4, + "isdst": false, + "text": "(UTC+04:00) Tbilisi", + "utc": [ + "Asia/Tbilisi" + ] + }, + { + "value": "Caucasus Standard Time", + "abbr": "CST", + "offset": 4, + "isdst": false, + "text": "(UTC+04:00) Yerevan", + "utc": [ + "Asia/Yerevan" + ] + }, + { + "value": "Afghanistan Standard Time", + "abbr": "AST", + "offset": 4.5, + "isdst": false, + "text": "(UTC+04:30) Kabul", + "utc": [ + "Asia/Kabul" + ] + }, + { + "value": "West Asia Standard Time", + "abbr": "WAST", + "offset": 5, + "isdst": false, + "text": "(UTC+05:00) Ashgabat, Tashkent", + "utc": [ + "Antarctica/Mawson", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Dushanbe", + "Asia/Oral", + "Asia/Samarkand", + "Asia/Tashkent", + "Etc/GMT-5", + "Indian/Kerguelen", + "Indian/Maldives" + ] + }, + { + "value": "Pakistan Standard Time", + "abbr": "PST", + "offset": 5, + "isdst": false, + "text": "(UTC+05:00) Islamabad, Karachi", + "utc": [ + "Asia/Karachi" + ] + }, + { + "value": "India Standard Time", + "abbr": "IST", + "offset": 5.5, + "isdst": false, + "text": "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi", + "utc": [ + "Asia/Kolkata" + ] + }, + { + "value": "Sri Lanka Standard Time", + "abbr": "SLST", + "offset": 5.5, + "isdst": false, + "text": "(UTC+05:30) Sri Jayawardenepura", + "utc": [ + "Asia/Colombo" + ] + }, + { + "value": "Nepal Standard Time", + "abbr": "NST", + "offset": 5.75, + "isdst": false, + "text": "(UTC+05:45) Kathmandu", + "utc": [ + "Asia/Katmandu" + ] + }, + { + "value": "Central Asia Standard Time", + "abbr": "CAST", + "offset": 6, + "isdst": false, + "text": "(UTC+06:00) Astana", + "utc": [ + "Antarctica/Vostok", + "Asia/Almaty", + "Asia/Bishkek", + "Asia/Qyzylorda", + "Asia/Urumqi", + "Etc/GMT-6", + "Indian/Chagos" + ] + }, + { + "value": "Bangladesh Standard Time", + "abbr": "BST", + "offset": 6, + "isdst": false, + "text": "(UTC+06:00) Dhaka", + "utc": [ + "Asia/Dhaka", + "Asia/Thimphu" + ] + }, + { + "value": "Ekaterinburg Standard Time", + "abbr": "EST", + "offset": 6, + "isdst": false, + "text": "(UTC+06:00) Ekaterinburg", + "utc": [ + "Asia/Yekaterinburg" + ] + }, + { + "value": "Myanmar Standard Time", + "abbr": "MST", + "offset": 6.5, + "isdst": false, + "text": "(UTC+06:30) Yangon (Rangoon)", + "utc": [ + "Asia/Rangoon", + "Indian/Cocos" + ] + }, + { + "value": "SE Asia Standard Time", + "abbr": "SAST", + "offset": 7, + "isdst": false, + "text": "(UTC+07:00) Bangkok, Hanoi, Jakarta", + "utc": [ + "Antarctica/Davis", + "Asia/Bangkok", + "Asia/Hovd", + "Asia/Jakarta", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Saigon", + "Asia/Vientiane", + "Etc/GMT-7", + "Indian/Christmas" + ] + }, + { + "value": "N. Central Asia Standard Time", + "abbr": "NCAST", + "offset": 7, + "isdst": false, + "text": "(UTC+07:00) Novosibirsk", + "utc": [ + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk" + ] + }, + { + "value": "China Standard Time", + "abbr": "CST", + "offset": 8, + "isdst": false, + "text": "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi", + "utc": [ + "Asia/Hong_Kong", + "Asia/Macau", + "Asia/Shanghai" + ] + }, + { + "value": "North Asia Standard Time", + "abbr": "NAST", + "offset": 8, + "isdst": false, + "text": "(UTC+08:00) Krasnoyarsk", + "utc": [ + "Asia/Krasnoyarsk" + ] + }, + { + "value": "Singapore Standard Time", + "abbr": "MPST", + "offset": 8, + "isdst": false, + "text": "(UTC+08:00) Kuala Lumpur, Singapore", + "utc": [ + "Asia/Brunei", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Makassar", + "Asia/Manila", + "Asia/Singapore", + "Etc/GMT-8" + ] + }, + { + "value": "W. Australia Standard Time", + "abbr": "WAST", + "offset": 8, + "isdst": false, + "text": "(UTC+08:00) Perth", + "utc": [ + "Antarctica/Casey", + "Australia/Perth" + ] + }, + { + "value": "Taipei Standard Time", + "abbr": "TST", + "offset": 8, + "isdst": false, + "text": "(UTC+08:00) Taipei", + "utc": [ + "Asia/Taipei" + ] + }, + { + "value": "Ulaanbaatar Standard Time", + "abbr": "UST", + "offset": 8, + "isdst": false, + "text": "(UTC+08:00) Ulaanbaatar", + "utc": [ + "Asia/Choibalsan", + "Asia/Ulaanbaatar" + ] + }, + { + "value": "North Asia East Standard Time", + "abbr": "NAEST", + "offset": 9, + "isdst": false, + "text": "(UTC+09:00) Irkutsk", + "utc": [ + "Asia/Irkutsk" + ] + }, + { + "value": "Tokyo Standard Time", + "abbr": "TST", + "offset": 9, + "isdst": false, + "text": "(UTC+09:00) Osaka, Sapporo, Tokyo", + "utc": [ + "Asia/Dili", + "Asia/Jayapura", + "Asia/Tokyo", + "Etc/GMT-9", + "Pacific/Palau" + ] + }, + { + "value": "Korea Standard Time", + "abbr": "KST", + "offset": 9, + "isdst": false, + "text": "(UTC+09:00) Seoul", + "utc": [ + "Asia/Pyongyang", + "Asia/Seoul" + ] + }, + { + "value": "Cen. Australia Standard Time", + "abbr": "CAST", + "offset": 9.5, + "isdst": false, + "text": "(UTC+09:30) Adelaide", + "utc": [ + "Australia/Adelaide", + "Australia/Broken_Hill" + ] + }, + { + "value": "AUS Central Standard Time", + "abbr": "ACST", + "offset": 9.5, + "isdst": false, + "text": "(UTC+09:30) Darwin", + "utc": [ + "Australia/Darwin" + ] + }, + { + "value": "E. Australia Standard Time", + "abbr": "EAST", + "offset": 10, + "isdst": false, + "text": "(UTC+10:00) Brisbane", + "utc": [ + "Australia/Brisbane", + "Australia/Lindeman" + ] + }, + { + "value": "AUS Eastern Standard Time", + "abbr": "AEST", + "offset": 10, + "isdst": false, + "text": "(UTC+10:00) Canberra, Melbourne, Sydney", + "utc": [ + "Australia/Melbourne", + "Australia/Sydney" + ] + }, + { + "value": "West Pacific Standard Time", + "abbr": "WPST", + "offset": 10, + "isdst": false, + "text": "(UTC+10:00) Guam, Port Moresby", + "utc": [ + "Antarctica/DumontDUrville", + "Etc/GMT-10", + "Pacific/Guam", + "Pacific/Port_Moresby", + "Pacific/Saipan", + "Pacific/Truk" + ] + }, + { + "value": "Tasmania Standard Time", + "abbr": "TST", + "offset": 10, + "isdst": false, + "text": "(UTC+10:00) Hobart", + "utc": [ + "Australia/Currie", + "Australia/Hobart" + ] + }, + { + "value": "Yakutsk Standard Time", + "abbr": "YST", + "offset": 10, + "isdst": false, + "text": "(UTC+10:00) Yakutsk", + "utc": [ + "Asia/Chita", + "Asia/Khandyga", + "Asia/Yakutsk" + ] + }, + { + "value": "Central Pacific Standard Time", + "abbr": "CPST", + "offset": 11, + "isdst": false, + "text": "(UTC+11:00) Solomon Is., New Caledonia", + "utc": [ + "Antarctica/Macquarie", + "Etc/GMT-11", + "Pacific/Efate", + "Pacific/Guadalcanal", + "Pacific/Kosrae", + "Pacific/Noumea", + "Pacific/Ponape" + ] + }, + { + "value": "Vladivostok Standard Time", + "abbr": "VST", + "offset": 11, + "isdst": false, + "text": "(UTC+11:00) Vladivostok", + "utc": [ + "Asia/Sakhalin", + "Asia/Ust-Nera", + "Asia/Vladivostok" + ] + }, + { + "value": "New Zealand Standard Time", + "abbr": "NZST", + "offset": 12, + "isdst": false, + "text": "(UTC+12:00) Auckland, Wellington", + "utc": [ + "Antarctica/McMurdo", + "Pacific/Auckland" + ] + }, + { + "value": "UTC+12", + "abbr": "U", + "offset": 12, + "isdst": false, + "text": "(UTC+12:00) Coordinated Universal Time+12", + "utc": [ + "Etc/GMT-12", + "Pacific/Funafuti", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Nauru", + "Pacific/Tarawa", + "Pacific/Wake", + "Pacific/Wallis" + ] + }, + { + "value": "Fiji Standard Time", + "abbr": "FST", + "offset": 12, + "isdst": false, + "text": "(UTC+12:00) Fiji", + "utc": [ + "Pacific/Fiji" + ] + }, + { + "value": "Magadan Standard Time", + "abbr": "MST", + "offset": 12, + "isdst": false, + "text": "(UTC+12:00) Magadan", + "utc": [ + "Asia/Anadyr", + "Asia/Kamchatka", + "Asia/Magadan", + "Asia/Srednekolymsk" + ] + }, + { + "value": "Kamchatka Standard Time", + "abbr": "KDT", + "offset": 13, + "isdst": true, + "text": "(UTC+12:00) Petropavlovsk-Kamchatsky - Old", + "utc": [ + "Asia/Kamchatka" + ] + }, + { + "value": "Tonga Standard Time", + "abbr": "TST", + "offset": 13, + "isdst": false, + "text": "(UTC+13:00) Nuku'alofa", + "utc": [ + "Etc/GMT-13", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Tongatapu" + ] + }, + { + "value": "Samoa Standard Time", + "abbr": "SST", + "offset": 13, + "isdst": false, + "text": "(UTC+13:00) Samoa", + "utc": [ + "Pacific/Apia" + ] + } +] \ No newline at end of file diff --git a/app/src/main/java/com/nynja/mobile/communicator/data/FragmentTransferObject.java b/app/src/main/java/com/nynja/mobile/communicator/data/FragmentTransferObject.java index 67a204afe6db8bfb5363fb5cbce2752523576b84..1bf39a688791cc0e8081a1efc30c4bdd7bf54c11 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/data/FragmentTransferObject.java +++ b/app/src/main/java/com/nynja/mobile/communicator/data/FragmentTransferObject.java @@ -2,6 +2,7 @@ package com.nynja.mobile.communicator.data; import com.nynja.mobile.communicator.data.models.mqtt.BaseParcelMQQT; import com.nynja.mobile.communicator.data.models.mqtt.Contact; +import com.nynja.mobile.communicator.data.models.mqtt.Message; import java.util.ArrayList; @@ -27,7 +28,8 @@ public class FragmentTransferObject { OpenChooseUsersEditModeView, OpenChooseUsersEditModeDelete, OpenChooseUsersCreateModeNew, - OpenChooseUsersCreateModeAdd + OpenChooseUsersCreateModeAdd, + OpenScheduledMessage, } public FragmentTransferObject(ArrayList contacts, BaseParcelMQQT prevModel, Type type) { diff --git a/app/src/main/java/com/nynja/mobile/communicator/data/models/mqtt/BaseMQQT.java b/app/src/main/java/com/nynja/mobile/communicator/data/models/mqtt/BaseMQQT.java index 9dd8bafe2fb3d90325d5e9cea0c143bc2fd99cb5..887e90aea17f1d38a3e0c27b541a63c3107581cd 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/data/models/mqtt/BaseMQQT.java +++ b/app/src/main/java/com/nynja/mobile/communicator/data/models/mqtt/BaseMQQT.java @@ -60,5 +60,6 @@ public abstract class BaseMQQT { String File = "file"; String Thumb = "thumb"; String Contact = "contact"; + String TimeZone = "timezone"; } } diff --git a/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ChatPresenter.java b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ChatPresenter.java index 7378bf6f56665e91f7980360532aa3f82b2e45db..c325a908930eb0afc94794030b5a50b1c3ddf240 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ChatPresenter.java +++ b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ChatPresenter.java @@ -16,15 +16,18 @@ import com.nynja.mobile.communicator.data.models.mqtt.Room; import com.nynja.mobile.communicator.data.upload.UploadProgressInterface; import com.nynja.mobile.communicator.mvp.view.ChatView; import com.nynja.mobile.communicator.ui.fragments.chats.ChatSettingsFragment; +import com.nynja.mobile.communicator.ui.fragments.chats.ScheduledMessagesFragment; import com.nynja.mobile.communicator.ui.fragments.contacts.ShareContactFragment; import com.nynja.mobile.communicator.ui.fragments.profile.MyProfileFragment; import com.nynja.mobile.communicator.ui.fragments.profile.UserProfileFragment; import com.nynja.mobile.communicator.utils.StringUtils; +import com.nynja.mobile.communicator.utils.TimeZoneUtil; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.TimeZone; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -96,6 +99,34 @@ public class ChatPresenter extends BaseErrorPresenter { mRouter.replaceScreen(ShareContactFragment.class.getSimpleName(), fto); } + public void navigateToScheduledMessageScreen(String text) { + Message message = new Message(); + + Desc messageDesc = new Desc(); + messageDesc.mime = BaseMQQT.MimeTypes.Text; + messageDesc.payload = text; + message.files.add(messageDesc); + + Contact contact = mDataManager.getContactByChatId(mRoom.id); + ArrayList contacts = new ArrayList<>(); + contacts.add(contact); + Desc contactDesc = new Desc(); + contactDesc.mime = BaseMQQT.MimeTypes.Contact; + contactDesc.payload = EncodeFactory.encodeContacts(contacts); + message.files.add(contactDesc); + + Desc timeZoneDesc = new Desc(); + timeZoneDesc.mime = BaseMQQT.MimeTypes.TimeZone; + timeZoneDesc.payload = TimeZoneUtil.getCityFromFullString(TimeZone.getDefault().getID()); + message.files.add(timeZoneDesc); + + if (contact != null && text != null) { + final FragmentTransferObject fto = new FragmentTransferObject(message, + FragmentTransferObject.Type.OpenScheduledMessage, null); + mRouter.replaceScreen(ScheduledMessagesFragment.class.getSimpleName(), fto); + } + } + @Override public void detachView(ChatView view) { super.detachView(view); mDataManager.setActiveRoom(null); diff --git a/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/MyProfilePresenter.java b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/MyProfilePresenter.java index 49568e68672f23d63686c459ec2a843a69b8bbe1..4e3dab9ad364661298f1fd317836a68f41e0c7cf 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/MyProfilePresenter.java +++ b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/MyProfilePresenter.java @@ -14,6 +14,7 @@ import com.nynja.mobile.communicator.ui.fragments.chats.ChatFragment; import com.nynja.mobile.communicator.ui.fragments.chats.RoomListFragment; import com.nynja.mobile.communicator.ui.fragments.chats.ScheduledMessagesFragment; import com.nynja.mobile.communicator.ui.fragments.chats.StarredMessagesFragment; +import com.nynja.mobile.communicator.ui.fragments.chats.TimeZoneSelectorFragment; import com.nynja.mobile.communicator.ui.fragments.contacts.HistoryFragment; import com.nynja.mobile.communicator.ui.fragments.profile.MyBalanceFragment; import com.nynja.mobile.communicator.ui.fragments.profile.MyProfileFragment; @@ -202,7 +203,6 @@ public class MyProfilePresenter extends BasePresenter { break; case VHModel.SCHEDULED_MESSAGES_TAG: - openScheduledMessages(); break; case VHModel.STARRED_MESSAGES_TAG: @@ -228,10 +228,6 @@ public class MyProfilePresenter extends BasePresenter { mRouter.replaceScreen(StarredMessagesFragment.class.getSimpleName()); } - private void openScheduledMessages() { - mRouter.replaceScreen(ScheduledMessagesFragment.class.getSimpleName()); - } - private void openChat(VHModel item) { if (item.mObject instanceof Room) navigateToChat((Room) item.mObject); } diff --git a/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ScheduledMessagesPresenter.java b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ScheduledMessagesPresenter.java new file mode 100644 index 0000000000000000000000000000000000000000..45616f4a4f3b142bea0d90d37fde9935a3f25e9e --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/ScheduledMessagesPresenter.java @@ -0,0 +1,68 @@ +package com.nynja.mobile.communicator.mvp.presenters; + +import android.content.Context; + +import com.arellomobile.mvp.InjectViewState; +import com.nynja.mobile.communicator.data.FragmentTransferObject; +import com.nynja.mobile.communicator.data.models.mqtt.BaseMQQT; +import com.nynja.mobile.communicator.data.models.mqtt.Desc; +import com.nynja.mobile.communicator.data.models.mqtt.Message; +import com.nynja.mobile.communicator.mvp.view.ScheduledMessagesView; +import com.nynja.mobile.communicator.ui.fragments.chats.TimeZoneSelectorFragment; +import com.nynja.mobile.communicator.utils.TimeZoneData; +import com.nynja.mobile.communicator.utils.TimeZoneUtil; + +import java.util.List; +import java.util.TimeZone; + + +@InjectViewState +public class ScheduledMessagesPresenter extends BasePresenter { + + public void setMessage(Message message) { + getViewState().setMessage(message); + } + + public void navigateToTimeZoneSelectorFragment(Message message) { + final FragmentTransferObject fto = new FragmentTransferObject(message, + FragmentTransferObject.Type.OpenScheduledMessage, null); + mRouter.replaceScreen(TimeZoneSelectorFragment.class.getSimpleName(), fto); + } + + public void setTimeFromMessage(Message message, Context context) { + for (Desc desc : message.files) { + if (desc.mime.equals(BaseMQQT.MimeTypes.TimeZone)) { + TimeZoneData timeZoneData = getCurrentTimeZoneByCity(desc.payload, context); + if (timeZoneData != null) { + getViewState().setTime(timeZoneData.getText()); + } + break; + } + } + } + + private TimeZoneData getCurrentTimeZoneByCity(String city, Context context) { + List timeZoneData = TimeZoneUtil.parseTimeZonesFromJson(context); + for (TimeZoneData item : timeZoneData) { + if (item.getCity().equals(city)) { + return item; + } + } + + return null; + } + + public void keepDefaultTime(Message message, Context context) { + String city = TimeZoneUtil.getCityFromFullString(TimeZone.getDefault().getID()); + TimeZoneData timeZoneData = getCurrentTimeZoneByCity(city, context); + if (timeZoneData != null) { + for (Desc desc : message.files) { + if (desc.mime.equals(BaseMQQT.MimeTypes.TimeZone)) { + desc.payload = TimeZone.getDefault().getID(); + break; + } + } + getViewState().setTime(timeZoneData.getText()); + } + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/TimeZoneSelectorPresenter.java b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/TimeZoneSelectorPresenter.java new file mode 100644 index 0000000000000000000000000000000000000000..0d6f4fd33982614b7f7fa1b48646017fc2096bba --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/mvp/presenters/TimeZoneSelectorPresenter.java @@ -0,0 +1,36 @@ +package com.nynja.mobile.communicator.mvp.presenters; + +import com.arellomobile.mvp.InjectViewState; +import com.nynja.mobile.communicator.data.FragmentTransferObject; +import com.nynja.mobile.communicator.data.models.mqtt.BaseMQQT; +import com.nynja.mobile.communicator.data.models.mqtt.Desc; +import com.nynja.mobile.communicator.data.models.mqtt.Message; +import com.nynja.mobile.communicator.mvp.view.TimeZoneSelectorView; +import com.nynja.mobile.communicator.ui.fragments.chats.ScheduledMessagesFragment; +import com.nynja.mobile.communicator.utils.TimeZoneData; + + +@InjectViewState +public class TimeZoneSelectorPresenter extends BasePresenter { + + public void closeFragment(Message message) { + final FragmentTransferObject fto = new FragmentTransferObject(message, + FragmentTransferObject.Type.OpenScheduledMessage, null); + mRouter.replaceScreen(ScheduledMessagesFragment.class.getSimpleName(), fto); + } + + public void onTimeZoneClicked(Message message, TimeZoneData timeZoneData) { + for (Desc desc : message.files) { + if (desc.mime.equals(BaseMQQT.MimeTypes.TimeZone)) { + desc.payload = timeZoneData.getCity(); + } + } + final FragmentTransferObject fto = new FragmentTransferObject(message, + FragmentTransferObject.Type.OpenScheduledMessage, null); + mRouter.replaceScreen(ScheduledMessagesFragment.class.getSimpleName(), fto); + } + + public void setMessage(Message message) { + getViewState().setMessage(message); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/mvp/view/ScheduledMessagesView.java b/app/src/main/java/com/nynja/mobile/communicator/mvp/view/ScheduledMessagesView.java new file mode 100644 index 0000000000000000000000000000000000000000..ddeeaa8b3604818f8a7ad0e0b610c315e89b3d4e --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/mvp/view/ScheduledMessagesView.java @@ -0,0 +1,13 @@ +package com.nynja.mobile.communicator.mvp.view; + +import android.support.annotation.NonNull; + +import com.nynja.mobile.communicator.data.models.mqtt.Message; + + +public interface ScheduledMessagesView extends BaseMvpView { + + void setMessage(@NonNull Message message); + + void setTime(String timezone); +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/mvp/view/TimeZoneSelectorView.java b/app/src/main/java/com/nynja/mobile/communicator/mvp/view/TimeZoneSelectorView.java new file mode 100644 index 0000000000000000000000000000000000000000..1e499e17ced11ea8430d7d8b5fe8b495536977ab --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/mvp/view/TimeZoneSelectorView.java @@ -0,0 +1,15 @@ +package com.nynja.mobile.communicator.mvp.view; + +import android.support.annotation.NonNull; + +import com.nynja.mobile.communicator.data.models.mqtt.Message; + +/** + * Created by pavel on 11/21/17. + */ + +public interface TimeZoneSelectorView extends BaseMvpView { + + void setMessage(@NonNull Message message); + +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/activities/HomeActivity.java b/app/src/main/java/com/nynja/mobile/communicator/ui/activities/HomeActivity.java index 363d80f3924b579f90b79d89c443976ff9414e42..828d47d39498d86c3f499a8411b40d2f5afed5e0 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/ui/activities/HomeActivity.java +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/activities/HomeActivity.java @@ -26,6 +26,7 @@ import com.nynja.mobile.communicator.data.models.CountryCode; import com.nynja.mobile.communicator.data.models.PushNotificationCounter; import com.nynja.mobile.communicator.data.models.mqtt.BaseParcelMQQT; import com.nynja.mobile.communicator.data.models.mqtt.Contact; +import com.nynja.mobile.communicator.data.models.mqtt.Message; import com.nynja.mobile.communicator.data.models.mqtt.Room; import com.nynja.mobile.communicator.data.voximplant.ActiveCall; import com.nynja.mobile.communicator.data.voximplant.VoxImplantModule; @@ -43,6 +44,7 @@ import com.nynja.mobile.communicator.ui.fragments.chats.CreateGroupChatFragment; import com.nynja.mobile.communicator.ui.fragments.chats.RoomListFragment; import com.nynja.mobile.communicator.ui.fragments.chats.ScheduledMessagesFragment; import com.nynja.mobile.communicator.ui.fragments.chats.StarredMessagesFragment; +import com.nynja.mobile.communicator.ui.fragments.chats.TimeZoneSelectorFragment; import com.nynja.mobile.communicator.ui.fragments.contacts.AddContactByPhoneFragment; import com.nynja.mobile.communicator.ui.fragments.contacts.CountryPickerFragment; import com.nynja.mobile.communicator.ui.fragments.contacts.HistoryFragment; @@ -231,7 +233,9 @@ public class HomeActivity extends BaseActivity implements HomeView, MenuButton.O (!currentFragment.getClass().getSimpleName().equalsIgnoreCase(simpleName) || ChatFragment.class.getSimpleName().equalsIgnoreCase(simpleName) || RoomListFragment.class.getSimpleName().equalsIgnoreCase(simpleName) - || UserProfileFragment.class.getSimpleName().equalsIgnoreCase(simpleName))) { + || UserProfileFragment.class.getSimpleName().equalsIgnoreCase(simpleName) + || ScheduledMessagesFragment.class.getSimpleName().equalsIgnoreCase(simpleName) + || TimeZoneSelectorFragment.class.getSimpleName().equalsIgnoreCase(simpleName))) { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.container, fragment, simpleName); fragmentTransaction.commit(); @@ -318,8 +322,6 @@ public class HomeActivity extends BaseActivity implements HomeView, MenuButton.O } } else if (StarredMessagesFragment.class.getSimpleName().equalsIgnoreCase(screenKey)) { fragment = StarredMessagesFragment.newInstance(); - } else if (ScheduledMessagesFragment.class.getSimpleName().equalsIgnoreCase(screenKey)) { - fragment = ScheduledMessagesFragment.newInstance(); } else if (MyBalanceFragment.class.getSimpleName().equalsIgnoreCase(screenKey)) { fragment = MyBalanceFragment.newInstance(); } else if (ChatSettingsFragment.class.getSimpleName().equals(screenKey)) { @@ -328,6 +330,14 @@ public class HomeActivity extends BaseActivity implements HomeView, MenuButton.O } } else if (ChatSettingsRulesFragment.class.getSimpleName().equals(screenKey)) { fragment = ChatSettingsRulesFragment.newInstance((Room) fto.mPrevModel); + } else if (ScheduledMessagesFragment.class.getSimpleName().equals(screenKey)) { + if (fto != null && fto.mPrevModel instanceof Message) { + fragment = ScheduledMessagesFragment.newInstance((Message) fto.mPrevModel); + } + } else if (TimeZoneSelectorFragment.class.getSimpleName().equals(screenKey)) { + if (fto != null && fto.mPrevModel instanceof Message) { + fragment = TimeZoneSelectorFragment.newInstance((Message) fto.mPrevModel); + } } return fragment; } diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/MyProfileAdapter.java b/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/MyProfileAdapter.java index 8016eaf6ca73316ef7a130af9d8750fe45f84525..33614ba9a9eeddc4143314a9200e8439e8eeb6a8 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/MyProfileAdapter.java +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/MyProfileAdapter.java @@ -70,7 +70,7 @@ public class MyProfileAdapter extends BaseAdapter> { case CONTACT_REQUEST_TAG: return new MyProfileContactMessageVH(parent, mListener); case SCHEDULED_MESSAGES_TAG: - return new MyProfileScheduledMessageVH(parent); + return new MyProfileScheduledMessageVH(parent, mListener); } return null; } diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/TimeZoneAdapter.java b/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/TimeZoneAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..cabac87be0c585498d2ad389c67a3740f87ebaab --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/TimeZoneAdapter.java @@ -0,0 +1,144 @@ +package com.nynja.mobile.communicator.ui.adapters; + +import android.content.Context; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.TextView; + +import com.l4digital.fastscroll.FastScroller; +import com.nynja.mobile.communicator.R; +import com.nynja.mobile.communicator.ui.adapters.viewholders.CountryPickerLetterViewHolder; +import com.nynja.mobile.communicator.utils.TimeZoneData; +import com.nynja.mobile.communicator.ui.views.calendar.ColorCircleDrawable; +import com.nynja.mobile.communicator.utils.TimeZoneUtil; +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class TimeZoneAdapter extends RecyclerView.Adapter + implements FastScroller.SectionIndexer, + StickyRecyclerHeadersAdapter, + Filterable { + + private List data; + private List list; + + + public TimeZoneAdapter(Context context) { + data = TimeZoneUtil.parseTimeZonesFromJson(context); + Collections.sort(data, (o1, o2) -> o1.getCity().compareTo(o2.getCity())); + list = data; + } + + private boolean containsIgnoreCase(String str, String searchStr) { + if (str == null || searchStr == null) return false; + final int length = searchStr.length(); + if (length == 0) return true; + for (int i = str.length() - length; i >= 0; i--) { + if (str.regionMatches(true, i, searchStr, 0, length)) + return true; + } + return false; + } + + @Override public long getHeaderId(int position) { + return getCellChar(position); + } + + @Override + public CountryPickerLetterViewHolder onCreateHeaderViewHolder(ViewGroup parent) { + return new CountryPickerLetterViewHolder(parent); + } + + @Override + public void onBindHeaderViewHolder(CountryPickerLetterViewHolder holder, int position) { + holder.setData(String.valueOf(getCellChar(position))); + } + + private char getCellChar(int position) { + TimeZoneData item = getItem(position); + if (item == null || TextUtils.isEmpty(item.getCity())) { + return 0; + } else { + return item.getCity().charAt(0); + } + } + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filtered = new ArrayList<>(); + for (int i = 0; i < data.size(); i++) { + if (containsIgnoreCase(data.get(i).getText(), constraint.toString())) { + filtered.add(data.get(i)); + } + } + FilterResults results = new FilterResults(); + results.values = filtered; + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + list = (List) results.values; + TimeZoneAdapter.this.notifyDataSetChanged(); + } + }; + } + + public TimeZoneData getItem(int position) { + return list.get(position); + } + + @Override public String getSectionText(int position) { + return null; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + private final TextView textView; + private final TextView offset; + + ViewHolder(View v) { + super(v); + textView = v.findViewById(R.id.title); + offset = v.findViewById(R.id.offset); + } + + TextView getTextView() { + return textView; + } + + public TextView getOffset() { + return offset; + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + View v = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.test_recycler_view_row, viewGroup, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, final int position) { + viewHolder.getTextView().setText(list.get(position).getCity()); + viewHolder.getOffset().setText(list.get(position).getText()); + } + + @Override + public int getItemCount() { + return list.size(); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/viewholders/MyProfileScheduledMessageVH.java b/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/viewholders/MyProfileScheduledMessageVH.java index 6bd5d8d5a54ce37e142b046102a84c95b5a77e1f..63b3444ac2a0f189f39b5d71f673afca04780df2 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/viewholders/MyProfileScheduledMessageVH.java +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/adapters/viewholders/MyProfileScheduledMessageVH.java @@ -36,7 +36,7 @@ public class MyProfileScheduledMessageVH extends BaseMyProfileVH { @Override public void setData(VHModel item) { itemView.setOnClickListener(v -> { if (mListener != null) { - mListener.onItemClick(item, -1); + mListener.onItemClick(item, getAdapterPosition()); } }); } diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ChatFragment.java b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ChatFragment.java index aa5b98ccafa342b2ebb17ffd0416c8dd423929b8..58992e7ca57ab368a9b21e3b2976890be29bf71a 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ChatFragment.java +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ChatFragment.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; +import android.graphics.Point; import android.location.Location; import android.location.LocationManager; import android.net.Uri; @@ -17,6 +18,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; +import android.support.v4.view.GestureDetectorCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -25,6 +27,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.PopupWindow; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; @@ -63,6 +66,7 @@ import com.nynja.mobile.communicator.ui.views.wheel.WheelItemFactory; import com.nynja.mobile.communicator.utils.FileUtils; import com.nynja.mobile.communicator.utils.ImageUtils; import com.nynja.mobile.communicator.utils.LocationUtils; +import com.nynja.mobile.communicator.utils.OnSwipeListener; import com.nynja.mobile.communicator.utils.StringUtils; import com.nynja.mobile.communicator.utils.Utils; import com.vanniktech.emoji.EmojiPopup; @@ -79,6 +83,8 @@ import butterknife.OnTouch; import io.reactivex.disposables.Disposable; import timber.log.Timber; +import static android.content.Context.LAYOUT_INFLATER_SERVICE; + /** * Created by dmitro.boiko on 27/07/2017. **/ @@ -86,7 +92,7 @@ import timber.log.Timber; public class ChatFragment extends BaseFragment implements ChatView, OnChatMessageClickListener, IRecyclerView.OnMoreListener { - private static final String CHAT_TAG = "chat"; + public static final String CHAT_TAG = "chat"; private static final String CONTACTS_TAG = "contacts"; private static final int IMAGE_REQUEST_CODE = 42; @@ -124,10 +130,39 @@ public class ChatFragment extends BaseFragment implements ChatView, private Uri mPhotoURI; private FusedLocationProviderClient mFusedLocationClient; private MicDialog mMicDialog; + private PopupWindow mPopupWindow; + private GestureDetectorCompat mGestureDetector; private boolean mHotFix = false; // FIXME private RecyclerView.OnScrollListener mOnScrollListener; + private OnSwipeListener onSwipeListener = new OnSwipeListener() { + + @Override + public boolean onSwipe(Direction direction) { + if (direction == Direction.up) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.custom_view_more_send_message, null); + mPopupWindow = new PopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + + TextView scheduledMessage = view.findViewById(R.id.schedule_message); + + scheduledMessage.setOnClickListener(v1 -> { + mPresenter.navigateToScheduledMessageScreen(mMessageText.getText().toString()); + mPopupWindow.dismiss(); + }); + + mPopupWindow.showAsDropDown(mRootView, + getLocationOnScreen(mSendButton).x, + getLocationOnScreen(mSendButton).y + mPopupWindow.getHeight() - 400); + return true; + } + return super.onSwipe(direction); + } + }; + public static BaseFragment newInstance(Room chatModel, ArrayList contacts) { Bundle bundle = new Bundle(); bundle.putParcelable(CHAT_TAG, chatModel); @@ -137,20 +172,23 @@ public class ChatFragment extends BaseFragment implements ChatView, return chatFragment; } - @Nullable @Override + @Nullable + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_chat, container, false); } - @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initViews(); mPresenter.getHistory(mRoom); mPresenter.sendContacts(mContacts); } - @Override public void onStart() { + @Override + public void onStart() { super.onStart(); mPresenter.subscribeOnNewMsgs(); mPresenter.subscribeOnOldMsgs(); @@ -158,7 +196,8 @@ public class ChatFragment extends BaseFragment implements ChatView, mPresenter.subscribeOnUserInChat(); } - @Override public void onStop() { + @Override + public void onStop() { mPresenter.stopPlay(); super.onStop(); } @@ -177,7 +216,8 @@ public class ChatFragment extends BaseFragment implements ChatView, mRecyclerView.setOnMoreListener(this); mOnScrollListener = new RecyclerView.OnScrollListener() { - @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE && (mRoom != null && mRoom.unread > 0) && @@ -198,10 +238,12 @@ public class ChatFragment extends BaseFragment implements ChatView, .setOnEmojiPopupShownListener(() -> mSmilesButton.setImageResource(R.drawable.text_bar)) .setOnEmojiPopupDismissListener(() -> mSmilesButton.setImageResource(R.drawable.ic_smile_white)) .build(mMessageText); + mGestureDetector = new GestureDetectorCompat(getContext(), onSwipeListener); } private void initFooter() { keyboardOn.setOnClickListener(v -> mPresenter.showKeyboard()); + mSendButton.setOnTouchListener((v, event) -> mGestureDetector.onTouchEvent(event)); } private void initHeader() { @@ -227,35 +269,48 @@ public class ChatFragment extends BaseFragment implements ChatView, .into(headerPhoto); } - @Override protected BasePresenter getPresenter() { + private static Point getLocationOnScreen(View view) { + int[] location = new int[2]; + view.getLocationOnScreen(location); + return new Point(location[0], location[1]); + } + + @Override + protected BasePresenter getPresenter() { return mPresenter; } - @Override public void onResume() { + @Override + public void onResume() { super.onResume(); mDisposable = RxTextView.textChanges(mMessageText) .subscribe(charSequence -> mSendButton.setEnabled(charSequence.length() > 0)); } - @OnClick(R.id.f_chat_send_button) public void sendMessage() { + @OnClick(R.id.f_chat_send_button) + public void sendMessage() { mPresenter.sendMessage(StringUtils.getText(mMessageText)); mMessageText.setText(null); } - @OnClick(R.id.f_chat_header) public void clickOnHeader() { + @OnClick(R.id.f_chat_header) + public void clickOnHeader() { mPresenter.onHeaderClick(); } - @OnClick(R.id.f_chat_smile) void emojiClick() { + @OnClick(R.id.f_chat_smile) + void emojiClick() { mPresenter.toggleEmoji(); } - @Override public void toggleEmojiUI() { + @Override + public void toggleEmojiUI() { mEmojiPopup.toggle(); } //UI with keyboard and audio icons - @Override public void onShowSwitcherUI() { + @Override + public void onShowSwitcherUI() { mMessageText.setOnBackPressedListener(null); Utils.hideKeyboard(getActivity()); keyboardLayout.setVisibility(View.GONE); @@ -264,14 +319,16 @@ public class ChatFragment extends BaseFragment implements ChatView, } //UI with keyboard and chat msg - @Override public void onShowKeyboard() { + @Override + public void onShowKeyboard() { keyboardOrAudio.setVisibility(View.GONE); keyboardLayout.setVisibility(View.VISIBLE); mMessageText.requestFocus(); Utils.showKeyboard(getActivity(), mMessageText); } - @Override public void navigateToVideo() { + @Override + public void navigateToVideo() { mRxPermissions.request(Manifest.permission.CAMERA) .filter(granted -> granted) .subscribe(granted -> { @@ -298,18 +355,22 @@ public class ChatFragment extends BaseFragment implements ChatView, }); } - @Override public void navigateToVideoPlay(Message message) { + @Override + public void navigateToVideoPlay(Message message) { startActivity(VideoPlayActivity.getLaunchIntent(getContext(), message)); } - @Override public void navigateToAudioPlay(Message message) { + @Override + public void navigateToAudioPlay(Message message) { } - @Override public void navigateToPhotoView(Message message) { + @Override + public void navigateToPhotoView(Message message) { startActivity(ImageViewerActivity.getLaunchIntent(getContext(), message)); } - @Override public void setMessages(List messages) { + @Override + public void setMessages(List messages) { mAdapter.setItems(messages); if (mRoom.unread > 0) { mRecyclerView.removeOnScrollListener(null); @@ -318,7 +379,8 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void newMessage(Message message) { + @Override + public void newMessage(Message message) { mAdapter.addItem(message); if (!mRecyclerView.canScrollVertically(1)) { mRecyclerView.removeOnScrollListener(mOnScrollListener); @@ -327,7 +389,8 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void navigateToGallery() { + @Override + public void navigateToGallery() { mRxPermissions.request(Manifest.permission.READ_EXTERNAL_STORAGE) .filter(granted -> granted) .subscribe(granted -> { @@ -338,7 +401,8 @@ public class ChatFragment extends BaseFragment implements ChatView, }); } - @Override public void navigateToCamera() { + @Override + public void navigateToCamera() { mRxPermissions.request(Manifest.permission.CAMERA) .filter(granted -> granted) .subscribe(granted -> { @@ -363,7 +427,8 @@ public class ChatFragment extends BaseFragment implements ChatView, }); } - @Override public void navigateToVideoCall() { + @Override + public void navigateToVideoCall() { mRxPermissions.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) .subscribe(isGrantedVideo -> { if (isGrantedVideo) { @@ -374,14 +439,16 @@ public class ChatFragment extends BaseFragment implements ChatView, }); } - @Override public void navigateToAudioCall() { + @Override + public void navigateToAudioCall() { mRxPermissions.request(Manifest.permission.RECORD_AUDIO) .subscribe(isGranted -> { if (isGranted) mPresenter.startAudioCall(); }); } - @Override public void changeUserState(List list) { + @Override + public void changeUserState(List list) { Contact contact = list.get(0); if (contact != null && mRoom.id.equalsIgnoreCase(contact.phoneId)) { //// FIXME: 29.08.17 @@ -404,7 +471,8 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void changeGroupState(List list) { + @Override + public void changeGroupState(List list) { setChatStatus(mRoom.getRoomStatus(list, getContext())); } @@ -414,10 +482,12 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void onItemClick(Message item, int position) { + @Override + public void onItemClick(Message item, int position) { } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { if (requestCode == IMAGE_REQUEST_CODE) { sendImage(); @@ -503,7 +573,8 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @OnTouch(R.id.chat_audio) boolean onMicTouch(MotionEvent event) { + @OnTouch(R.id.chat_audio) + boolean onMicTouch(MotionEvent event) { if (!mRxPermissions.isGranted(Manifest.permission.RECORD_AUDIO)) { mRxPermissions.request(Manifest.permission.RECORD_AUDIO).subscribe(); return false; @@ -523,30 +594,35 @@ public class ChatFragment extends BaseFragment implements ChatView, return false; } - @Override public void showMic() { + @Override + public void showMic() { mMicDialog = MicDialog.newInstance(); mMicDialog.show(getFragmentManager(), MicDialog.class.getSimpleName()); } - @Override public void updateMic(float volume, long milliseconds) { + @Override + public void updateMic(float volume, long milliseconds) { mMicDialog.setVolumeLevel(volume); mMicDialog.setTime(milliseconds); } - @Override public void hideMic() { + @Override + public void hideMic() { if (mMicDialog != null) { mMicDialog.dismiss(); mMicDialog = null; } } - @Override public void showAudiogramView() { + @Override + public void showAudiogramView() { keyboardOrAudio.setVisibility(View.GONE); mFooterAudiogramView.setVisibility(View.VISIBLE); } - @Override public void updateWaveProgress(int duration, int currentPosition, int position) { + @Override + public void updateWaveProgress(int duration, int currentPosition, int position) { if (position == -1) { mWaveFormView.setProgress(currentPosition / (float) duration); } else { @@ -560,23 +636,28 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void setAudioData(double[] data) { + @Override + public void setAudioData(double[] data) { mWaveFormView.setData(data); } - @OnClick(R.id.f_chat_img_delete) void onDeleteRecord() { + @OnClick(R.id.f_chat_img_delete) + void onDeleteRecord() { mPresenter.deleteRecord(); } - @OnClick(R.id.f_chat_img_play) void onPlayRecord() { + @OnClick(R.id.f_chat_img_play) + void onPlayRecord() { mPresenter.playRecordClick(); } - @OnClick(R.id.f_chat_send_audio) void onSendAudioClick() { + @OnClick(R.id.f_chat_send_audio) + void onSendAudioClick() { mPresenter.sendAudio(); } - @Override public void recordIsPlaying(boolean isPlaying, int position) { + @Override + public void recordIsPlaying(boolean isPlaying, int position) { if (position == -1) { mPlayRecordImage.setImageResource(isPlaying ? R.drawable.v_pause : R.drawable.v_play); } else { @@ -595,7 +676,8 @@ public class ChatFragment extends BaseFragment implements ChatView, mAdapter.setUnreadMessages(count); } - @Override public void navigateToLocation() { + @Override + public void navigateToLocation() { mRxPermissions.request(Manifest.permission.ACCESS_COARSE_LOCATION) .filter(granted -> granted) .subscribe(isGranted -> { @@ -603,7 +685,8 @@ public class ChatFragment extends BaseFragment implements ChatView, }); } - @Override public void navigateToFileExplorer() { + @Override + public void navigateToFileExplorer() { mRxPermissions.request(Manifest.permission.READ_EXTERNAL_STORAGE) .filter(granted -> granted) .subscribe(isGranted -> openFileExplorer()); @@ -620,17 +703,20 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void showInternetError() { + @Override + public void showInternetError() { Toast.makeText(getContext(), R.string.error_no_internet_connection, Toast.LENGTH_SHORT).show(); } - @Override public void onPause() { + @Override + public void onPause() { super.onPause(); if (mDisposable != null && !mDisposable.isDisposed()) mDisposable.dispose(); } - @Override public void refreshAdapter(Message message) { + @Override + public void refreshAdapter(Message message) { getActivity().runOnUiThread(() -> { int p = mAdapter.getItems().indexOf(message); if (p == -1) p = mAdapter.getItemCount() - 1; @@ -638,23 +724,28 @@ public class ChatFragment extends BaseFragment implements ChatView, }); } - @Override public void onVideoClick(Message message) { + @Override + public void onVideoClick(Message message) { mPresenter.onVideoClick(message); } - @Override public void onAudioClick(String message, int position) { + @Override + public void onAudioClick(String message, int position) { mPresenter.onAudioClick(message, position); } - @Override public void onLocationClick(Message message) { + @Override + public void onLocationClick(Message message) { mPresenter.onLocationClick(message); } - @Override public void onOpenLocation(Message message) { + @Override + public void onOpenLocation(Message message) { LocationUtils.lauchMaps(getActivity(), message); } - @Override public void onPhotoClick(Message message) { + @Override + public void onPhotoClick(Message message) { mPresenter.onPhotoClick(message); } @@ -685,12 +776,14 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void onKeyboardClose() { + @Override + public void onKeyboardClose() { super.onKeyboardClose(); mPresenter.showSwitcherUI(); } - @Override protected List getWheelData(boolean isCallActive) { + @Override + protected List getWheelData(boolean isCallActive) { if (mRoom.id.equalsIgnoreCase(mPresenter.getOwnRosterId())) { return WheelItemFactory.getFirstWheelItemsMyChat(getResources(), isCallActive, mRoom.isGroupChat()); } else { @@ -698,20 +791,24 @@ public class ChatFragment extends BaseFragment implements ChatView, } } - @Override public void addOldMsgs(List message) { + @Override + public void addOldMsgs(List message) { mAdapter.addItems(0, message); } - @Override public void hasMoreItems(boolean hasMoreItems) { + @Override + public void hasMoreItems(boolean hasMoreItems) { mRecyclerView.setHasMoreItems(hasMoreItems); } - @Override public void clearNotificationManager(int notificationId) { + @Override + public void clearNotificationManager(int notificationId) { NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager != null) notificationManager.cancel(notificationId); } - @Override public void setUnreadMsgCount(@NonNull Room room) { + @Override + public void setUnreadMsgCount(@NonNull Room room) { updateUnreadMessages(room.unread); } diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ScheduledMessagesFragment.java b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ScheduledMessagesFragment.java index 26dd00a2384db6c367d0c99c875972803738f6bd..4de0f0c0f2810687d21995e741f0637f5b003f36 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ScheduledMessagesFragment.java +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/ScheduledMessagesFragment.java @@ -1,27 +1,185 @@ package com.nynja.mobile.communicator.ui.fragments.chats; +import android.annotation.SuppressLint; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.arellomobile.mvp.presenter.InjectPresenter; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.nynja.mobile.communicator.R; +import com.nynja.mobile.communicator.data.models.mqtt.BaseMQQT; +import com.nynja.mobile.communicator.data.models.mqtt.Contact; +import com.nynja.mobile.communicator.data.models.mqtt.DecodeFactory; +import com.nynja.mobile.communicator.data.models.mqtt.Desc; +import com.nynja.mobile.communicator.data.models.mqtt.Message; +import com.nynja.mobile.communicator.mvp.presenters.ScheduledMessagesPresenter; +import com.nynja.mobile.communicator.mvp.view.ScheduledMessagesView; import com.nynja.mobile.communicator.ui.base.BaseFragment; +import com.nynja.mobile.communicator.ui.views.calendar.CalendarListener; +import com.nynja.mobile.communicator.ui.views.calendar.CustomCalendarView; +import com.nynja.mobile.communicator.ui.views.segmented.SegmentedButtonGroup; +import com.nynja.mobile.communicator.ui.views.timepicker.SingleDateAndTimePicker; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import butterknife.BindView; +import butterknife.OnClick; + +import static com.nynja.mobile.communicator.ui.fragments.chats.ChatFragment.CHAT_TAG; /** * Created by Stas Safyanov on 26.10.17. */ -public class ScheduledMessagesFragment extends BaseFragment { +public class ScheduledMessagesFragment extends BaseFragment implements ScheduledMessagesView { + + @BindView(R.id.calendar_view) CustomCalendarView calendarView; + @BindView(R.id.date_time_picker) LinearLayout dateTimeTv; + @BindView(R.id.date_tv) TextView dateTv; + @BindView(R.id.time_tv) TextView timeTv; + @BindView(R.id.time_picker) SingleDateAndTimePicker timeView; + @BindView(R.id.pickers_layout) LinearLayout pickersLayout; + @BindView(R.id.segmented_bg) SegmentedButtonGroup segmentedButtonGroup; + @BindView(R.id.username_tv) TextView usernameTv; + @BindView(R.id.avatar_photo) ImageView avatarImg; + @BindView(R.id.message_tv) TextView messageTv; + @BindView(R.id.time_zone_tv) TextView timeZoneTv; + + @InjectPresenter ScheduledMessagesPresenter mPresenter; + + private Message mMessage; + + private SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM dd, YYYY", Locale.getDefault()); + private SimpleDateFormat timeFormat = new SimpleDateFormat("EEEE - hh:mm aa", Locale.getDefault()); + - public static ScheduledMessagesFragment newInstance() { + public static ScheduledMessagesFragment newInstance(Message message) { Bundle args = new Bundle(); + args.putParcelable(CHAT_TAG, message); ScheduledMessagesFragment fragment = new ScheduledMessagesFragment(); fragment.setArguments(args); return fragment; } - @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return super.onCreateView(inflater, container, savedInstanceState); + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_scheduled_messages, container, false); + } + + @OnClick(R.id.date_time_picker) + public void onDateTimeClicked() { + if (pickersLayout.getVisibility() == View.GONE) { + pickersLayout.setVisibility(View.VISIBLE); + calendarView.setVisibility(View.VISIBLE); + segmentedButtonGroup.setPosition(0); + timeView.setVisibility(View.GONE); + } else { + pickersLayout.setVisibility(View.GONE); + } + } + + @OnClick(R.id.time_zone_layout) + public void onTimeZoneLayoutClicked() { + mPresenter.navigateToTimeZoneSelectorFragment(mMessage); + } + + @OnClick(R.id.keep_time_tv) + public void onKeepDefaultTimeClicked() { + mPresenter.keepDefaultTime(mMessage, getContext()); + } + + @SuppressLint("DefaultLocale") + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Bundle arguments = getArguments(); + if (arguments != null) { + mMessage = getArguments().getParcelable(CHAT_TAG); + if (mMessage != null) { + mPresenter.setMessage(mMessage); + } + } + + initCalendar(); + } + + private void initCalendar() { + Calendar currentCalendar = Calendar.getInstance(Locale.getDefault()); + calendarView.markDayAsSelectedDay(currentCalendar.getTime()); + timeView.selectDate(currentCalendar); + dateTv.setText(dateFormat.format(currentCalendar.getTime())); + timeTv.setText(timeFormat.format(currentCalendar.getTime())); + + calendarView.setCalendarListener(new CalendarListener() { + @Override + public void onDateSelected(Date date) { + dateTv.setText(dateFormat.format(date)); + timeTv.setText(timeFormat.format(date)); + } + + @Override + public void onMonthChanged(Date time) { + + } + }); + } + + private void initViews(@NonNull Message message) { + for (Desc desc : message.files) { + switch (desc.mime) { + case BaseMQQT.MimeTypes.Text: + messageTv.setText(desc.payload); + break; + case BaseMQQT.MimeTypes.Contact: + ArrayList contacts = DecodeFactory.decodeContacts(desc.payload); + if (contacts != null && contacts.size() > 0) { + usernameTv.setText(contacts.get(0).getFullName()); + Glide.with(getContext()) + .load(contacts.get(0).avatar) + .apply(new RequestOptions() + .placeholder(R.drawable.avatar_placeholder) + .circleCrop()) + .into(avatarImg); + } + break; + case BaseMQQT.MimeTypes.TimeZone: + mPresenter.setTimeFromMessage(message, getContext()); + break; + } + } + + segmentedButtonGroup.setOnClickedButtonListener(position -> { + if (position == 0) { + calendarView.setVisibility(View.VISIBLE); + timeView.setVisibility(View.GONE); + } else { + calendarView.setVisibility(View.GONE); + timeView.setVisibility(View.VISIBLE); + } + }); + + timeView.setListener((displayed, date) -> timeTv.setText(timeFormat.format(date))); + } + + @Override public void setMessage(@NonNull Message message) { + mMessage = message; + initViews(message); + } + + @Override public void setTime(String timezone) { + timeZoneTv.setText(timezone); } } diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/TimeZoneSelectorFragment.java b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/TimeZoneSelectorFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..7fa29ae7a0b61a1b17faa9415f484baf1b2d351a --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/chats/TimeZoneSelectorFragment.java @@ -0,0 +1,110 @@ +package com.nynja.mobile.communicator.ui.fragments.chats; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import com.arellomobile.mvp.presenter.InjectPresenter; +import com.nynja.mobile.communicator.R; +import com.nynja.mobile.communicator.data.models.mqtt.Message; +import com.nynja.mobile.communicator.mvp.presenters.TimeZoneSelectorPresenter; +import com.nynja.mobile.communicator.mvp.view.TimeZoneSelectorView; +import com.nynja.mobile.communicator.ui.adapters.TimeZoneAdapter; +import com.nynja.mobile.communicator.ui.base.BaseFragment; +import com.nynja.mobile.communicator.utils.TimeZoneData; +import com.nynja.mobile.communicator.utils.ItemClickSupport; +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersDecoration; + +import butterknife.BindView; +import butterknife.OnClick; + +import static com.nynja.mobile.communicator.ui.fragments.chats.ChatFragment.CHAT_TAG; + + +public class TimeZoneSelectorFragment extends BaseFragment implements TimeZoneSelectorView { + + @BindView(R.id.recyclerView) RecyclerView recyclerView; + @BindView(R.id.search_edt) EditText searchEdt; + + @InjectPresenter TimeZoneSelectorPresenter mPresenter; + + private TimeZoneAdapter adapter; + private Message mMessage; + + public static TimeZoneSelectorFragment newInstance(Message message) { + Bundle args = new Bundle(); + args.putParcelable(CHAT_TAG, message); + TimeZoneSelectorFragment fragment = new TimeZoneSelectorFragment(); + fragment.setArguments(args); + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_time_zone_selector, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Bundle arguments = getArguments(); + if (arguments != null) { + mMessage = getArguments().getParcelable(CHAT_TAG); + mPresenter.setMessage(mMessage); + } + setRecyclerView(recyclerView); + setSearchView(); + } + + @OnClick(R.id.close_btn) + public void onCloseClicked() { + mPresenter.closeFragment(mMessage); + } + + private void setSearchView() { + searchEdt.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + adapter.getFilter().filter(s); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + } + + public void setRecyclerView(RecyclerView recyclerView) { + adapter = new TimeZoneAdapter(getContext()); + recyclerView.setAdapter(adapter); + + int scrollPosition = 0; + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + recyclerView.scrollToPosition(scrollPosition); + recyclerView.addItemDecoration(new StickyRecyclerHeadersDecoration(adapter)); + + ItemClickSupport.addTo(recyclerView).setOnItemClickListener((recyclerView1, position, v) -> { + TimeZoneData timeZoneData = adapter.getItem(position); + mPresenter.onTimeZoneClicked(mMessage, timeZoneData); + }); + } + + @Override public void setMessage(@NonNull Message message) { + mMessage = message; + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/profile/MyProfileFragment.java b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/profile/MyProfileFragment.java index ed55b782096e8d5e780eb2724fe814f0647849e1..72b6dcc59333d34a421cc95d86dad3a2df1c40d0 100644 --- a/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/profile/MyProfileFragment.java +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/fragments/profile/MyProfileFragment.java @@ -16,6 +16,7 @@ import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import com.arellomobile.mvp.presenter.InjectPresenter; import com.nynja.mobile.communicator.BuildConfig; @@ -47,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import butterknife.BindView; +import butterknife.OnClick; /** * Created by dmitro.boiko on 27/06/2017. diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/ICalendarView.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/ICalendarView.java new file mode 100644 index 0000000000000000000000000000000000000000..e13dcb2ca5c27db0be0c3e1b4d8e5bfba14ec840 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/ICalendarView.java @@ -0,0 +1,36 @@ +package com.nynja.mobile.communicator.ui.views; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.widget.CalendarView; +import android.widget.LinearLayout; + +/** + * Created by pavel on 11/14/17. + */ + +public class ICalendarView extends LinearLayout { + + public ICalendarView(@NonNull Context context) { + super(context); + } + + public ICalendarView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ICalendarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public ICalendarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CalendarListener.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CalendarListener.java new file mode 100644 index 0000000000000000000000000000000000000000..b39fc37e1c5184e750d224b4b1056ef0428e0351 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CalendarListener.java @@ -0,0 +1,10 @@ +package com.nynja.mobile.communicator.ui.views.calendar; + + +import java.util.Date; + +public interface CalendarListener { + void onDateSelected(Date date); + + void onMonthChanged(Date time); +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CalendarUtils.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CalendarUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8ca71340e335ee16b20a9a7f21b6c4194c189314 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CalendarUtils.java @@ -0,0 +1,51 @@ +package com.nynja.mobile.communicator.ui.views.calendar; + +import java.util.Calendar; +import java.util.Date; + + +public class CalendarUtils { + + public static boolean isSameMonth(Calendar c1, Calendar c2) { + if (c1 == null || c2 == null) + return false; + return (c1.get(Calendar.ERA) == c2.get(Calendar.ERA) + && c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) + && c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH)); + } + + public static boolean isToday(Calendar calendar) { + return isSameDay(calendar, Calendar.getInstance()); + } + + public static boolean isSameDay(Calendar cal1, Calendar cal2) { + if (cal1 == null || cal2 == null) + throw new IllegalArgumentException("The dates must not be null"); + return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) && + cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)); + } + + public static int getTotalWeeks(Calendar calendar) { + if (null == calendar) return 0; + int maxWeeks = calendar.getActualMaximum(Calendar.WEEK_OF_MONTH); + return maxWeeks; + + } + + public static int getTotalWeeks(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return getTotalWeeks(cal); + } + + public static boolean isPastDay(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return date.before(calendar.getTime()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/ColorCircleDrawable.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/ColorCircleDrawable.java new file mode 100644 index 0000000000000000000000000000000000000000..edc085f78467197ad66a00cd1691476369eaeb65 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/ColorCircleDrawable.java @@ -0,0 +1,45 @@ +package com.nynja.mobile.communicator.ui.views.calendar; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +public class ColorCircleDrawable extends Drawable { + private final Paint mPaint; + private int mRadius = 0; + + public ColorCircleDrawable(final int color) { + this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.mPaint.setColor(color); + } + + @Override + public void draw(final Canvas canvas) { + final Rect bounds = getBounds(); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), mRadius, mPaint); + } + + @Override + protected void onBoundsChange(final Rect bounds) { + super.onBoundsChange(bounds); + mRadius = Math.min(bounds.width(), bounds.height()) / 2; + } + + @Override + public void setAlpha(final int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(final ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CustomCalendarView.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CustomCalendarView.java new file mode 100644 index 0000000000000000000000000000000000000000..8e8661041e4bd17a604afa6fe346814211876157 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/CustomCalendarView.java @@ -0,0 +1,355 @@ +package com.nynja.mobile.communicator.ui.views.calendar; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.nynja.mobile.communicator.R; + +import java.text.DateFormatSymbols; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class CustomCalendarView extends LinearLayout { + private Context mContext; + private View view; + private CalendarListener calendarListener; + private Calendar currentCalendar; + private Locale locale; + private Date lastSelectedDay; + private Typeface customTypeface; + + private int firstDayOfWeek = Calendar.MONDAY; + private List decorators = null; + + private static final String DAY_OF_WEEK = "dayOfWeek"; + private static final String DAY_OF_MONTH_TEXT = "dayOfMonthText"; + private static final String DAY_OF_MONTH_CONTAINER = "dayOfMonthContainer"; + + private int disabledDayBackgroundColor; + private int disabledDayTextColor; + private int calendarBackgroundColor; + private int selectedDayBackground; + private int weekLayoutBackgroundColor; + private int calendarTitleBackgroundColor; + private int selectedDayTextColor; + private int calendarTitleTextColor; + private int dayOfWeekTextColor; + private int dayOfMonthTextColor; + private int currentDayOfMonth; + + private boolean isOverflowDateVisible = true; + + public CustomCalendarView(Context mContext) { + this(mContext, null); + } + + public CustomCalendarView(Context mContext, AttributeSet attrs) { + super(mContext, attrs); + this.mContext = mContext; + + if (isInEditMode()) + return; + + getAttributes(attrs); + + initializeCalendar(); + } + + private void getAttributes(AttributeSet attrs) { + final TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.CustomCalendarView, 0, 0); + calendarTitleTextColor = typedArray.getColor(R.styleable.CustomCalendarView_calendarTitleTextColor, getResources().getColor(R.color.white)); + dayOfWeekTextColor = typedArray.getColor(R.styleable.CustomCalendarView_dayOfWeekTextColor, getResources().getColor(R.color.white)); + dayOfMonthTextColor = typedArray.getColor(R.styleable.CustomCalendarView_dayOfMonthTextColor, getResources().getColor(R.color.white)); + disabledDayTextColor = typedArray.getColor(R.styleable.CustomCalendarView_disabledDayTextColor, getResources().getColor(R.color.colorPrimary)); + selectedDayBackground = typedArray.getColor(R.styleable.CustomCalendarView_selectedDayBackgroundColor, getResources().getColor(R.color.colorAccent)); + selectedDayTextColor = typedArray.getColor(R.styleable.CustomCalendarView_selectedDayTextColor, getResources().getColor(R.color.white)); + currentDayOfMonth = typedArray.getColor(R.styleable.CustomCalendarView_currentDayOfMonthColor, getResources().getColor(R.color.white)); + typedArray.recycle(); + } + + private void initializeCalendar() { + final LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflate.inflate(R.layout.custom_calendar_layout, this, true); + + // Initialize calendar for current month + Locale locale = mContext.getResources().getConfiguration().locale; + Calendar currentCalendar = Calendar.getInstance(locale); + + setFirstDayOfWeek(Calendar.MONDAY); + markDayAsSelectedDay(currentCalendar.getTime()); + refreshCalendar(currentCalendar); + } + + @SuppressLint("DefaultLocale") + private void initializeWeekLayout() { + TextView dayOfWeek; + String dayOfTheWeekString; + + View titleLayout = view.findViewById(R.id.weekLayout); + titleLayout.setBackgroundColor(weekLayoutBackgroundColor); + + final String[] weekDaysArray = new DateFormatSymbols(locale).getShortWeekdays(); + for (int i = 1; i < weekDaysArray.length; i++) { + dayOfTheWeekString = weekDaysArray[i]; + if (dayOfTheWeekString.length() > 2) { + dayOfTheWeekString = dayOfTheWeekString.substring(0, 2); + } + + dayOfWeek = view.findViewWithTag(DAY_OF_WEEK + getWeekIndex(i, currentCalendar)); + dayOfWeek.setText(dayOfTheWeekString); + if (i == 1) { + dayOfWeek.setTextColor(ContextCompat.getColor(getContext(), R.color.colorAccent)); + } else { + dayOfWeek.setTextColor(dayOfWeekTextColor); + } + + if (null != getCustomTypeface()) { + dayOfWeek.setTypeface(getCustomTypeface()); + } + } + } + + private void setDaysInCalendar() { + Calendar calendar = Calendar.getInstance(locale); + calendar.setTime(currentCalendar.getTime()); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.setFirstDayOfWeek(getFirstDayOfWeek()); + int firstDayOfMonth = calendar.get(Calendar.DAY_OF_WEEK); + + int dayOfMonthIndex = getWeekIndex(firstDayOfMonth, calendar); + int actualMaximum = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); + + final Calendar startCalendar = (Calendar) calendar.clone(); + //Add required number of days + startCalendar.add(Calendar.DATE, -(dayOfMonthIndex - 1)); + int monthEndIndex = 42 - (actualMaximum + dayOfMonthIndex - 1); + + DayView dayView; + ViewGroup dayOfMonthContainer; + for (int i = 1; i < 43; i++) { + dayOfMonthContainer = view.findViewWithTag(DAY_OF_MONTH_CONTAINER + i); + dayView = view.findViewWithTag(DAY_OF_MONTH_TEXT + i); + if (dayView == null) + continue; + + //Apply the default styles + dayOfMonthContainer.setOnClickListener(null); + dayView.bind(startCalendar.getTime(), getDecorators()); + dayView.setVisibility(View.VISIBLE); + + if (null != getCustomTypeface()) { + dayView.setTypeface(getCustomTypeface()); + } + + if (CalendarUtils.isSameMonth(calendar, startCalendar)) { + dayOfMonthContainer.setOnClickListener(onDayOfMonthClickListener); + dayView.setBackgroundColor(calendarBackgroundColor); + dayView.setTextColor(dayOfWeekTextColor); + //Set the current day color + markDayAsCurrentDay(startCalendar); + } else { + dayView.setBackgroundColor(disabledDayBackgroundColor); + dayView.setTextColor(disabledDayTextColor); + + if (!isOverflowDateVisible()) + dayView.setVisibility(View.GONE); + else if (i >= 36 && ((float) monthEndIndex / 7.0f) >= 1) { + dayView.setVisibility(View.GONE); + } + } + dayView.decorate(); + + + startCalendar.add(Calendar.DATE, 1); + dayOfMonthIndex++; + } + + // If the last week row has no visible days, hide it or show it in case + ViewGroup weekRow = view.findViewWithTag("weekRow6"); + dayView = view.findViewWithTag("dayOfMonthText36"); + if (dayView.getVisibility() != VISIBLE) { + weekRow.setVisibility(GONE); + } else { + weekRow.setVisibility(VISIBLE); + } + } + + private void clearDayOfTheMonthStyle(Date currentDate) { + if (currentDate != null) { + final Calendar calendar = getTodaysCalendar(); + calendar.setFirstDayOfWeek(getFirstDayOfWeek()); + calendar.setTime(currentDate); + + final DayView dayView = getDayOfMonthText(calendar); + dayView.setBackgroundColor(calendarBackgroundColor); + dayView.setTextColor(dayOfWeekTextColor); + dayView.decorate(); + } + } + + private DayView getDayOfMonthText(Calendar currentCalendar) { + return (DayView) getView(DAY_OF_MONTH_TEXT, currentCalendar); + } + + private int getDayIndexByDate(Calendar currentCalendar) { + int monthOffset = getMonthOffset(currentCalendar); + int currentDay = currentCalendar.get(Calendar.DAY_OF_MONTH); + return currentDay + monthOffset; + } + + private int getMonthOffset(Calendar currentCalendar) { + final Calendar calendar = Calendar.getInstance(); + calendar.setFirstDayOfWeek(getFirstDayOfWeek()); + calendar.setTime(currentCalendar.getTime()); + calendar.set(Calendar.DAY_OF_MONTH, 1); + + int firstDayWeekPosition = calendar.getFirstDayOfWeek(); + int dayPosition = calendar.get(Calendar.DAY_OF_WEEK); + + if (firstDayWeekPosition == 1) { + return dayPosition - 1; + } else { + if (dayPosition == 1) { + return 6; + } else { + return dayPosition - 2; + } + } + } + + private int getWeekIndex(int weekIndex, Calendar currentCalendar) { + int firstDayWeekPosition = currentCalendar.getFirstDayOfWeek(); + if (firstDayWeekPosition == 1) { + return weekIndex; + } else { + + if (weekIndex == 1) { + return 7; + } else { + return weekIndex - 1; + } + } + } + + private View getView(String key, Calendar currentCalendar) { + int index = getDayIndexByDate(currentCalendar); + return view.findViewWithTag(key + index); + } + + private Calendar getTodaysCalendar() { + Calendar currentCalendar = Calendar.getInstance(mContext.getResources().getConfiguration().locale); + currentCalendar.setFirstDayOfWeek(getFirstDayOfWeek()); + return currentCalendar; + } + + @SuppressLint("DefaultLocale") + public void refreshCalendar(Calendar currentCalendar) { + this.currentCalendar = currentCalendar; + this.currentCalendar.setFirstDayOfWeek(getFirstDayOfWeek()); + locale = mContext.getResources().getConfiguration().locale; + + // Set weeks days titles + initializeWeekLayout(); + + // Initialize and set days in calendar + setDaysInCalendar(); + } + + public int getFirstDayOfWeek() { + return firstDayOfWeek; + } + + public void setFirstDayOfWeek(int firstDayOfWeek) { + this.firstDayOfWeek = firstDayOfWeek; + } + + public void markDayAsCurrentDay(Calendar calendar) { + if (calendar != null && CalendarUtils.isToday(calendar)) { + DayView dayOfMonth = getDayOfMonthText(calendar); + dayOfMonth.setTextColor(currentDayOfMonth); + } + } + + public void markDayAsSelectedDay(Date currentDate) { + final Calendar currentCalendar = getTodaysCalendar(); + currentCalendar.setFirstDayOfWeek(getFirstDayOfWeek()); + currentCalendar.setTime(currentDate); + clearDayOfTheMonthStyle(lastSelectedDay); + storeLastValues(currentDate); + DayView view = getDayOfMonthText(currentCalendar); + view.setBackground(new ColorCircleDrawable(ContextCompat.getColor(getContext(), + R.color.colorAccent))); + view.setTextColor(selectedDayTextColor); + } + + private void storeLastValues(Date currentDate) { + lastSelectedDay = currentDate; + } + + public void setCalendarListener(CalendarListener calendarListener) { + this.calendarListener = calendarListener; + } + + private OnClickListener onDayOfMonthClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + // Extract day selected + ViewGroup dayOfMonthContainer = (ViewGroup) view; + String tagId = (String) dayOfMonthContainer.getTag(); + tagId = tagId.substring(DAY_OF_MONTH_CONTAINER.length(), tagId.length()); + final TextView dayOfMonthText = view.findViewWithTag(DAY_OF_MONTH_TEXT + tagId); + + // Fire event + final Calendar calendar = Calendar.getInstance(); + calendar.setFirstDayOfWeek(getFirstDayOfWeek()); + calendar.setTime(currentCalendar.getTime()); + calendar.set(Calendar.DAY_OF_MONTH, Integer.valueOf(dayOfMonthText.getText().toString())); + markDayAsSelectedDay(calendar.getTime()); + + //Set the current day color + markDayAsCurrentDay(currentCalendar); + + if (calendarListener != null) + calendarListener.onDateSelected(calendar.getTime()); + } + }; + + public List getDecorators() { + return decorators; + } + + public void setDecorators(List decorators) { + this.decorators = decorators; + } + + public boolean isOverflowDateVisible() { + return isOverflowDateVisible; + } + + public void setShowOverflowDate(boolean isOverFlowEnabled) { + isOverflowDateVisible = isOverFlowEnabled; + } + + public void setCustomTypeface(Typeface customTypeface) { + this.customTypeface = customTypeface; + } + + public Typeface getCustomTypeface() { + return customTypeface; + } + + public Calendar getCurrentCalendar() { + return currentCalendar; + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/DayDecorator.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/DayDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..10197772464b3925c6cf46247dcb2b0ee9ec0d0f --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/DayDecorator.java @@ -0,0 +1,6 @@ +package com.nynja.mobile.communicator.ui.views.calendar; + + +public interface DayDecorator { + void decorate(DayView cell); +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/DayView.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/DayView.java new file mode 100644 index 0000000000000000000000000000000000000000..85566dbe354f4b12f13e7736d542836559135ba9 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/calendar/DayView.java @@ -0,0 +1,51 @@ +package com.nynja.mobile.communicator.ui.views.calendar; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class DayView extends TextView { + private Date date; + private List decorators; + + public DayView(Context context) { + this(context, null, 0); + } + + public DayView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DayView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + if (isInEditMode()) + return; + } + + public void bind(Date date, List decorators) { + this.date = date; + this.decorators = decorators; + + @SuppressLint("SimpleDateFormat") + final SimpleDateFormat df = new SimpleDateFormat("d"); + int day = Integer.parseInt(df.format(date)); + setText(String.valueOf(day)); + } + + public void decorate() { + if (null != decorators) { + for (DayDecorator decorator : decorators) { + decorator.decorate(this); + } + } + } + + public Date getDate() { + return date; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/BackgroundHelper.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/BackgroundHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..0c8cafeacaae70bb55261b29c774815f682644ed --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/BackgroundHelper.java @@ -0,0 +1,16 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; + +class BackgroundHelper { + + static void setBackground(View view, Drawable drawable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setBackground(drawable); + } else { + view.setBackgroundDrawable(drawable); + } + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/BackgroundView.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/BackgroundView.java new file mode 100644 index 0000000000000000000000000000000000000000..e42193eaa97b3a009684d834d4e220da051f9b95 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/BackgroundView.java @@ -0,0 +1,32 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +public class BackgroundView extends View { + public BackgroundView(Context context) { + super(context); + } + + public BackgroundView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public BackgroundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int w = 0, h = 0; + + w = Math.max(w, getSuggestedMinimumWidth()); + h = Math.max(h, getSuggestedMinimumHeight()); + + int widthSize = resolveSizeAndState(w, widthMeasureSpec, 0); + int heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); + setMeasuredDimension(widthSize, heightSize); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/ConversionHelper.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/ConversionHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..28d40a2e5c37073f2f8a3738df29e62a3a3f2a82 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/ConversionHelper.java @@ -0,0 +1,18 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.content.Context; + +class ConversionHelper { + + static float pxToDp(final Context context, final float px) { + return px / context.getResources().getDisplayMetrics().density; + } + + static int dpToPx(final Context context, final float dp) { + return (int) (dp * context.getResources().getDisplayMetrics().density); + } + + static float spToPx(final Context context, int size) { + return size * context.getResources().getDisplayMetrics().scaledDensity; + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/RippleHelper.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/RippleHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..a1b5f55290b720636fd7666feee2319fb32a49c6 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/RippleHelper.java @@ -0,0 +1,101 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.StateListDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.os.Build; +import android.view.View; + +import java.util.Arrays; + +class RippleHelper { + + static void setSelectableItemBackground(Context context, View view) { + int[] attrs = new int[]{android.R.attr.selectableItemBackground}; + TypedArray ta = context.obtainStyledAttributes(attrs); + Drawable drawableFromTheme = ta.getDrawable(0 /* index */); + ta.recycle(); + BackgroundHelper.setBackground(view, drawableFromTheme); + } + + static void setRipple(View view, int pressedColor, int radius) { + setRipple(view, pressedColor, null, radius); + } + + static void setRipple(View view, int pressedColor, Integer normalColor, int radius) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setBackground(getRippleDrawable(pressedColor, normalColor, radius)); + } else { + view.setBackgroundDrawable(getStateListDrawable(pressedColor, normalColor, radius)); + } + } + + private static StateListDrawable getStateListDrawable(int pressedColor, Integer normalColor, int radius) { + StateListDrawable states = new StateListDrawable(); + states.addState( + new int[]{android.R.attr.state_pressed} + , getDrawable(pressedColor, radius) + ); + if (null != normalColor) + states.addState( + new int[]{} + , getDrawable(normalColor, radius) + ); + return states; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static Drawable getRippleDrawable(int pressedColor, Integer normalColor, int radius) { + ColorStateList colorStateList = getPressedColorSelector(pressedColor); + Drawable content = null != normalColor ? new ColorDrawable(normalColor) : null; + Drawable mask = getRippleMask(Color.WHITE, radius); + + return new RippleDrawable(colorStateList, content, mask); + } + + private static ColorStateList getPressedColorSelector(int pressedColor) { + return new ColorStateList( + new int[][]{ + new int[]{} + }, + new int[]{ + pressedColor + } + ); + } + + private static Drawable getRippleMask(int color, int radius) { + float[] outerRadii = new float[8]; + Arrays.fill(outerRadii, radius); + RoundRectShape r = new RoundRectShape(outerRadii, null, null); + ShapeDrawable shapeDrawable = new ShapeDrawable(r); + shapeDrawable.getPaint().setColor(color); + return shapeDrawable; + } + + private static Drawable getDrawable(Integer color, int radius) { + if (color == null) + return null; + if (radius == 0) + return new ColorDrawable(color); + + float[] outerRadii = new float[8]; + Arrays.fill(outerRadii, radius); + + GradientDrawable shape = new GradientDrawable(); + shape.setShape(GradientDrawable.RECTANGLE); + shape.setCornerRadii(outerRadii); + shape.setColor(color); + return shape; + } + +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/RoundHelper.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/RoundHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..772b84c64d7b81b321c5d7d9d70f1cd0a4a55955 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/RoundHelper.java @@ -0,0 +1,39 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.view.View; +import android.widget.LinearLayout; + +class RoundHelper { + + static void makeRound(View view, int dividerColor, int dividerRadius, int dividerSize) { + GradientDrawable gradient = getGradientDrawable(dividerColor, dividerRadius, dividerSize); + BackgroundHelper.setBackground(view, gradient); + } + + static void makeDividerRound(LinearLayout layout, int dividerColor, int dividerRadius, int dividerSize, Drawable drawable) { + GradientDrawable gradient = null; + if (null != drawable) { + if (drawable instanceof GradientDrawable) { + gradient = (GradientDrawable) drawable; + gradient.setSize(dividerSize, 0); + gradient.setCornerRadius(dividerRadius); + } else { + layout.setDividerDrawable(drawable); + } + } else { + gradient = getGradientDrawable(dividerColor, dividerRadius, dividerSize); + } + layout.setDividerDrawable(gradient); + } + + private static GradientDrawable getGradientDrawable(int dividerColor, int dividerRadius, int dividerSize) { + GradientDrawable gradient = + new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, new int[]{dividerColor, dividerColor}); + gradient.setShape(GradientDrawable.RECTANGLE); + gradient.setCornerRadius(dividerRadius); + gradient.setSize(dividerSize, 0); + return gradient; + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/SegmentedButton.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/SegmentedButton.java new file mode 100644 index 0000000000000000000000000000000000000000..07e39a6e27215f3cbe409a4f298b4285f34fad57 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/SegmentedButton.java @@ -0,0 +1,673 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.support.v4.content.ContextCompat; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.view.View; + +import com.nynja.mobile.communicator.R; + + +public class SegmentedButton extends View { + + private Context context; + + public SegmentedButton(Context context) { + super(context); + init(context, null); + } + + public SegmentedButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public SegmentedButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private float mClipAmount; + private boolean clipLeftToRight; + + private TextPaint mTextPaint; + private StaticLayout mStaticLayout, mStaticLayoutOverlay; + private Rect mTextBounds = new Rect(); + private int mRadius, mBorderSize; + private boolean hasBorderLeft, hasBorderRight; + + // private RectF rectF = new RectF(); + + private void init(Context context, AttributeSet attrs) { + this.context = context; + getAttributes(attrs); + + initText(); + initBitmap(); + mRectF = new RectF(); + mPaint = new Paint(); + mPaint.setColor(Color.BLACK); + mPaint.setAntiAlias(true); + } + + void setSelectorColor(int color) { + mPaint.setColor(color); + } + + void setSelectorRadius(int radius) { + mRadius = radius; + } + + void setBorderSize(int borderSize) { + mBorderSize = borderSize; + } + + void hasBorderLeft(boolean hasBorderLeft) { + this.hasBorderLeft = hasBorderLeft; + } + + void hasBorderRight(boolean hasBorderRight) { + this.hasBorderRight = hasBorderRight; + } + + private RectF mRectF; + private Paint mPaint; + + @RequiresApi(api = Build.VERSION_CODES.O) + private void initText() { + if (!hasText) + return; + + mTextPaint = new TextPaint(); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextSize(textSize); + mTextPaint.setColor(textColor); + + /*if (hasTextTypefacePath) + setTypeface(textTypefacePath); + else if (null != textTypeface) { + setTypeface(textTypeface); + }*/ + + // default to a single line of text + int width = (int) mTextPaint.measureText(text); + mStaticLayout = new StaticLayout(text, mTextPaint, width, Layout.Alignment.ALIGN_CENTER, 1.0f, 0, false); + mStaticLayoutOverlay = new StaticLayout(text, mTextPaint, width, Layout.Alignment.ALIGN_CENTER, 1.0f, 0, false); + } + + private void initBitmap() { + if (hasDrawable) { + mDrawable = ContextCompat.getDrawable(context, drawable); + } + + if (hasDrawableTint) { + mBitmapNormalColor = new PorterDuffColorFilter(drawableTint, PorterDuff.Mode.SRC_IN); + } + + if (hasDrawableTintOnSelection) + mBitmapClipColor = new PorterDuffColorFilter(drawableTintOnSelection, PorterDuff.Mode.SRC_IN); + } + + private void measureTextWidth(int width) { + if (!hasText) + return; + + int bitmapWidth = hasDrawable && drawableGravity.isHorizontal() ? mDrawable.getIntrinsicWidth() : 0; + + int textWidth = width - (bitmapWidth + getPaddingLeft() + getPaddingRight()); + + if (textWidth < 0) + return; + + mStaticLayout = new StaticLayout(text, mTextPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false); + mStaticLayoutOverlay = new StaticLayout(text, mTextPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthRequirement = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightRequirement = MeasureSpec.getSize(heightMeasureSpec); + + int width = 0; + int bitmapWidth = hasDrawable ? mDrawable.getIntrinsicWidth() : 0; + int textWidth = hasText ? mStaticLayout.getWidth() : 0; + + int height = getPaddingTop() + getPaddingBottom(); + int bitmapHeight = hasDrawable ? mDrawable.getIntrinsicHeight() : 0; + int textHeight = hasText ? mStaticLayout.getHeight() : 0; + + switch (widthMode) { + case MeasureSpec.EXACTLY: + if (width < widthRequirement) { + width = widthRequirement; + measureTextWidth(width); + } + break; + case MeasureSpec.AT_MOST: + + if (drawableGravity.isHorizontal()) { + width = textWidth + bitmapWidth + drawablePadding; + } else { + width = Math.max(bitmapWidth, textWidth); + } + width += getPaddingLeft() * 2 + getPaddingRight() * 2; + + /* + if (width > widthRequirement) { + width = widthRequirement; + measureTextWidth(width); + }*/ + break; + case MeasureSpec.UNSPECIFIED: + width = textWidth + bitmapWidth; + break; + } + + if (hasText) + mTextPaint.getTextBounds(text, 0, text.length(), mTextBounds); + + + switch (heightMode) { + case MeasureSpec.EXACTLY: + + if (drawableGravity.isHorizontal()) { + height = heightRequirement; + int h = Math.max(textHeight, bitmapHeight) + getPaddingTop() + getPaddingBottom(); + if (heightRequirement < h) { + height = h; + } + } else { + int h = textHeight + bitmapHeight + getPaddingTop() + getPaddingBottom(); + if (heightRequirement < h) + height = h; + else + height = heightRequirement + getPaddingTop() - getPaddingBottom(); + } + break; + + case MeasureSpec.AT_MOST: + int vHeight; + if (drawableGravity.isHorizontal()) { + vHeight = Math.max(textHeight, bitmapHeight); + } else { + vHeight = textHeight + bitmapHeight + drawablePadding; + } + + height = vHeight + getPaddingTop() * 2 + getPaddingBottom() * 2; + + break; + case MeasureSpec.UNSPECIFIED: + // height = heightMeasureSpec; + break; + } + + calculate(width, height); + setMeasuredDimension(width, height); + } + + private float text_X = 0.0f, text_Y = 0.0f, bitmap_X = 0.0f, bitmap_Y = 0.0f; + + private void calculate(int width, int height) { + float textHeight = 0, textWidth = 0, textBoundsWidth = 0; + if (hasText) { + textHeight = mStaticLayout.getHeight(); + textWidth = mStaticLayout.getWidth(); + textBoundsWidth = mTextBounds.width(); + } + + float bitmapHeight = 0, bitmapWidth = 0; + if (hasDrawable) { + bitmapHeight = mDrawable.getIntrinsicHeight(); + bitmapWidth = mDrawable.getIntrinsicWidth(); + } + + + if (drawableGravity.isHorizontal()) { + if (height > Math.max(textHeight, bitmapHeight)) { + text_Y = height / 2f - textHeight / 2f + getPaddingTop() - getPaddingBottom(); + bitmap_Y = height / 2f - bitmapHeight / 2f + getPaddingTop() - getPaddingBottom(); + } else if (textHeight > bitmapHeight) { + text_Y = getPaddingTop(); + bitmap_Y = text_Y + textHeight / 2f - bitmapHeight / 2f; + } else { + bitmap_Y = getPaddingTop(); + text_Y = bitmap_Y + bitmapHeight / 2f - textHeight / 2f; + } + + text_X = getPaddingLeft(); + bitmap_X = textWidth; + + float remainingSpace = width - (textBoundsWidth + bitmapWidth); + if (remainingSpace > 0) { + remainingSpace /= 2f; + } + + if (drawableGravity == DrawableGravity.RIGHT) { + text_X = remainingSpace + getPaddingLeft() - getPaddingRight() - drawablePadding / 2f; + bitmap_X = text_X + textBoundsWidth + drawablePadding; + } else if (drawableGravity == DrawableGravity.LEFT) { + bitmap_X = remainingSpace + getPaddingLeft() - getPaddingRight() - drawablePadding / 2f; + text_X = bitmap_X + bitmapWidth + drawablePadding; + } + } else { + + + if (drawableGravity == DrawableGravity.TOP) { + bitmap_Y = getPaddingTop() - getPaddingBottom() - drawablePadding / 2f; + + float vHeight = (height - (textHeight + bitmapHeight)) / 2f; + + if (vHeight > 0) + bitmap_Y += vHeight; + + text_Y = bitmap_Y + bitmapHeight + drawablePadding; + + } else if (drawableGravity == DrawableGravity.BOTTOM) { + text_Y = getPaddingTop() - getPaddingBottom() - drawablePadding / 2f; + + float vHeight = height - (textHeight + bitmapHeight); + if (vHeight > 0) + text_Y += vHeight / 2f; + + bitmap_Y = text_Y + textHeight + drawablePadding; + } + + + if (width > Math.max(textBoundsWidth, bitmapWidth)) { + text_X = width / 2f - textBoundsWidth / 2f + getPaddingLeft() - getPaddingRight(); + bitmap_X = width / 2f - bitmapWidth / 2f + getPaddingLeft() - getPaddingRight(); + } else if (textBoundsWidth > bitmapWidth) { + text_X = getPaddingLeft(); + bitmap_X = text_X + textBoundsWidth / 2f - bitmapWidth / 2f; + } else { + bitmap_X = getPaddingLeft(); + text_X = bitmap_X + bitmapWidth / 2f - textBoundsWidth / 2f; + } + } + } + + private PorterDuffColorFilter mBitmapNormalColor, mBitmapClipColor; + + private Drawable mDrawable; + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int width = canvas.getWidth(); + int height = canvas.getHeight(); + + canvas.save(); + + if (clipLeftToRight) + canvas.translate(-width * (mClipAmount - 1), 0); + else + canvas.translate(width * (mClipAmount - 1), 0); + + + mRectF.set(hasBorderLeft ? mBorderSize : 0, mBorderSize, hasBorderRight ? width - mBorderSize : width, height - mBorderSize); + canvas.drawRoundRect(mRectF, mRadius, mRadius, mPaint); + + canvas.restore(); + + canvas.save(); + + if (hasText) { + canvas.translate(text_X, text_Y); + if (hasTextColorOnSelection) + mTextPaint.setColor(textColor); + mStaticLayout.draw(canvas); + + canvas.restore(); + } + canvas.save(); + + // Bitmap normal + if (hasDrawable) { + drawDrawableWithColorFilter(canvas, mBitmapNormalColor); + } + // NORMAL -end + + + // CLIPPING + if (clipLeftToRight) { + canvas.clipRect(width * (1 - mClipAmount), 0, width, height); + } else { + canvas.clipRect(0, 0, width * mClipAmount, height); + } + + // CLIP -start + // Text clip + canvas.save(); + + if (hasText) { + canvas.translate(text_X, text_Y); + if (hasTextColorOnSelection) + mTextPaint.setColor(textColorOnSelection); + mStaticLayoutOverlay.draw(canvas); + canvas.restore(); + } + + // Bitmap clip + if (hasDrawable) { + drawDrawableWithColorFilter(canvas, mBitmapClipColor); + } + // CLIP -end + + canvas.restore(); + } + + private void drawDrawableWithColorFilter(Canvas canvas, ColorFilter colorFilter){ + int drawableX = (int)bitmap_X; + int drawableY = (int)bitmap_Y; + int drawableWidth = mDrawable.getIntrinsicWidth(); + if (hasDrawableWidth) { + drawableWidth = this.drawableWidth; + } + int drawableHeight = mDrawable.getIntrinsicHeight(); + if (hasDrawableHeight) { + drawableHeight = this.drawableHeight; + } + mDrawable.setColorFilter(colorFilter); + mDrawable.setBounds(drawableX, drawableY, drawableX + drawableWidth, drawableY + drawableHeight); + mDrawable.draw(canvas); + } + + public void clipToLeft(float clip) { + clipLeftToRight = false; + mClipAmount = 1.0f - clip; + invalidate(); + } + + public void clipToRight(float clip) { + clipLeftToRight = true; + mClipAmount = clip; + invalidate(); + } + + private int drawableTintOnSelection, textColorOnSelection, textColor, rippleColor, buttonWidth, + drawable, drawableTint, drawableWidth, drawableHeight, drawablePadding; + private boolean hasTextColorOnSelection, hasRipple, hasWidth, hasWeight, hasDrawableTintOnSelection, + hasDrawableWidth, hasDrawableHeight, hasDrawableTint, hasTextTypefacePath; + private float buttonWeight, textSize; + private String textTypefacePath, text; + private Typeface textTypeface; + + private void getAttributes(AttributeSet attrs) { + TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.SegmentedButton); + + drawableTintOnSelection = ta.getColor(R.styleable.SegmentedButton_sb_drawableTint_onSelection, Color.WHITE); + hasDrawableTintOnSelection = ta.hasValue(R.styleable.SegmentedButton_sb_drawableTint_onSelection); + + textColorOnSelection = ta.getColor(R.styleable.SegmentedButton_sb_textColor_onSelection, Color.WHITE); + hasTextColorOnSelection = ta.hasValue(R.styleable.SegmentedButton_sb_textColor_onSelection); + + rippleColor = ta.getColor(R.styleable.SegmentedButton_sb_rippleColor, 0); + hasRipple = ta.hasValue(R.styleable.SegmentedButton_sb_rippleColor); + + text = ta.getString(R.styleable.SegmentedButton_sb_text); + hasText = ta.hasValue(R.styleable.SegmentedButton_sb_text); + textSize = ta.getDimension(R.styleable.SegmentedButton_sb_textSize, ConversionHelper.spToPx(getContext(), 14)); + textColor = ta.getColor(R.styleable.SegmentedButton_sb_textColor, Color.GRAY); + textTypefacePath = ta.getString(R.styleable.SegmentedButton_sb_textTypefacePath); + hasTextTypefacePath = ta.hasValue(R.styleable.SegmentedButton_sb_textTypefacePath); + int typeface = ta.getInt(R.styleable.SegmentedButton_sb_textTypeface, 1); + switch (typeface) { + case 0: + textTypeface = Typeface.MONOSPACE; + break; + case 1: + textTypeface = Typeface.DEFAULT; + break; + case 2: + textTypeface = Typeface.SANS_SERIF; + break; + case 3: + textTypeface = Typeface.SERIF; + break; + } + + try { + hasWeight = ta.hasValue(R.styleable.SegmentedButton_android_layout_weight); + buttonWeight = ta.getFloat(R.styleable.SegmentedButton_android_layout_weight, 0); + + buttonWidth = ta.getDimensionPixelSize(R.styleable.SegmentedButton_android_layout_width, 0); + + } catch (Exception ex) { + hasWeight = true; + buttonWeight = 1; + } + hasWidth = !hasWeight && buttonWidth > 0; + + + drawable = ta.getResourceId(R.styleable.SegmentedButton_sb_drawable, 0); + drawableTint = ta.getColor(R.styleable.SegmentedButton_sb_drawableTint, -1); + drawableWidth = ta.getDimensionPixelSize(R.styleable.SegmentedButton_sb_drawableWidth, -1); + drawableHeight = ta.getDimensionPixelSize(R.styleable.SegmentedButton_sb_drawableHeight, -1); + drawablePadding = ta.getDimensionPixelSize(R.styleable.SegmentedButton_sb_drawablePadding, 0); + + hasDrawable = ta.hasValue(R.styleable.SegmentedButton_sb_drawable); + hasDrawableTint = ta.hasValue(R.styleable.SegmentedButton_sb_drawableTint); + hasDrawableWidth = ta.hasValue(R.styleable.SegmentedButton_sb_drawableWidth); + hasDrawableHeight = ta.hasValue(R.styleable.SegmentedButton_sb_drawableHeight); + + drawableGravity = DrawableGravity.getById(ta.getInteger(R.styleable.SegmentedButton_sb_drawableGravity, 0)); + + + ta.recycle(); + } + + /** + * Typeface.NORMAL: 0 + * Typeface.BOLD: 1 + * Typeface.ITALIC: 2 + * Typeface.BOLD_ITALIC: 3 + * + * @param typeface you can use above variations using the bitwise OR operator + */ + public void setTypeface(Typeface typeface) { + mTextPaint.setTypeface(typeface); + } + + /** + * @param location is .ttf file's path in assets folder. Example: 'fonts/my_font.ttf' + */ + public void setTypeface(String location) { + if (null != location && !location.equals("")) { + Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), "font/avenir_medium.ttf"); + mTextPaint.setTypeface(typeface); + } + } + + /** + * GRAVITY + */ + + private DrawableGravity drawableGravity; + + public enum DrawableGravity { + LEFT(0), + TOP(1), + RIGHT(2), + BOTTOM(3); + + private int intValue; + + DrawableGravity(int intValue) { + this.intValue = intValue; + } + + private int getIntValue() { + return intValue; + } + + public static DrawableGravity getById(int id) { + for (DrawableGravity e : values()) { + if (e.intValue == id) return e; + } + return null; + } + + public boolean isHorizontal() { + return intValue == 0 || intValue == 2; + } + } + + private boolean hasDrawable, hasText; + + + /** + * Sets button's drawable by given drawable object and its position + * + * @param resId is your drawable's resource id + */ + public void setDrawable(int resId) { + setDrawable(ContextCompat.getDrawable(context, resId)); + } + + /** + * Sets button's drawable by given drawable object and its position + * + * @param drawable is your drawable object + */ + public void setDrawable(Drawable drawable){ + mDrawable = drawable; + hasDrawable = true; + requestLayout(); + } + + /** + * Sets button's drawable by given drawable id and its position + * + * @param gravity specifies button's drawable position relative to text position. + * These values can be given to position: + * {DrawableGravity.LEFT} sets drawable to the left of button's text + * {DrawableGravity.TOP} sets drawable to the top of button's text + * {DrawableGravity.RIGHT} sets drawable to the right of button's text + * {DrawableGravity.BOTTOM} sets drawable to the bottom of button's text + */ + public void setGravity(DrawableGravity gravity) { + drawableGravity = gravity; + } + + /** + * removes drawable's tint + */ + public void removeDrawableTint() { + hasDrawableTint = false; + } + + public void removeDrawableTintOnSelection() { + hasDrawableTintOnSelection = false; + } + + public void removeTextColorOnSelection() { + hasTextColorOnSelection = false; + } + + /** + * If button has any drawable, it sets drawable's tint color without changing drawable's position. + * + * @param color is used to set drawable's tint color + */ + public void setDrawableTint(int color) { + drawableTint = color; + } + + /** + * @return button's current ripple color + */ + public int getRippleColor() { + return rippleColor; + } + + /** + * @return true if the button has a ripple effect + */ + public boolean hasRipple() { + return hasRipple; + } + + /** + * @return button's text color when selector is on the button + */ + public int getTextColorOnSelection() { + return textColorOnSelection; + } + + /** + * @param textColorOnSelection set button's text color when selector is on the button + */ + public void setTextColorOnSelection(int textColorOnSelection) { + this.textColorOnSelection = textColorOnSelection; + } + + /** + * @return drawable's tint color when selector is on the button + */ + public int getDrawableTintOnSelection() { + return drawableTintOnSelection; + } + + /** + * @return drawable's tint color + */ + public int getDrawableTint() { + return drawableTint; + } + + /** + * @return true if button's drawable is not empty + */ + public boolean hasDrawableTint() { + return hasDrawableTint; + } + + /** + * @return true if button's drawable has tint when selector is on the button + */ + public boolean hasDrawableTintOnSelection() { + return hasDrawableTintOnSelection; + } + + /** + * + */ + boolean hasWeight() { + return hasWeight; + } + + float getWeight() { + return buttonWeight; + } + + int getButtonWidth() { + return buttonWidth; + } + + boolean hasWidth() { + return hasWidth; + } + + boolean hasTextColorOnSelection() { + return hasTextColorOnSelection; + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/SegmentedButtonGroup.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/SegmentedButtonGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..5b993b2bfaecbac1d4d29e468cad62073f3e144c --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/segmented/SegmentedButtonGroup.java @@ -0,0 +1,799 @@ +package com.nynja.mobile.communicator.ui.views.segmented; + +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnticipateInterpolator; +import android.view.animation.AnticipateOvershootInterpolator; +import android.view.animation.BounceInterpolator; +import android.view.animation.CycleInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.nynja.mobile.communicator.R; + +import java.util.ArrayList; + +public class SegmentedButtonGroup extends LinearLayout { + + public SegmentedButtonGroup(Context context) { + super(context); + init(null); + } + + public SegmentedButtonGroup(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + public SegmentedButtonGroup(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SegmentedButtonGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private LinearLayout mainGroup, rippleContainer, dividerContainer; + + private boolean draggable = false; + + @Override + public boolean onTouchEvent(MotionEvent event) { + float selectorWidth, offsetX; + int position = 0; + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + + selectorWidth = (float) getWidth() / numberOfButtons / 2f; + offsetX = ((event.getX() - selectorWidth) * numberOfButtons) / getWidth(); + position = (int) Math.floor(offsetX + 0.5); + + toggledPositionOffset = lastPositionOffset = offsetX; + + toggle(position, animateSelectorDuration, true); + + break; + case MotionEvent.ACTION_DOWN: + break; + case MotionEvent.ACTION_MOVE: + + if (!draggable) + break; + + selectorWidth = (float) getWidth() / numberOfButtons / 2f; + + offsetX = ((event.getX() - selectorWidth) * numberOfButtons) / (float) getWidth(); + position = (int) Math.floor(offsetX); + offsetX -= position; + + if (event.getRawX() - selectorWidth < getLeft()) { + offsetX = 0; + animateViews(position + 1, offsetX); + break; + } + if (event.getRawX() + selectorWidth > getRight()) { + offsetX = 1; + animateViews(position - 1, offsetX); + break; + } + + animateViews(position, offsetX); + + break; + } + return true; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private class ButtonOutlineProvider extends ViewOutlineProvider { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight(), radius); + } + } + + private void init(AttributeSet attrs) { + getAttributes(attrs); + setWillNotDraw(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setOutlineProvider(new ButtonOutlineProvider()); + } + + setClickable(true); + + buttons = new ArrayList<>(); + + FrameLayout container = new FrameLayout(getContext()); + container.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + addView(container); + + mainGroup = new LinearLayout(getContext()); + mainGroup.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + mainGroup.setOrientation(LinearLayout.HORIZONTAL); + container.addView(mainGroup); + + rippleContainer = new LinearLayout(getContext()); + rippleContainer.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + rippleContainer.setOrientation(LinearLayout.HORIZONTAL); + rippleContainer.setClickable(false); + rippleContainer.setFocusable(false); + rippleContainer.setPadding(borderSize, borderSize, borderSize, borderSize); + container.addView(rippleContainer); + + dividerContainer = new LinearLayout(getContext()); + dividerContainer.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + dividerContainer.setOrientation(LinearLayout.HORIZONTAL); + dividerContainer.setClickable(false); + dividerContainer.setFocusable(false); + container.addView(dividerContainer); + + initInterpolations(); + setContainerAttrs(); + setDividerAttrs(); + + rectF = new RectF(); + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + private RectF rectF; + private Paint paint; + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float width = canvas.getWidth(); + float height = canvas.getHeight(); + + rectF.set(0, 0, width, height); + paint.setStyle(Paint.Style.FILL); + paint.setColor(backgroundColor); + canvas.drawRoundRect(rectF, radius, radius, paint); + + if (borderSize > 0) { + float bSize = borderSize / 2f; + rectF.set(0 + bSize, 0 + bSize, width - bSize, height - bSize); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(borderColor); + paint.setStrokeWidth(borderSize); + canvas.drawRoundRect(rectF, radius, radius, paint); + } + } + + private void setBackgroundColor(View v, Drawable d, int c) { + if (null != d) { + BackgroundHelper.setBackground(v, d); + } else { + v.setBackgroundColor(c); + } + } + + private void setDividerAttrs() { + if (!hasDivider) + return; + dividerContainer.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE); + // Divider Views + RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); + dividerContainer.setDividerPadding(dividerPadding); + } + + private int numberOfButtons = 0; + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + + if (child instanceof SegmentedButton) { + SegmentedButton button = (SegmentedButton) child; + final int position = numberOfButtons++; + + button.setSelectorColor(selectorColor); + button.setSelectorRadius(radius); + button.setBorderSize(borderSize); + + if (position == 0) + button.hasBorderLeft(true); + + if (position > 0) + buttons.get(position - 1).hasBorderRight(false); + + button.hasBorderRight(true); + + mainGroup.addView(child, params); + buttons.add(button); + + if (this.position == position) { + button.clipToRight(1); + + lastPosition = toggledPosition = position; + lastPositionOffset = toggledPositionOffset = (float) position; + } + + // RIPPLE + BackgroundView rippleView = new BackgroundView(getContext()); + if (!draggable) { + rippleView.setOnClickListener(v -> { + if (clickable && enabled) + toggle(position, animateSelectorDuration, true); + }); + } + + setRipple(rippleView, enabled && clickable); + rippleContainer.addView(rippleView, + new LinearLayout.LayoutParams(button.getButtonWidth(), ViewGroup.LayoutParams.MATCH_PARENT, button.getWeight())); + ripples.add(rippleView); + + if (!hasDivider) + return; + + BackgroundView dividerView = new BackgroundView(getContext()); + dividerContainer.addView(dividerView, + new LinearLayout.LayoutParams(button.getButtonWidth(), ViewGroup.LayoutParams.MATCH_PARENT, button.getWeight())); + } else + super.addView(child, index, params); + } + + private ArrayList ripples = new ArrayList<>(); + + private void setRipple(View v, boolean isClickable) { + if (isClickable) { + if (hasRippleColor) + RippleHelper.setRipple(v, rippleColor, radius); + else if (ripple) + RippleHelper.setSelectableItemBackground(getContext(), v); + else { + for (View button : buttons) { + if (button instanceof SegmentedButton && ((SegmentedButton) button).hasRipple()) + RippleHelper.setRipple(v, ((SegmentedButton) button).getRippleColor(), radius); + } + } + } else { + BackgroundHelper.setBackground(v, null); + } + } + + private void setContainerAttrs() { + if (isInEditMode()) + mainGroup.setBackgroundColor(backgroundColor); + } + + private ArrayList buttons; + + private int selectorColor, animateSelector, animateSelectorDuration, position, backgroundColor, dividerColor, radius, + dividerSize, rippleColor, dividerPadding, dividerRadius, borderSize, borderColor; + private boolean clickable, enabled, ripple, hasRippleColor, hasDivider; + + private Drawable backgroundDrawable, selectorBackgroundDrawable, dividerBackgroundDrawable; + + /** + * Get attributes + **/ + private void getAttributes(AttributeSet attrs) { + TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SegmentedButtonGroup); + + hasDivider = typedArray.hasValue(R.styleable.SegmentedButtonGroup_sbg_dividerSize); + dividerSize = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_dividerSize, 0); + dividerColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_dividerColor, Color.WHITE); + dividerPadding = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_dividerPadding, 0); + dividerRadius = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_dividerRadius, 0); + + selectorColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_selectorColor, Color.GRAY); + animateSelector = typedArray.getInt(R.styleable.SegmentedButtonGroup_sbg_animateSelector, 0); + animateSelectorDuration = typedArray.getInt(R.styleable.SegmentedButtonGroup_sbg_animateSelectorDuration, 500); + + radius = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_radius, 0); + position = typedArray.getInt(R.styleable.SegmentedButtonGroup_sbg_position, 0); + backgroundColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_backgroundColor, Color.TRANSPARENT); + + ripple = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_sbg_ripple, false); + hasRippleColor = typedArray.hasValue(R.styleable.SegmentedButtonGroup_sbg_rippleColor); + rippleColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_rippleColor, Color.GRAY); + + borderSize = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_borderSize, 0); + borderColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_borderColor, Color.BLACK); + + backgroundDrawable = typedArray.getDrawable(R.styleable.SegmentedButtonGroup_sbg_backgroundDrawable); + selectorBackgroundDrawable = typedArray.getDrawable(R.styleable.SegmentedButtonGroup_sbg_selectorBackgroundDrawable); + dividerBackgroundDrawable = typedArray.getDrawable(R.styleable.SegmentedButtonGroup_sbg_dividerBackgroundDrawable); + + enabled = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_sbg_enabled, true); + + draggable = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_sbg_draggable, false); + + try { + clickable = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_android_clickable, true); + } catch (Exception ex) { + Log.d("SegmentedButtonGroup", ex.toString()); + } + + typedArray.recycle(); + } + + private Interpolator interpolatorSelector; + + private void initInterpolations() { + ArrayList interpolatorList = new ArrayList() {{ + add(FastOutSlowInInterpolator.class); + add(BounceInterpolator.class); + add(LinearInterpolator.class); + add(DecelerateInterpolator.class); + add(CycleInterpolator.class); + add(AnticipateInterpolator.class); + add(AccelerateDecelerateInterpolator.class); + add(AccelerateInterpolator.class); + add(AnticipateOvershootInterpolator.class); + add(FastOutLinearInInterpolator.class); + add(LinearOutSlowInInterpolator.class); + add(OvershootInterpolator.class); + }}; + + try { + interpolatorSelector = (Interpolator) interpolatorList.get(animateSelector).newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public final static int FastOutSlowInInterpolator = 0; + public final static int BounceInterpolator = 1; + public final static int LinearInterpolator = 2; + public final static int DecelerateInterpolator = 3; + public final static int CycleInterpolator = 4; + public final static int AnticipateInterpolator = 5; + public final static int AccelerateDecelerateInterpolator = 6; + public final static int AccelerateInterpolator = 7; + public final static int AnticipateOvershootInterpolator = 8; + public final static int FastOutLinearInInterpolator = 9; + public final static int LinearOutSlowInInterpolator = 10; + public final static int OvershootInterpolator = 11; + + private OnPositionChangedListener onPositionChangedListener; + + /** + * @param onPositionChangedListener set your instance that you have created to listen any position change + */ + public void setOnPositionChangedListener(OnPositionChangedListener onPositionChangedListener) { + this.onPositionChangedListener = onPositionChangedListener; + } + + /** + * Use this listener if you want to know any position change. + * Listener is called when one of segmented button is clicked or setPosition is called. + */ + public interface OnPositionChangedListener { + void onPositionChanged(int position); + } + + private OnClickedButtonListener onClickedButtonListener; + + /** + * @param onClickedButtonListener set your instance that you have created to listen clicked positions + */ + public void setOnClickedButtonListener(OnClickedButtonListener onClickedButtonListener) { + this.onClickedButtonListener = onClickedButtonListener; + } + + /** + * Use this listener if you want to know which button is clicked. + * Listener is called when one of segmented button is clicked + */ + public interface OnClickedButtonListener { + void onClickedButton(int position); + } + + /** + * @param position is used to select one of segmented buttons + */ + public void setPosition(int position) { + this.position = position; + + if (null == buttons) { + lastPosition = toggledPosition = position; + lastPositionOffset = toggledPositionOffset = (float) position; + } else { + toggle(position, animateSelectorDuration, false); + } + } + + /** + * @param position is used to select one of segmented buttons + * @param duration determines how long animation takes to finish + */ + public void setPosition(int position, int duration) { + this.position = position; + + if (null == buttons) { + lastPosition = toggledPosition = position; + lastPositionOffset = toggledPositionOffset = (float) position; + } else { + toggle(position, duration, false); + } + } + + /** + * @param position is used to select one of segmented buttons + * @param withAnimation if true default animation will perform + */ + public void setPosition(int position, boolean withAnimation) { + this.position = position; + + if (null == buttons) { + lastPosition = toggledPosition = position; + lastPositionOffset = toggledPositionOffset = (float) position; + } else { + if (withAnimation) + toggle(position, animateSelectorDuration, false); + else + toggle(position, 1, false); + } + } + + /** + * @param selectorColor sets color to selector + * default: Color.GRAY + */ + public void setSelectorColor(int selectorColor) { + this.selectorColor = selectorColor; + } + + /** + * @param backgroundColor sets background color of whole layout including buttons on top of it + * default: Color.WHITE + */ + @Override + public void setBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + } + + /** + * @param ripple applies android's default ripple on layout + */ + public void setRipple(boolean ripple) { + this.ripple = ripple; + } + + /** + * @param rippleColor sets ripple color and adds ripple when a button is hovered + * default: Color.GRAY + */ + public void setRippleColor(int rippleColor) { + this.rippleColor = rippleColor; + } + + /** + * @param hasRippleColor if true ripple will be shown. + * if setRipple(boolean) is also set to false, there will be no ripple + */ + public void setRippleColor(boolean hasRippleColor) { + this.hasRippleColor = hasRippleColor; + } + + /** + * @param radius determines how round layout's corners should be + */ + public void setRadius(int radius) { + this.radius = radius; + } + + /** + * @param dividerPadding adjusts divider's top and bottom distance to its container + */ + public void setDividerPadding(int dividerPadding) { + this.dividerPadding = dividerPadding; + } + + /** + * @param animateSelectorDuration sets how long selector animation should last + */ + public void setSelectorAnimationDuration(int animateSelectorDuration) { + this.animateSelectorDuration = animateSelectorDuration; + } + + /** + * @param animateSelector is used to give an animation to selector with the given interpolator constant + */ + public void setSelectorAnimation(int animateSelector) { + this.animateSelector = animateSelector; + } + + /** + * @param interpolatorSelector is used to give an animation to selector with the given one of android's interpolator. + * Ex: {@link FastOutSlowInInterpolator}, {@link BounceInterpolator}, {@link LinearInterpolator} + */ + public void setInterpolatorSelector(Interpolator interpolatorSelector) { + this.interpolatorSelector = interpolatorSelector; + } + + /** + * @param dividerColor changes divider's color with the given one + * default: Color.WHITE + */ + public void setDividerColor(int dividerColor) { + this.dividerColor = dividerColor; + RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); + } + + /** + * @param dividerSize sets thickness of divider + * default: 0 + */ + public void setDividerSize(int dividerSize) { + this.dividerSize = dividerSize; + RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); + } + + /** + * @param dividerRadius determines how round divider should be + * default: 0 + */ + public void setDividerRadius(int dividerRadius) { + this.dividerRadius = dividerRadius; + RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); + } + + /** + * @param hasDivider if true divider will be shown. + */ + public void setDivider(boolean hasDivider) { + this.hasDivider = hasDivider; + } + + /** + * @param borderSize sets thickness of border + * default: 0 + */ + public void setBorderSize(int borderSize) { + this.borderSize = borderSize; + } + + /** + * @param borderColor sets border color to the given one + * default: Color.BLACK + */ + public void setBorderColor(int borderColor) { + this.borderColor = borderColor; + } + + public int getDividerSize() { + return dividerSize; + } + + public int getRippleColor() { + return rippleColor; + } + + public int getSelectorColor() { + return selectorColor; + } + + public int getSelectorAnimation() { + return animateSelector; + } + + public int getSelectorAnimationDuration() { + return animateSelectorDuration; + } + + public int getPosition() { + return position; + } + + public int getBackgroundColor() { + return backgroundColor; + } + + public int getDividerColor() { + return dividerColor; + } + + public float getRadius() { + return radius; + } + + public int getDividerPadding() { + return dividerPadding; + } + + public float getDividerRadius() { + return dividerRadius; + } + + public boolean isHasDivider() { + return hasDivider; + } + + public boolean isHasRippleColor() { + return hasRippleColor; + } + + public boolean isRipple() { + return ripple; + } + + public Interpolator getInterpolatorSelector() { + return interpolatorSelector; + } + + private void setRippleState(boolean state) { + for (View v : ripples) { + setRipple(v, state); + } + } + + private void setEnabledAlpha(boolean enabled) { + float alpha = 1f; + if (!enabled) + alpha = 0.5f; + + setAlpha(alpha); + } + + + /** + * @param enabled set it to: + * false, if you want buttons to be unclickable and add grayish looking which gives disabled look, + * true, if you want buttons to be clickable and remove grayish looking + */ + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + setRippleState(enabled); + setEnabledAlpha(enabled); + } + + /** + * @param clickable set it to: + * false for unclickable buttons, + * true for clickable buttons + */ + @Override + public void setClickable(boolean clickable) { + this.clickable = clickable; + setRippleState(clickable); + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + bundle.putParcelable("state", super.onSaveInstanceState()); + bundle.putInt("position", position); + return bundle; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + position = bundle.getInt("position"); + state = bundle.getParcelable("state"); + + setPosition(position, false); + } + super.onRestoreInstanceState(state); + } + + /** + * + * + * + * + * + * + * + * + * + */ + + private int toggledPosition = 0; + private float toggledPositionOffset = 0; + + private void toggle(int position, int duration, boolean isToggledByTouch) { + if (!draggable && toggledPosition == position) + return; + + toggledPosition = position; + + ValueAnimator animator = ValueAnimator.ofFloat(toggledPositionOffset, position); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float animatedValue = toggledPositionOffset = (float) animation.getAnimatedValue(); + + int position = (int) animatedValue; + float positionOffset = animatedValue - position; + + animateViews(position, positionOffset); + + invalidate(); + } + }); + animator.setInterpolator(interpolatorSelector); + animator.setDuration(duration); + animator.start(); + + + if (null != onClickedButtonListener && isToggledByTouch) + onClickedButtonListener.onClickedButton(position); + + if (null != onPositionChangedListener) + onPositionChangedListener.onPositionChanged(position); + + this.position = position; + } + + private int lastPosition = 0; + private float lastPositionOffset = 0; + + private void animateViews(int position, float positionOffset) { + float realPosition = position + positionOffset; + float lastRealPosition = lastPosition + lastPositionOffset; + + + if (realPosition == lastRealPosition) { + return; + } + + int nextPosition = position + 1; + if (positionOffset == 0.0f) { + if (lastRealPosition <= realPosition) { + nextPosition = position - 1; + } + } + + if (lastPosition > position) { + if (lastPositionOffset > 0f) { + toNextPosition(nextPosition + 1, 1); + } + } + + if (lastPosition < position) { + if (lastPositionOffset < 1.0f) { + toPosition(position - 1, 0); + } + } + + toNextPosition(nextPosition, 1.0f - positionOffset); + toPosition(position, 1.0f - positionOffset); + + lastPosition = position; + lastPositionOffset = positionOffset; + } + + private void toPosition(int position, float clip) { + if (position >= 0 && position < numberOfButtons) + buttons.get(position).clipToRight(clip); + } + + private void toNextPosition(int position, float clip) { + if (position >= 0 && position < numberOfButtons) + buttons.get(position).clipToLeft(clip); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/SingleDateAndTimePicker.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/SingleDateAndTimePicker.java new file mode 100644 index 0000000000000000000000000000000000000000..6ca8729a348076d1ea784498da9dc14a73676a16 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/SingleDateAndTimePicker.java @@ -0,0 +1,389 @@ +package com.nynja.mobile.communicator.ui.views.timepicker; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.support.annotation.Nullable; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +import com.nynja.mobile.communicator.R; +import com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker.WheelAmPmPicker; +import com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker.WheelHourPicker; +import com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker.WheelMinutePicker; +import com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker.WheelPicker; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; + +public class SingleDateAndTimePicker extends LinearLayout { + + public static final boolean IS_CYCLIC_DEFAULT = true; + public static final boolean IS_CURVED_DEFAULT = false; + public static final boolean MUST_BE_ON_FUTUR_DEFAULT = false; + public static final int DELAY_BEFORE_CHECK_PAST = 200; + private static final int VISIBLE_ITEM_COUNT_DEFAULT = 7; + private static final int PM_HOUR_ADDITION = 12; + + private static final CharSequence FORMAT_24_HOUR = "EEE d MMM H:mm"; + private static final CharSequence FORMAT_12_HOUR = "EEE d MMM h:mm a"; + + private WheelMinutePicker minutesPicker; + private WheelHourPicker hoursPicker; + private WheelAmPmPicker amPmPicker; + + private Listener listener; + + private String todayText; + private int textColor; + private int selectedTextColor; + private int textSize; + private int selectorColor; + private boolean isCyclic; + private boolean isCurved; + private int visibleItemCount; + private boolean mustBeOnFuture; + + @Nullable + private Date minDate; + @Nullable + private Date maxDate; + private Date defaultDate; + + private boolean displayDays = true; + private boolean displayMinutes = true; + private boolean displayHours = true; + + private boolean isAmPm; + private int selectorHeight; + + public SingleDateAndTimePicker(Context context) { + this(context, null); + } + + public SingleDateAndTimePicker(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SingleDateAndTimePicker(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + inflate(context, R.layout.single_day_picker, this); + + isAmPm = true; + + minutesPicker = findViewById(R.id.minutesPicker); + hoursPicker = findViewById(R.id.hoursPicker); + amPmPicker = findViewById(R.id.amPmPicker); + + minutesPicker.setOnMinuteSelectedListener(new WheelMinutePicker.OnMinuteSelectedListener() { + @Override + public void onMinuteSelected(WheelMinutePicker picker, int position, int minutes) { + updateListener(); + checkMinMaxDate(picker); + } + + @Override + public void onMinuteCurrentScrolled(WheelMinutePicker picker, int position, int minutes) { + + } + + @Override + public void onMinuteScrolledNewHour(WheelMinutePicker picker) { + hoursPicker.scrollTo(hoursPicker.getCurrentItemPosition() + 1); + } + }); + + hoursPicker.setOnHourSelectedListener(new WheelHourPicker.OnHourSelectedListener() { + @Override + public void onHourSelected(WheelHourPicker picker, int position, int hours) { + updateListener(); + checkMinMaxDate(picker); + } + + @Override + public void onHourCurrentScrolled(WheelHourPicker picker, int position, int hours) { + + } + + @Override + public void onHourCurrentNewDay(WheelHourPicker picker) { + } + }); + + amPmPicker.setOnAmPmSelectedListener(new WheelAmPmPicker.OnAmPmSelectedListener() { + @Override + public void onAmSelected(WheelAmPmPicker picker) { + updateListener(); + checkMinMaxDate(picker); + } + + @Override + public void onPmSelected(WheelAmPmPicker picker) { + updateListener(); + checkMinMaxDate(picker); + } + }); + + updatePicker(); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + minutesPicker.setEnabled(enabled); + hoursPicker.setEnabled(enabled); + amPmPicker.setEnabled(enabled); + } + + public void setDisplayDays(boolean displayDays) { + this.displayDays = displayDays; + updatePicker(); + } + + public void setDisplayMinutes(boolean displayMinutes) { + this.displayMinutes = displayMinutes; + updatePicker(); + } + + public void setDisplayHours(boolean displayHours) { + this.displayHours = displayHours; + updatePicker(); + } + + public void setCurved(boolean curved) { + isCurved = curved; + updatePicker(); + } + + public void setCyclic(boolean cyclic) { + isCyclic = cyclic; + updatePicker(); + } + + public void setTextSize(int textSize) { + this.textSize = textSize; + updatePicker(); + } + + public void setSelectedTextColor(int selectedTextColor) { + this.selectedTextColor = selectedTextColor; + updatePicker(); + } + + public void setTextColor(int textColor) { + this.textColor = textColor; + updatePicker(); + } + + public void setSelectorColor(int selectorColor) { + this.selectorColor = selectorColor; + } + + public void setVisibleItemCount(int visibleItemCount) { + this.visibleItemCount = visibleItemCount; + updatePicker(); + } + + public void setIsAmPm(boolean isAmPm) { + this.isAmPm = isAmPm; + updatePicker(); + } + + public boolean isAmPm() { + return isAmPm; + } + + public Date getMinDate() { + return minDate; + } + + public void setMinDate(@Nullable Date minDate) { + this.minDate = minDate; + } + + public Date getMaxDate() { + return maxDate; + } + + public void setMaxDate(@Nullable Date maxDate) { + this.maxDate = maxDate; + } + + private void updatePicker() { + if (minutesPicker != null && hoursPicker != null && amPmPicker != null) { + for (WheelPicker wheelPicker : Arrays.asList(minutesPicker, hoursPicker, amPmPicker)) { + wheelPicker.setVisibleItemCount(visibleItemCount); + wheelPicker.setCurved(isCurved); + if (wheelPicker != amPmPicker) { + wheelPicker.setCyclic(isCyclic); + } + } + } + + if (amPmPicker != null) { + amPmPicker.setVisibility((isAmPm && displayHours) ? VISIBLE : GONE); + } + if (hoursPicker != null) { + hoursPicker.setIsAmPm(isAmPm); + + if (defaultDate != null) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(defaultDate); + if (isAmPm) { + hoursPicker.setDefaultHour(calendar.get(Calendar.HOUR)); + } else { + hoursPicker.setDefaultHour(calendar.get(Calendar.HOUR_OF_DAY)); + } + } + } + + if (hoursPicker != null) { + hoursPicker.setVisibility(displayHours ? VISIBLE : GONE); + } + if (minutesPicker != null) { + minutesPicker.setVisibility(displayMinutes ? VISIBLE : GONE); + } + } + + private void checkMinMaxDate(final WheelPicker picker) { + checkBeforeMinDate(picker); + checkAfterMaxDate(picker); + } + + private void checkBeforeMinDate(final WheelPicker picker) { + picker.postDelayed(() -> { + if (minDate != null && isBeforeMinDate(getDate())) { + //scroll to Min position + amPmPicker.scrollTo(amPmPicker.findIndexOfDate(minDate)); + minutesPicker.scrollTo(minutesPicker.findIndexOfDate(minDate)); + hoursPicker.scrollTo(hoursPicker.findIndexOfDate(minDate)); + } + }, DELAY_BEFORE_CHECK_PAST); + } + + private void checkAfterMaxDate(final WheelPicker picker) { + picker.postDelayed(() -> { + if (maxDate != null && isAfterMaxDate(getDate())) { + //scroll to Max position + amPmPicker.scrollTo(amPmPicker.findIndexOfDate(maxDate)); + minutesPicker.scrollTo(minutesPicker.findIndexOfDate(maxDate)); + hoursPicker.scrollTo(hoursPicker.findIndexOfDate(maxDate)); + } + }, DELAY_BEFORE_CHECK_PAST); + } + + private boolean isBeforeMinDate(Date date) { + final Calendar minDateCalendar = Calendar.getInstance(); + minDateCalendar.setTime(minDate); + minDateCalendar.set(Calendar.MILLISECOND, 0); + minDateCalendar.set(Calendar.SECOND, 0); + + final Calendar dateCalendar = Calendar.getInstance(); + dateCalendar.setTime(date); + dateCalendar.set(Calendar.MILLISECOND, 0); + dateCalendar.set(Calendar.SECOND, 0); + + return dateCalendar.before(minDateCalendar); + } + + private boolean isAfterMaxDate(Date date) { + final Calendar maxDateCalendar = Calendar.getInstance(); + maxDateCalendar.setTime(maxDate); + maxDateCalendar.set(Calendar.MILLISECOND, 0); + maxDateCalendar.set(Calendar.SECOND, 0); + + final Calendar dateCalendar = Calendar.getInstance(); + dateCalendar.setTime(date); + dateCalendar.set(Calendar.MILLISECOND, 0); + dateCalendar.set(Calendar.SECOND, 0); + + return dateCalendar.after(maxDateCalendar); + } + + public void setListener(Listener listener) { + this.listener = listener; + } + + public Date getDate() { + int hour = hoursPicker.getCurrentHour(); + if (isAmPm && amPmPicker.isPm()) { + hour += PM_HOUR_ADDITION; + } + final int minute = minutesPicker.getCurrentMinute(); + + final Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + + return calendar.getTime(); + } + + public void setStepMinutes(int minutesStep) { + minutesPicker.setStepMinutes(minutesStep); + } + + public void setHoursStep(int hoursStep) { + hoursPicker.setHoursStep(hoursStep); + } + + public void setDefaultDate(Date date) { + this.defaultDate = date; + } + + public void selectDate(Calendar calendar) { + if (calendar == null) { + return; + } + Date date = calendar.getTime(); + amPmPicker.setSelectedItemPosition(amPmPicker.findIndexOfDate(date)); + hoursPicker.setSelectedItemPosition(hoursPicker.findIndexOfDate(date)); + minutesPicker.setSelectedItemPosition(minutesPicker.findIndexOfDate(date)); + } + + private void updateListener() { + final Date date = getDate(); + CharSequence format = isAmPm ? FORMAT_12_HOUR : FORMAT_24_HOUR; + String displayed = DateFormat.format(format, date).toString(); + if (listener != null) { + listener.onDateChanged(displayed, date); + } + } + + public void setMustBeOnFuture(boolean mustBeOnFuture) { + this.mustBeOnFuture = mustBeOnFuture; + if (mustBeOnFuture) { + minDate = Calendar.getInstance().getTime(); //minDate is Today + } + } + + public boolean mustBeOnFuture() { + return mustBeOnFuture; + } + + private void init(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SingleDateAndTimePicker); + + final Resources resources = getResources(); + todayText = a.getString(R.styleable.SingleDateAndTimePicker_picker_todayText); + selectorHeight = a.getDimensionPixelSize(R.styleable.SingleDateAndTimePicker_picker_selectorHeight, resources.getDimensionPixelSize(R.dimen.wheelSelectorHeight)); + textSize = a.getDimensionPixelSize(R.styleable.SingleDateAndTimePicker_picker_textSize, + resources.getDimensionPixelSize(R.dimen.WheelItemTextSize)); + isCurved = a.getBoolean(R.styleable.SingleDateAndTimePicker_picker_curved, IS_CURVED_DEFAULT); + isCyclic = a.getBoolean(R.styleable.SingleDateAndTimePicker_picker_cyclic, IS_CYCLIC_DEFAULT); + mustBeOnFuture = a.getBoolean(R.styleable.SingleDateAndTimePicker_picker_mustBeOnFuture, MUST_BE_ON_FUTUR_DEFAULT); + visibleItemCount = a.getInt(R.styleable.SingleDateAndTimePicker_picker_visibleItemCount, VISIBLE_ITEM_COUNT_DEFAULT); + + displayDays = a.getBoolean(R.styleable.SingleDateAndTimePicker_picker_displayDays, displayDays); + displayMinutes = a.getBoolean(R.styleable.SingleDateAndTimePicker_picker_displayMinutes, displayMinutes); + displayHours = a.getBoolean(R.styleable.SingleDateAndTimePicker_picker_displayHours, displayHours); + + a.recycle(); + } + + public interface Listener { + void onDateChanged(String displayed, Date date); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelAmPmPicker.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelAmPmPicker.java new file mode 100644 index 0000000000000000000000000000000000000000..fe97c70a5ce32fdf64bef28af3df18f74ec6421e --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelAmPmPicker.java @@ -0,0 +1,93 @@ +package com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; + +import com.nynja.mobile.communicator.R; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class WheelAmPmPicker extends WheelPicker { + + public static final int INDEX_AM = 0; + public static final int INDEX_PM = 1; + private Adapter adapter; + + private int lastScrollPosition; + + private OnAmPmSelectedListener onAmPmSelectedListener; + + public WheelAmPmPicker(Context context) { + this(context, null); + } + + public WheelAmPmPicker(Context context, AttributeSet attrs) { + super(context, attrs); + initAdapter(); + } + + private void initAdapter() { + final List values = new ArrayList<>(); + Resources resources = getResources(); + values.add(resources.getString(R.string.picker_am)); + values.add(resources.getString(R.string.picker_pm)); + adapter = new Adapter(values); + setAdapter(adapter); + } + + + public void setOnAmPmSelectedListener(OnAmPmSelectedListener onAmPmSelectedListener) { + this.onAmPmSelectedListener = onAmPmSelectedListener; + } + + @Override + protected void onItemSelected(int position, Object item) { + if (onAmPmSelectedListener != null) { + if (position == INDEX_AM) { + onAmPmSelectedListener.onAmSelected(this); + } else { + onAmPmSelectedListener.onPmSelected(this); + } + } + } + + @Override + protected void onItemCurrentScroll(int position, Object item) { + if (lastScrollPosition != position) { + lastScrollPosition = position; + } + } + + @Override + protected String getFormattedValue(Object value) { + if (value instanceof Date) { + Calendar instance = Calendar.getInstance(); + instance.setTime((Date) value); + return getResources().getString(instance.get(Calendar.AM_PM) == Calendar.PM ? R.string.picker_pm: R.string.picker_am); + } + return String.valueOf(value); + } + + @Override + public int getDefaultItemPosition() { + return INDEX_AM; + } + + public boolean isAm() { + return getCurrentItemPosition() == INDEX_AM; + } + + public boolean isPm() { + return getCurrentItemPosition() == INDEX_PM; + } + + public interface OnAmPmSelectedListener { + void onAmSelected(WheelAmPmPicker picker); + + void onPmSelected(WheelAmPmPicker picker); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelDayPicker.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelDayPicker.java new file mode 100644 index 0000000000000000000000000000000000000000..da4924f5d6ad494dba3c0c61fc8d405e82303249 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelDayPicker.java @@ -0,0 +1,158 @@ +package com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker; + +import android.content.Context; +import android.util.AttributeSet; + +import com.nynja.mobile.communicator.R; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class WheelDayPicker extends WheelPicker { + + public static final int DAYS_PADDING = 364; + private int defaultIndex; + + private int todayPosition; + + private SimpleDateFormat simpleDateFormat; + + private OnDaySelectedListener onDaySelectedListener; + + WheelPicker.Adapter adapter; + + public WheelDayPicker(Context context) { + this(context, null); + } + + public WheelDayPicker(Context context, AttributeSet attrs) { + super(context, attrs); + + this.simpleDateFormat = new SimpleDateFormat("EEE d MMM", getCurrentLocale()); + this.adapter = new Adapter(); + setAdapter(adapter); + + updateDays(); + + updateDefaultDay(); + } + + public WheelDayPicker setDayFormatter(SimpleDateFormat simpleDateFormat){ + this.simpleDateFormat = simpleDateFormat; + updateDays(); + return this; + } + + @Override + protected void onItemSelected(int position, Object item) { + if (onDaySelectedListener != null) { + final String itemText = (String) item; + final Date date = convertItemToDate(position); + onDaySelectedListener.onDaySelected(this, position, itemText, date); + } + } + + @Override + protected void onItemCurrentScroll(int position, Object item) { + } + + @Override + public int getDefaultItemPosition() { + return defaultIndex; + } + + private void updateDays() { + final List data = new ArrayList<>(); + + Calendar instance = Calendar.getInstance(); + instance.add(Calendar.DATE, -1 * DAYS_PADDING - 1); + for (int i = (-1) * DAYS_PADDING; i < 0; ++i) { + instance.add(Calendar.DAY_OF_MONTH, 1); + data.add(getFormattedValue(instance.getTime())); + } + + todayPosition = data.size(); + defaultIndex = todayPosition; + + //today + data.add(getResources().getString(R.string.picker_today)); + + instance = Calendar.getInstance(); + + for (int i = 0; i < DAYS_PADDING; ++i) { + instance.add(Calendar.DATE, 1); + data.add(getFormattedValue(instance.getTime())); + } + + adapter.setData(data); + notifyDatasetChanged(); + } + + protected String getFormattedValue(Object value) { + return simpleDateFormat.format(value); + } + + public void setOnDaySelectedListener(OnDaySelectedListener onDaySelectedListener) { + this.onDaySelectedListener = onDaySelectedListener; + } + + private void updateDefaultDay() { + setSelectedItemPosition(defaultIndex); + } + + public int getDefaultDayIndex() { + return defaultIndex; + } + + public Date getCurrentDate() { + return convertItemToDate(super.getCurrentItemPosition()); + } + + private Date convertItemToDate(int itemPosition) { + Date date = null; + String itemText = adapter.getItemText(itemPosition); + final Calendar todayCalendar = Calendar.getInstance(); + if (itemPosition == todayPosition) { + date = todayCalendar.getTime(); + } else { + try { + date = simpleDateFormat.parse(itemText); + } catch (ParseException e) { + e.printStackTrace(); + } + } + + if (date != null) { + //try to know the year + final Calendar dateCalendar = Calendar.getInstance(); + dateCalendar.setTime(date); + + todayCalendar.add(Calendar.DATE, (itemPosition - todayPosition)); + + dateCalendar.set(Calendar.YEAR, todayCalendar.get(Calendar.YEAR)); + date = dateCalendar.getTime(); + } + + return date; + } + + public String getCurrentDay() { + return adapter.getItemText(getCurrentItemPosition()); + } + + public void setTodayText(String todayText) { + int index = adapter.getData().indexOf(getResources().getString(R.string.picker_today)); + if (index != -1) { + adapter.getData().set(index, todayText); + notifyDatasetChanged(); + } + } + + public interface OnDaySelectedListener { + void onDaySelected(WheelDayPicker picker, int position, String name, Date date); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelHourPicker.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelHourPicker.java new file mode 100644 index 0000000000000000000000000000000000000000..ffbfa6d00784ae64113c13697d129a7c44a58c9f --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelHourPicker.java @@ -0,0 +1,186 @@ +package com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class WheelHourPicker extends WheelPicker { + + public static final int MIN_HOUR_DEFAULT = 0; + public static final int MAX_HOUR_DEFAULT = 23; + public static final int MAX_HOUR_AM_PM = 12; + public static final int STEP_HOURS_DEFAULT = 1; + + private OnHourSelectedListener hoursSelectedListener; + + private int defaultHour; + private int minHour = MIN_HOUR_DEFAULT; + private int maxHour = MAX_HOUR_DEFAULT; + private int hoursStep = STEP_HOURS_DEFAULT; + + private int lastScrollPosition; + protected boolean isAmPm = false; + + private WheelPicker.Adapter adapter; + + public WheelHourPicker(Context context) { + this(context, null); + } + + public WheelHourPicker(Context context, AttributeSet attrs) { + super(context, attrs); + initAdapter(); + } + + private void initAdapter() { + final List hours = new ArrayList<>(); + + if (isAmPm) { + hours.add(getFormattedValue(12)); + for (int hour = hoursStep; hour < maxHour; hour += hoursStep) { + hours.add(getFormattedValue(hour)); + } + } else { + for (int hour = minHour; hour <= maxHour; hour += hoursStep) { + hours.add(getFormattedValue(hour)); + } + } + + adapter = new Adapter(hours); + setAdapter(adapter); + + java.util.Calendar calendar = java.util.Calendar.getInstance(); + // todo set default date calendar.setTimeFromMessage(defa); + + defaultHour = calendar.get(Calendar.HOUR_OF_DAY); + if (isAmPm && defaultHour >= MAX_HOUR_AM_PM) { + defaultHour -= MAX_HOUR_AM_PM; + } + + updateDefaultHour(); + } + + @Override + protected void onItemSelected(int position, Object item) { + if (hoursSelectedListener != null) { + hoursSelectedListener.onHourSelected(this, position, convertItemToHour(item)); + } + } + + @Override + protected void onItemCurrentScroll(int position, Object item) { + if (lastScrollPosition != position) { + if (hoursSelectedListener != null) { + hoursSelectedListener.onHourCurrentScrolled(this, position, convertItemToHour(item)); + if (lastScrollPosition == MAX_HOUR_DEFAULT && position == 0) + hoursSelectedListener.onHourCurrentNewDay(this); + } + lastScrollPosition = position; + } + } + + @Override + public int findIndexOfDate(@NonNull Date date) { + if (isAmPm) { + final int hours = date.getHours(); + if (hours >= MAX_HOUR_AM_PM) { + Date copy = new Date(date.getTime()); + copy.setHours(hours % 12); + return super.findIndexOfDate(copy); + } + } + return super.findIndexOfDate(date); + } + + protected String getFormattedValue(Object value) { + Object valueItem = value; + if (value instanceof Date) { + Calendar instance = Calendar.getInstance(); + instance.setTime((Date) value); + valueItem = instance.get(Calendar.HOUR_OF_DAY); + } + return valueItem.toString(); + } + + private void updateDefaultHour() { + setSelectedItemPosition(defaultHour); + } + + @Override + public int getDefaultItemPosition() { + return defaultHour; + } + + public void setOnHourSelectedListener(OnHourSelectedListener hoursSelectedListener) { + this.hoursSelectedListener = hoursSelectedListener; + } + + public void setDefaultHour(int hour) { + if (isAmPm && hour >= MAX_HOUR_AM_PM) { + hour -= MAX_HOUR_AM_PM; + } + + defaultHour = hour; + updateDefaultHour(); + } + + public void setIsAmPm(boolean isAmPm) { + this.isAmPm = isAmPm; + if (isAmPm) { + setMaxHour(MAX_HOUR_AM_PM); + } else { + setMaxHour(MAX_HOUR_DEFAULT); + } + } + + public void setMaxHour(int maxHour) { + if (maxHour >= MIN_HOUR_DEFAULT && maxHour <= MAX_HOUR_DEFAULT) { + this.maxHour = maxHour; + } + initAdapter(); + } + + public void setMinHour(int minHour) { + if (minHour >= MIN_HOUR_DEFAULT && minHour <= MAX_HOUR_DEFAULT) { + this.minHour = minHour; + } + initAdapter(); + } + + public void setHoursStep(int hourStep) { + if (hoursStep >= MIN_HOUR_DEFAULT && hoursStep <= MAX_HOUR_DEFAULT) { + this.hoursStep = hoursStep; + } + initAdapter(); + } + + private int convertItemToHour(Object item) { + Integer hour = Integer.valueOf(String.valueOf(item)); + if (!isAmPm) { + return hour; + } + + if (hour == 12) { + hour = 0; + } + + return hour; + } + + public int getCurrentHour() { + return convertItemToHour(adapter.getItem(getCurrentItemPosition())); + } + + public interface OnHourSelectedListener { + void onHourSelected(WheelHourPicker picker, int position, int hours); + + void onHourCurrentScrolled(WheelHourPicker picker, int position, int hours); + + void onHourCurrentNewDay(WheelHourPicker picker); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelMinutePicker.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelMinutePicker.java new file mode 100644 index 0000000000000000000000000000000000000000..9256cbfa741bb91aef713f5532c2272b41bbd64e --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelMinutePicker.java @@ -0,0 +1,131 @@ +package com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker; + +import android.content.Context; +import android.util.AttributeSet; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class WheelMinutePicker extends WheelPicker { + public static final int MIN_MINUTES = 0; + public static final int MAX_MINUTES = 59; + public static final int STEP_MINUTES_DEFAULT = 1; + + private int defaultMinute; + private int stepMinutes = STEP_MINUTES_DEFAULT; + + private WheelPicker.Adapter adapter; + + private int lastScrollPosition; + + private OnMinuteSelectedListener onMinuteSelectedListener; + + public WheelMinutePicker(Context context) { + this(context, null); + } + + public WheelMinutePicker(Context context, AttributeSet attrs) { + super(context, attrs); + initAdapter(); + } + + private void initAdapter(){ + final List minutes = new ArrayList<>(); + for (int min = MIN_MINUTES; min <= MAX_MINUTES; min += stepMinutes) { + minutes.add(getFormattedValue(min)); + } + adapter = new Adapter(minutes); + setAdapter(adapter); + + java.util.Calendar calendar = java.util.Calendar.getInstance(); + // todo fix. calendar.setTimeFromMessage(defaultDate); + defaultMinute = calendar.get(Calendar.MINUTE); + + updateDefaultMinute(); + } + + + public void setOnMinuteSelectedListener(OnMinuteSelectedListener onMinuteSelectedListener) { + this.onMinuteSelectedListener = onMinuteSelectedListener; + } + + @Override + protected void onItemSelected(int position, Object item) { + if (onMinuteSelectedListener != null) { + onMinuteSelectedListener.onMinuteSelected(this, position, convertItemToMinute(item)); + } + } + + @Override + protected void onItemCurrentScroll(int position, Object item) { + if (lastScrollPosition != position) { + if (onMinuteSelectedListener != null) { + onMinuteSelectedListener.onMinuteCurrentScrolled(this, position, convertItemToMinute(item)); + if (lastScrollPosition == 11 && position == 0) + onMinuteSelectedListener.onMinuteScrolledNewHour(this); + } + lastScrollPosition = position; + } + } + + private int findIndexOfMinute(int minute) { + final int itemCount = adapter.getItemCount(); + for (int i = 0; i < itemCount; ++i) { + final String object = adapter.getItemText(i); + final Integer value = Integer.valueOf(object); + if (minute < value) { + return i - 1; + } + } + return 0; + } + + protected String getFormattedValue(Object value) { + Object valueItem = value; + if (value instanceof Date) { + Calendar instance = Calendar.getInstance(); + instance.setTime((Date) value); + valueItem = instance.get(Calendar.MINUTE); + } + return String.format(getCurrentLocale(), FORMAT, valueItem); + } + + private void updateDefaultMinute() { + setSelectedItemPosition(findIndexOfMinute(defaultMinute)); + } + + public void setDefaultMinute(int minutes) { + this.defaultMinute = minutes; + updateDefaultMinute(); + } + + public void setStepMinutes(int stepMinutes) { + if (stepMinutes < 60 && stepMinutes > 0) { + this.stepMinutes = stepMinutes; + initAdapter(); + } + } + + @Override + public int getDefaultItemPosition() { + return findIndexOfMinute(defaultMinute); + } + + private int convertItemToMinute(Object item) { + return Integer.valueOf(String.valueOf(item)); + } + + public int getCurrentMinute() { + return convertItemToMinute(adapter.getItem(getCurrentItemPosition())); + } + + public interface OnMinuteSelectedListener { + void onMinuteSelected(WheelMinutePicker picker, int position, int minutes); + + void onMinuteCurrentScrolled(WheelMinutePicker picker, int position, int minutes); + + void onMinuteScrolledNewHour(WheelMinutePicker picker); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelPicker.java b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelPicker.java new file mode 100644 index 0000000000000000000000000000000000000000..61555f028e08a6123de57ef482913902f2f90f88 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/ui/views/timepicker/wheelpicker/WheelPicker.java @@ -0,0 +1,998 @@ +package com.nynja.mobile.communicator.ui.views.timepicker.wheelpicker; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Camera; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.Scroller; + +import com.nynja.mobile.communicator.R; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public abstract class WheelPicker extends View { + + public static final int SCROLL_STATE_IDLE = 0; + public static final int SCROLL_STATE_DRAGGING = 1; + public static final int SCROLL_STATE_SCROLLING = 2; + + public static final int ALIGN_CENTER = 0; + public static final int ALIGN_LEFT = 1; + public static final int ALIGN_RIGHT = 2; + + protected final static String FORMAT = "%1$02d"; // two digits + + private final Handler handler = new Handler(); + + private Paint paint; + private Scroller scroller; + private VelocityTracker tracker; + + private OnItemSelectedListener onItemSelectedListener; + private OnWheelChangeListener onWheelChangeListener; + + private Rect rectDrawn; + private Rect rectIndicatorHead, rectIndicatorFoot; + private Rect rectCurrentItem; + + private Camera camera; + private Matrix matrixRotate, matrixDepth; + private BaseAdapter adapter; + private String maxWidthText; + + private int mVisibleItemCount, mDrawnItemCount; + private int mHalfDrawnItemCount; + private int mTextMaxWidth, mTextMaxHeight; + private int mItemTextColor, mSelectedItemTextColor; + private int mItemTextSize; + private int mIndicatorSize; + private int mIndicatorColor; + private int mCurtainColor; + private int mItemSpace; + private int mItemAlign; + private int mItemHeight, mHalfItemHeight; + private int mHalfWheelHeight; + private int selectedItemPosition; + private int currentItemPosition; + private int minFlingY, maxFlingY; + private int minimumVelocity = 50, maximumVelocity = 8000; + private int wheelCenterX, wheelCenterY; + private int drawnCenterX, drawnCenterY; + private int scrollOffsetY; + private int textMaxWidthPosition; + private int lastPointY; + private int downPointY; + private int touchSlop = 8; + + private boolean hasSameWidth; + private boolean hasIndicator; + private boolean hasCurtain; + private boolean hasAtmospheric; + private boolean isCyclic; + private boolean isCurved; + + private boolean isClick; + private boolean isForceFinishScroll; + private Runnable runnable = new Runnable() { + @Override + public void run() { + if (null == adapter) return; + final int itemCount = adapter.getItemCount(); + if (itemCount == 0) return; + if (scroller.isFinished() && !isForceFinishScroll) { + if (mItemHeight == 0) return; + int position = (-scrollOffsetY / mItemHeight + selectedItemPosition) % itemCount; + position = position < 0 ? position + itemCount : position; + currentItemPosition = position; + onItemSelected(); + if (null != onWheelChangeListener) { + onWheelChangeListener.onWheelSelected(position); + onWheelChangeListener.onWheelScrollStateChanged(SCROLL_STATE_IDLE); + } + } + if (scroller.computeScrollOffset()) { + if (null != onWheelChangeListener) { + onWheelChangeListener.onWheelScrollStateChanged(SCROLL_STATE_SCROLLING); + } + + scrollOffsetY = scroller.getCurrY(); + + int position = (-scrollOffsetY / mItemHeight + selectedItemPosition) % itemCount; + if (onItemSelectedListener != null) { + onItemSelectedListener.onCurrentItemOfScroll(WheelPicker.this, position); + } + onItemCurrentScroll(position, adapter.getItem(position)); + + postInvalidate(); + handler.postDelayed(this, 16); + } + } + }; + + public WheelPicker(Context context) { + this(context, null); + } + + public WheelPicker(Context context, AttributeSet attrs) { + super(context, attrs); + adapter = new Adapter(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WheelPicker); + + mItemTextSize = a.getDimensionPixelSize(R.styleable.WheelPicker_wheel_item_text_size, + getResources().getDimensionPixelSize(R.dimen.WheelItemTextSize)); + mVisibleItemCount = a.getInt(R.styleable.WheelPicker_wheel_visible_item_count, 7); + selectedItemPosition = a.getInt(R.styleable.WheelPicker_wheel_selected_item_position, 0); + hasSameWidth = a.getBoolean(R.styleable.WheelPicker_wheel_same_width, false); + textMaxWidthPosition = a.getInt(R.styleable.WheelPicker_wheel_maximum_width_text_position, -1); + maxWidthText = a.getString(R.styleable.WheelPicker_wheel_maximum_width_text); + mSelectedItemTextColor = a.getColor(R.styleable.WheelPicker_wheel_selected_item_text_color, -1); + mItemTextColor = a.getColor(R.styleable.WheelPicker_wheel_item_text_color, 0xFF888888); + mItemSpace = a.getDimensionPixelSize(R.styleable.WheelPicker_wheel_item_space, + getResources().getDimensionPixelSize(R.dimen.WheelItemSpace)); + isCyclic = a.getBoolean(R.styleable.WheelPicker_wheel_cyclic, false); + hasIndicator = a.getBoolean(R.styleable.WheelPicker_wheel_indicator, false); + mIndicatorColor = a.getColor(R.styleable.WheelPicker_wheel_indicator_color, 0xFFEE3333); + mIndicatorSize = a.getDimensionPixelSize(R.styleable.WheelPicker_wheel_indicator_size, + getResources().getDimensionPixelSize(R.dimen.WheelIndicatorSize)); + hasCurtain = a.getBoolean(R.styleable.WheelPicker_wheel_curtain, false); + mCurtainColor = a.getColor(R.styleable.WheelPicker_wheel_curtain_color, 0x88FFFFFF); + hasAtmospheric = a.getBoolean(R.styleable.WheelPicker_wheel_atmospheric, false); + isCurved = a.getBoolean(R.styleable.WheelPicker_wheel_curved, false); + mItemAlign = a.getInt(R.styleable.WheelPicker_wheel_item_align, ALIGN_CENTER); + a.recycle(); + + updateVisibleItemCount(); + + paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG); + paint.setTextSize(mItemTextSize); + + updateItemTextAlign(); + + computeTextSize(); + + scroller = new Scroller(getContext()); + + ViewConfiguration conf = ViewConfiguration.get(getContext()); + minimumVelocity = conf.getScaledMinimumFlingVelocity(); + maximumVelocity = conf.getScaledMaximumFlingVelocity(); + touchSlop = conf.getScaledTouchSlop(); + rectDrawn = new Rect(); + + rectIndicatorHead = new Rect(); + rectIndicatorFoot = new Rect(); + + rectCurrentItem = new Rect(); + + camera = new Camera(); + + matrixRotate = new Matrix(); + matrixDepth = new Matrix(); + } + + private void updateVisibleItemCount() { + if (mVisibleItemCount < 2) { + throw new ArithmeticException("Wheel's visible item count can not be less than 2!"); + } + + if (mVisibleItemCount % 2 == 0) mVisibleItemCount += 1; + mDrawnItemCount = mVisibleItemCount + 2; + mHalfDrawnItemCount = mDrawnItemCount / 2; + } + + private void computeTextSize() { + mTextMaxWidth = mTextMaxHeight = 0; + if (hasSameWidth) { + mTextMaxWidth = (int) paint.measureText(adapter.getItemText(0)); + } else if (isPosInRang(textMaxWidthPosition)) { + mTextMaxWidth = (int) paint.measureText(adapter.getItemText(textMaxWidthPosition)); + } else if (!TextUtils.isEmpty(maxWidthText)) { + mTextMaxWidth = (int) paint.measureText(maxWidthText); + } else { + final int itemCount = adapter.getItemCount(); + for (int i = 0; i < itemCount; ++i) { + String text = adapter.getItemText(i); + int width = (int) paint.measureText(text); + mTextMaxWidth = Math.max(mTextMaxWidth, width); + } + } + Paint.FontMetrics metrics = paint.getFontMetrics(); + mTextMaxHeight = (int) (metrics.bottom - metrics.top); + } + + private void updateItemTextAlign() { + switch (mItemAlign) { + case ALIGN_LEFT: + paint.setTextAlign(Paint.Align.LEFT); + break; + case ALIGN_RIGHT: + paint.setTextAlign(Paint.Align.RIGHT); + break; + default: + paint.setTextAlign(Paint.Align.CENTER); + break; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int modeWidth = MeasureSpec.getMode(widthMeasureSpec); + int modeHeight = MeasureSpec.getMode(heightMeasureSpec); + + int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); + int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); + + // Correct sizes of original content + int resultWidth = mTextMaxWidth; + int resultHeight = mTextMaxHeight * mVisibleItemCount + mItemSpace * (mVisibleItemCount - 1); + + // Correct view sizes again if curved is enable + if (isCurved) { + resultHeight = (int) (2 * resultHeight / Math.PI); + } + + // Consideration padding influence the view sizes + resultWidth += getPaddingLeft() + getPaddingRight(); + resultHeight += getPaddingTop() + getPaddingBottom(); + + // Consideration sizes of parent can influence the view sizes + resultWidth = measureSize(modeWidth, sizeWidth, resultWidth); + resultHeight = measureSize(modeHeight, sizeHeight, resultHeight); + + setMeasuredDimension(resultWidth, resultHeight); + } + + private int measureSize(int mode, int sizeExpect, int sizeActual) { + int realSize; + if (mode == MeasureSpec.EXACTLY) { + realSize = sizeExpect; + } else { + realSize = sizeActual; + if (mode == MeasureSpec.AT_MOST) realSize = Math.min(realSize, sizeExpect); + } + return realSize; + } + + @Override + protected void onSizeChanged(int w, int h, int oldW, int oldH) { + // Set content region + rectDrawn.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), + getHeight() - getPaddingBottom()); + + // Get the center coordinates of content region + wheelCenterX = rectDrawn.centerX(); + wheelCenterY = rectDrawn.centerY(); + + // Correct item drawn center + computeDrawnCenter(); + + mHalfWheelHeight = rectDrawn.height() / 2; + + mItemHeight = rectDrawn.height() / mVisibleItemCount; + mHalfItemHeight = mItemHeight / 2; + + // Initialize fling max Y-coordinates + computeFlingLimitY(); + + // Correct region of indicator + computeIndicatorRect(); + + // Correct region of current select item + computeCurrentItemRect(); + } + + private void computeDrawnCenter() { + switch (mItemAlign) { + case ALIGN_LEFT: + drawnCenterX = rectDrawn.left; + break; + case ALIGN_RIGHT: + drawnCenterX = rectDrawn.right; + break; + default: + drawnCenterX = wheelCenterX; + break; + } + drawnCenterY = (int) (wheelCenterY - ((paint.ascent() + paint.descent()) / 2)); + } + + private void computeFlingLimitY() { + int currentItemOffset = selectedItemPosition * mItemHeight; + minFlingY = isCyclic ? Integer.MIN_VALUE + : -mItemHeight * (adapter.getItemCount() - 1) + currentItemOffset; + maxFlingY = isCyclic ? Integer.MAX_VALUE : currentItemOffset; + } + + private void computeIndicatorRect() { + if (!hasIndicator) return; + int halfIndicatorSize = mIndicatorSize / 2; + int indicatorHeadCenterY = wheelCenterY + mHalfItemHeight; + int indicatorFootCenterY = wheelCenterY - mHalfItemHeight; + rectIndicatorHead.set(rectDrawn.left, indicatorHeadCenterY - halfIndicatorSize, rectDrawn.right, + indicatorHeadCenterY + halfIndicatorSize); + rectIndicatorFoot.set(rectDrawn.left, indicatorFootCenterY - halfIndicatorSize, rectDrawn.right, + indicatorFootCenterY + halfIndicatorSize); + } + + private void computeCurrentItemRect() { + if (!hasCurtain && mSelectedItemTextColor == -1) return; + rectCurrentItem.set(rectDrawn.left, wheelCenterY - mHalfItemHeight, rectDrawn.right, + wheelCenterY + mHalfItemHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + if (null != onWheelChangeListener) onWheelChangeListener.onWheelScrolled(scrollOffsetY); + if (mItemHeight - mHalfDrawnItemCount <= 0) + return; + int drawnDataStartPos = -scrollOffsetY / mItemHeight - mHalfDrawnItemCount; + for (int drawnDataPos = drawnDataStartPos + selectedItemPosition, + drawnOffsetPos = -mHalfDrawnItemCount; + drawnDataPos < drawnDataStartPos + selectedItemPosition + mDrawnItemCount; + drawnDataPos++, drawnOffsetPos++) { + String data = ""; + if (isCyclic) { + final int itemCount = this.adapter.getItemCount(); + int actualPos = drawnDataPos % itemCount; + actualPos = actualPos < 0 ? (actualPos + itemCount) : actualPos; + data = adapter.getItemText(actualPos); + } else { + if (isPosInRang(drawnDataPos)) data = adapter.getItemText(drawnDataPos); + } + paint.setColor(mItemTextColor); + paint.setStyle(Paint.Style.FILL); + int mDrawnItemCenterY = drawnCenterY + (drawnOffsetPos * mItemHeight) + + scrollOffsetY % mItemHeight; + + int distanceToCenter = 0; + if (isCurved) { + // Correct ratio of item's drawn center to wheel center + float ratio = (drawnCenterY - Math.abs(drawnCenterY - mDrawnItemCenterY) - + rectDrawn.top) * 1.0F / (drawnCenterY - rectDrawn.top); + + // Correct unit + int unit = 0; + if (mDrawnItemCenterY > drawnCenterY) { + unit = 1; + } else if (mDrawnItemCenterY < drawnCenterY) unit = -1; + + float degree = (-(1 - ratio) * 90 * unit); + if (degree < -90) degree = -90; + if (degree > 90) degree = 90; + distanceToCenter = computeSpace((int) degree); + + int transX = wheelCenterX; + switch (mItemAlign) { + case ALIGN_LEFT: + transX = rectDrawn.left; + break; + case ALIGN_RIGHT: + transX = rectDrawn.right; + break; + } + int transY = wheelCenterY - distanceToCenter; + + camera.save(); + camera.rotateX(degree); + camera.getMatrix(matrixRotate); + camera.restore(); + matrixRotate.preTranslate(-transX, -transY); + matrixRotate.postTranslate(transX, transY); + + camera.save(); + camera.translate(0, 0, computeDepth((int) degree)); + camera.getMatrix(matrixDepth); + camera.restore(); + matrixDepth.preTranslate(-transX, -transY); + matrixDepth.postTranslate(transX, transY); + + matrixRotate.postConcat(matrixDepth); + } + if (hasAtmospheric) { + int alpha = + (int) ((drawnCenterY - Math.abs(drawnCenterY - mDrawnItemCenterY)) * 1.0F / drawnCenterY + * 255); + alpha = alpha < 0 ? 0 : alpha; + paint.setAlpha(alpha); + } + // Correct item's drawn centerY base on curved state + int drawnCenterY = isCurved ? this.drawnCenterY - distanceToCenter : mDrawnItemCenterY; + + // Judges need to draw different color for current item or not + if (mSelectedItemTextColor != -1) { + canvas.save(); + if (isCurved) canvas.concat(matrixRotate); + canvas.clipRect(rectCurrentItem, Region.Op.DIFFERENCE); + canvas.drawText(data, drawnCenterX, drawnCenterY, paint); + canvas.restore(); + + paint.setColor(mSelectedItemTextColor); + canvas.save(); + if (isCurved) canvas.concat(matrixRotate); + canvas.clipRect(rectCurrentItem); + canvas.drawText(data, drawnCenterX, drawnCenterY, paint); + canvas.restore(); + } else { + canvas.save(); + canvas.clipRect(rectDrawn); + if (isCurved) canvas.concat(matrixRotate); + canvas.drawText(data, drawnCenterX, drawnCenterY, paint); + canvas.restore(); + } + } + // Need to draw curtain or not + if (hasCurtain) { + paint.setColor(mCurtainColor); + paint.setStyle(Paint.Style.FILL); + canvas.drawRect(rectCurrentItem, paint); + } + // Need to draw indicator or not + if (hasIndicator) { + paint.setColor(mIndicatorColor); + paint.setStyle(Paint.Style.FILL); + canvas.drawRect(rectIndicatorHead, paint); + canvas.drawRect(rectIndicatorFoot, paint); + } + } + + private boolean isPosInRang(int position) { + return position >= 0 && position < adapter.getItemCount(); + } + + private int computeSpace(int degree) { + return (int) (Math.sin(Math.toRadians(degree)) * mHalfWheelHeight); + } + + private int computeDepth(int degree) { + return (int) (mHalfWheelHeight - Math.cos(Math.toRadians(degree)) * mHalfWheelHeight); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (isEnabled()) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (null != getParent()) getParent().requestDisallowInterceptTouchEvent(true); + if (null == tracker) { + tracker = VelocityTracker.obtain(); + } else { + tracker.clear(); + } + tracker.addMovement(event); + if (!scroller.isFinished()) { + scroller.abortAnimation(); + isForceFinishScroll = true; + } + downPointY = lastPointY = (int) event.getY(); + break; + case MotionEvent.ACTION_MOVE: + if (Math.abs(downPointY - event.getY()) < touchSlop) { + isClick = true; + break; + } + isClick = false; + tracker.addMovement(event); + if (null != onWheelChangeListener) { + onWheelChangeListener.onWheelScrollStateChanged(SCROLL_STATE_DRAGGING); + } + + // Scroll WheelPicker's content + float move = event.getY() - lastPointY; + if (Math.abs(move) < 1) break; + scrollOffsetY += move; + lastPointY = (int) event.getY(); + invalidate(); + break; + case MotionEvent.ACTION_UP: + if (null != getParent()) getParent().requestDisallowInterceptTouchEvent(false); + if (isClick) break; + tracker.addMovement(event); + + tracker.computeCurrentVelocity(1000, maximumVelocity); + + // Judges the WheelPicker is scroll or fling base on current velocity + isForceFinishScroll = false; + int velocity = (int) tracker.getYVelocity(); + if (Math.abs(velocity) > minimumVelocity) { + scroller.fling(0, scrollOffsetY, 0, velocity, 0, 0, minFlingY, maxFlingY); + scroller.setFinalY( + scroller.getFinalY() + computeDistanceToEndPoint(scroller.getFinalY() % mItemHeight)); + } else { + scroller.startScroll(0, scrollOffsetY, 0, + computeDistanceToEndPoint(scrollOffsetY % mItemHeight)); + } + // Correct coordinates + if (!isCyclic) { + if (scroller.getFinalY() > maxFlingY) { + scroller.setFinalY(maxFlingY); + } else if (scroller.getFinalY() < minFlingY) scroller.setFinalY(minFlingY); + } + handler.post(runnable); + if (null != tracker) { + tracker.recycle(); + tracker = null; + } + break; + case MotionEvent.ACTION_CANCEL: + if (null != getParent()) getParent().requestDisallowInterceptTouchEvent(false); + if (null != tracker) { + tracker.recycle(); + tracker = null; + } + break; + } + } + return true; + } + + private int computeDistanceToEndPoint(int remainder) { + if (Math.abs(remainder) > mHalfItemHeight) { + if (scrollOffsetY < 0) { + return -mItemHeight - remainder; + } else { + return mItemHeight - remainder; + } + } else { + return -remainder; + } + } + + public void scrollTo(final int itemPosition) { + if (itemPosition != currentItemPosition) { + final int differencesLines = currentItemPosition - itemPosition; + final int newScrollOffsetY = + scrollOffsetY + (differencesLines * mItemHeight); // % adapter.getItemCount(); + + ValueAnimator va = ValueAnimator.ofInt(scrollOffsetY, newScrollOffsetY); + va.setDuration(300); + va.addUpdateListener(animation -> { + scrollOffsetY = (int) animation.getAnimatedValue(); + invalidate(); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + currentItemPosition = itemPosition; + onItemSelected(); + } + }); + va.start(); + } + } + + private void onItemSelected() { + int position = currentItemPosition; + final Object item = this.adapter.getItem(position); + if (null != onItemSelectedListener) { + onItemSelectedListener.onItemSelected(this, item, position); + } + onItemSelected(position, item); + } + + protected abstract void onItemSelected(int position, Object item); + + protected abstract void onItemCurrentScroll(int position, Object item); + + protected abstract String getFormattedValue(Object value); + + public int getVisibleItemCount() { + return mVisibleItemCount; + } + + public void setVisibleItemCount(int count) { + mVisibleItemCount = count; + updateVisibleItemCount(); + requestLayout(); + } + + public boolean isCyclic() { + return isCyclic; + } + + public void setCyclic(boolean isCyclic) { + this.isCyclic = isCyclic; + computeFlingLimitY(); + invalidate(); + } + + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + onItemSelectedListener = listener; + } + + public int getSelectedItemPosition() { + return selectedItemPosition; + } + + public void setSelectedItemPosition(int position) { + position = Math.min(position, adapter.getItemCount() - 1); + position = Math.max(position, 0); + selectedItemPosition = position; + currentItemPosition = position; + scrollOffsetY = 0; + computeFlingLimitY(); + requestLayout(); + invalidate(); + } + + public int getCurrentItemPosition() { + return currentItemPosition; + } + + public abstract int getDefaultItemPosition(); + + public void setAdapter(Adapter adapter) { + this.adapter = adapter; + notifyDatasetChanged(); + } + + public void notifyDatasetChanged() { + if (selectedItemPosition > adapter.getItemCount() - 1 + || currentItemPosition > adapter.getItemCount() - 1) { + selectedItemPosition = currentItemPosition = adapter.getItemCount() - 1; + } else { + selectedItemPosition = currentItemPosition; + } + scrollOffsetY = 0; + computeTextSize(); + computeFlingLimitY(); + requestLayout(); + invalidate(); + } + + public void setSameWidth(boolean hasSameWidth) { + this.hasSameWidth = hasSameWidth; + computeTextSize(); + requestLayout(); + invalidate(); + } + + public boolean hasSameWidth() { + return hasSameWidth; + } + + public void setOnWheelChangeListener(OnWheelChangeListener listener) { + onWheelChangeListener = listener; + } + + public String getMaximumWidthText() { + return maxWidthText; + } + + public void setMaximumWidthText(String text) { + if (null == text) throw new NullPointerException("Maximum width text can not be null!"); + maxWidthText = text; + computeTextSize(); + requestLayout(); + invalidate(); + } + + public int getMaximumWidthTextPosition() { + return textMaxWidthPosition; + } + + public void setMaximumWidthTextPosition(int position) { + if (!isPosInRang(position)) { + throw new ArrayIndexOutOfBoundsException("Maximum width text Position must in [0, " + + adapter.getItemCount() + "), but current is " + position); + } + textMaxWidthPosition = position; + computeTextSize(); + requestLayout(); + invalidate(); + } + + public int getSelectedItemTextColor() { + return mSelectedItemTextColor; + } + + public void setSelectedItemTextColor(int color) { + mSelectedItemTextColor = color; + computeCurrentItemRect(); + invalidate(); + } + + public int getItemTextColor() { + return mItemTextColor; + } + + public void setItemTextColor(int color) { + mItemTextColor = color; + invalidate(); + } + + public int getItemTextSize() { + return mItemTextSize; + } + + public void setItemTextSize(int size) { + + if (mItemTextSize != size) { + mItemTextSize = size; + paint.setTextSize(mItemTextSize); + computeTextSize(); + requestLayout(); + invalidate(); + } + } + + public int getItemSpace() { + return mItemSpace; + } + + public void setItemSpace(int space) { + mItemSpace = space; + requestLayout(); + invalidate(); + } + + public void setIndicator(boolean hasIndicator) { + this.hasIndicator = hasIndicator; + computeIndicatorRect(); + invalidate(); + } + + public boolean hasIndicator() { + return hasIndicator; + } + + public int getIndicatorSize() { + return mIndicatorSize; + } + + public void setIndicatorSize(int size) { + mIndicatorSize = size; + computeIndicatorRect(); + invalidate(); + } + + public int getIndicatorColor() { + return mIndicatorColor; + } + + public void setIndicatorColor(int color) { + mIndicatorColor = color; + invalidate(); + } + + public void setCurtain(boolean hasCurtain) { + this.hasCurtain = hasCurtain; + computeCurrentItemRect(); + invalidate(); + } + + public boolean hasCurtain() { + return hasCurtain; + } + + public int getCurtainColor() { + return mCurtainColor; + } + + public void setCurtainColor(int color) { + mCurtainColor = color; + invalidate(); + } + + public void setAtmospheric(boolean hasAtmospheric) { + this.hasAtmospheric = hasAtmospheric; + invalidate(); + } + + public boolean hasAtmospheric() { + return hasAtmospheric; + } + + public boolean isCurved() { + return isCurved; + } + + public void setCurved(boolean isCurved) { + this.isCurved = isCurved; + requestLayout(); + invalidate(); + } + + public int getItemAlign() { + return mItemAlign; + } + + public void setItemAlign(int align) { + mItemAlign = align; + updateItemTextAlign(); + computeDrawnCenter(); + invalidate(); + } + + public Typeface getTypeface() { + if (null != paint) return paint.getTypeface(); + return null; + } + + public void setTypeface(Typeface tf) { + if (null != paint) paint.setTypeface(tf); + computeTextSize(); + requestLayout(); + invalidate(); + } + + /** + * a String: displayedValue (the value to be displayed in the wheel) and + * a Date/Calendar: comparisonDate (a reference date/calendar that will help to find the index). + * This could clean this method and {@link #getFormattedValue(Object)}. + *

+ * Finds the index in the wheel for a date + * + * @param date the targeted date + * @return the index closed to {@code date}. Returns 0 if not found. + */ + public int findIndexOfDate(@NonNull Date date) { + String formatItem = getFormattedValue(date); + + if (this instanceof WheelDayPicker) { + String today = getFormattedValue(new Date()); + if (today.equals(formatItem)) { + return getDefaultItemPosition(); + } + } + + int formatItemInt = Integer.MIN_VALUE; + try { + formatItemInt = Integer.parseInt(formatItem); + } catch (NumberFormatException ignored) { + } + + final int itemCount = adapter.getItemCount(); + int index = 0; + for (int i = 0; i < itemCount; ++i) { + final String object = adapter.getItemText(i); + + if (formatItemInt != Integer.MIN_VALUE) { + // displayed values are Integers + int objectInt = Integer.parseInt(object); + if (this instanceof WheelHourPicker && ((WheelHourPicker) this).isAmPm) { + // In case of hours and AM/PM mode, apply modulo 12 + objectInt = objectInt % 12; + } + if (objectInt <= formatItemInt) { + index = i; + } + } else if (formatItem.equals(object)) { + return i; + } + } + return index; + } + + @TargetApi(Build.VERSION_CODES.N) + public Locale getCurrentLocale() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getResources().getConfiguration().getLocales().get(0); + } else { + //noinspection deprecation + return getResources().getConfiguration().locale; + } + } + + public interface BaseAdapter { + + int getItemCount(); + + Object getItem(int position); + + String getItemText(int position); + } + + public interface OnItemSelectedListener { + void onItemSelected(WheelPicker picker, Object data, int position); + + void onCurrentItemOfScroll(WheelPicker picker, int position); + } + + public interface OnWheelChangeListener { + /** + *

+ * Invoke when WheelPicker scroll stopped + * WheelPicker will return a distance offset which between current scroll position and + * initial position, this offset is a positive or a negative, positive means WheelPicker is + * scrolling from bottom to top, negative means WheelPicker is scrolling from top to bottom + * + * @param offset

+ * Distance offset which between current scroll position and initial position + */ + void onWheelScrolled(int offset); + + /** + *

+ * Invoke when WheelPicker scroll stopped + * This method will be called when WheelPicker stop and return current selected item data's + * position in list + * + * @param position

+ * Current selected item data's position in list + */ + void onWheelSelected(int position); + + /** + *

+ * Invoke when WheelPicker's scroll state changed + * The state of WheelPicker always between idle, dragging, and scrolling, this method will + * be called when they switch + * + * @param state {@link WheelPicker#SCROLL_STATE_IDLE} + * {@link WheelPicker#SCROLL_STATE_DRAGGING} + * {@link WheelPicker#SCROLL_STATE_SCROLLING} + *

+ * State of WheelPicker, only one of the following + * {@link WheelPicker#SCROLL_STATE_IDLE} + * Express WheelPicker in state of idle + * {@link WheelPicker#SCROLL_STATE_DRAGGING} + * Express WheelPicker in state of dragging + * {@link WheelPicker#SCROLL_STATE_SCROLLING} + * Express WheelPicker in state of scrolling + */ + void onWheelScrollStateChanged(int state); + } + + public static class Adapter implements BaseAdapter { + private List data; + + Adapter() { + this(new ArrayList()); + } + + Adapter(List data) { + this.data = new ArrayList(); + this.data.addAll(data); + } + + @Override + public int getItemCount() { + return data.size(); + } + + @Override + public Object getItem(int position) { + final int itemCount = getItemCount(); + return data.get((position + itemCount) % itemCount); + } + + @Override + public String getItemText(int position) { + return String.valueOf(data.get(position)); + } + + public void setData(List data) { + this.data.clear(); + this.data.addAll(data); + } + + public List getData() { + return data; + } + + public void addData(List data) { + this.data.addAll(data); + } + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/utils/ItemClickSupport.java b/app/src/main/java/com/nynja/mobile/communicator/utils/ItemClickSupport.java new file mode 100755 index 0000000000000000000000000000000000000000..3f8bba52b6208aa373261a60730a45edb271fda3 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/utils/ItemClickSupport.java @@ -0,0 +1,95 @@ +package com.nynja.mobile.communicator.utils; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.nynja.mobile.communicator.R; + + +public class ItemClickSupport { + private final RecyclerView mRecyclerView; + private OnItemClickListener mOnItemClickListener; + private OnItemLongClickListener mOnItemLongClickListener; + private View.OnClickListener mOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mOnItemClickListener != null) { + RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); + mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v); + } + } + }; + private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (mOnItemLongClickListener != null) { + RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); + return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v); + } + return false; + } + }; + private RecyclerView.OnChildAttachStateChangeListener mAttachListener = new RecyclerView.OnChildAttachStateChangeListener() { + @Override + public void onChildViewAttachedToWindow(View view) { + if (mOnItemClickListener != null) { + view.setOnClickListener(mOnClickListener); + } + if (mOnItemLongClickListener != null) { + view.setOnLongClickListener(mOnLongClickListener); + } + } + + @Override + public void onChildViewDetachedFromWindow(View view) { + + } + }; + + private ItemClickSupport(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + mRecyclerView.setTag(R.id.item_click_support, this); + mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener); + } + + public static ItemClickSupport addTo(RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support == null) { + support = new ItemClickSupport(view); + } + return support; + } + + public static ItemClickSupport removeFrom(RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support != null) { + support.detach(view); + } + return support; + } + + public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) { + mOnItemClickListener = listener; + return this; + } + + public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) { + mOnItemLongClickListener = listener; + return this; + } + + private void detach(RecyclerView view) { + view.removeOnChildAttachStateChangeListener(mAttachListener); + view.setTag(R.id.item_click_support, null); + } + + public interface OnItemClickListener { + + void onItemClicked(RecyclerView recyclerView, int position, View v); + } + + public interface OnItemLongClickListener { + + boolean onItemLongClicked(RecyclerView recyclerView, int position, View v); + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/utils/OnSwipeListener.java b/app/src/main/java/com/nynja/mobile/communicator/utils/OnSwipeListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d54ad49e35548de6066935f1198a61284265c4cf --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/utils/OnSwipeListener.java @@ -0,0 +1,122 @@ +package com.nynja.mobile.communicator.utils; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +public class OnSwipeListener extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + // Grab two events located on the plane at e1=(x1, y1) and e2=(x2, y2) + // Let e1 be the initial event + // e2 can be located at 4 different positions, consider the following diagram + // (Assume that lines are separated by 90 degrees.) + // + // + // \ A / + // \ / + // D e1 B + // / \ + // / C \ + // + // So if (x2,y2) falls in region: + // A => it's an UP swipe + // B => it's a RIGHT swipe + // C => it's a DOWN swipe + // D => it's a LEFT swipe + // + + float x1 = e1.getX(); + float y1 = e1.getY(); + + float x2 = e2.getX(); + float y2 = e2.getY(); + + Direction direction = getDirection(x1,y1,x2,y2); + return onSwipe(direction); + } + + /** Override this method. The Direction enum will tell you how the user swiped. */ + public boolean onSwipe(Direction direction){ + return false; + } + + /** + * Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method + * returns the direction that an arrow pointing from p1 to p2 would have. + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the direction + */ + public Direction getDirection(float x1, float y1, float x2, float y2){ + double angle = getAngle(x1, y1, x2, y2); + return Direction.get(angle); + } + + /** + * + * Finds the angle between two points in the plane (x1,y1) and (x2, y2) + * The angle is measured with 0/360 being the X-axis to the right, angles + * increase counter clockwise. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the angle between two points + */ + public double getAngle(float x1, float y1, float x2, float y2) { + + double rad = Math.atan2(y1-y2,x2-x1) + Math.PI; + return (rad*180/Math.PI + 180)%360; + } + + + public enum Direction{ + up, + down, + left, + right; + + /** + * Returns a direction given an angle. + * Directions are defined as follows: + * + * Up: [45, 135] + * Right: [0,45] and [315, 360] + * Down: [225, 315] + * Left: [135, 225] + * + * @param angle an angle from 0 to 360 - e + * @return the direction of an angle + */ + public static Direction get(double angle){ + if(inRange(angle, 45, 135)){ + return Direction.up; + } + else if(inRange(angle, 0,45) || inRange(angle, 315, 360)){ + return Direction.right; + } + else if(inRange(angle, 225, 315)){ + return Direction.down; + } + else{ + return Direction.left; + } + + } + + /** + * @param angle an angle + * @param init the initial bound + * @param end the final bound + * @return returns true if the given angle is in the interval [init, end). + */ + private static boolean inRange(double angle, float init, float end){ + return (angle >= init) && (angle < end); + } + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/utils/TimeZoneData.java b/app/src/main/java/com/nynja/mobile/communicator/utils/TimeZoneData.java new file mode 100644 index 0000000000000000000000000000000000000000..2c4ba90817772d674b138c7e10cf9b30da84b0d0 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/utils/TimeZoneData.java @@ -0,0 +1,60 @@ +package com.nynja.mobile.communicator.utils; + + +import java.util.ArrayList; + +/** + * Created by pavel on 11/15/17. + */ + +public class TimeZoneData { + + private String value; + private String abbr; + private double offset; + private boolean isdst; + private String text; + private String city; + private ArrayList utc; + + public TimeZoneData() { + } + + public TimeZoneData(String value, String abbr, double offset, boolean isdst, String text, String city, ArrayList utc) { + this.value = value; + this.abbr = abbr; + this.offset = offset; + this.isdst = isdst; + this.text = text; + this.city = city; + this.utc = utc; + } + + public String getValue() { + return value; + } + + public String getAbbr() { + return abbr; + } + + public double getOffset() { + return offset; + } + + public boolean isIsdst() { + return isdst; + } + + public String getText() { + return text; + } + + public ArrayList getUtc() { + return utc; + } + + public String getCity() { + return city; + } +} diff --git a/app/src/main/java/com/nynja/mobile/communicator/utils/TimeZoneUtil.java b/app/src/main/java/com/nynja/mobile/communicator/utils/TimeZoneUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..71b50d310916ecf8ba33e0070dac919d76f4ebd1 --- /dev/null +++ b/app/src/main/java/com/nynja/mobile/communicator/utils/TimeZoneUtil.java @@ -0,0 +1,65 @@ +package com.nynja.mobile.communicator.utils; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; + +public class TimeZoneUtil { + + public static List parseTimeZonesFromJson(Context context) { + List cityTimeZoneList = new ArrayList<>(); + Type listType = new TypeToken>() { + }.getType(); + List timeZoneList = new Gson().fromJson(loadJSONFromAsset(context), listType); + for (int i = 0; i < timeZoneList.size(); i++) { + for (int j = 0; j < timeZoneList.get(i).getUtc().size(); j++) { + cityTimeZoneList.add(new TimeZoneData(timeZoneList.get(i).getValue(), + timeZoneList.get(i).getAbbr(), + timeZoneList.get(i).getOffset(), + timeZoneList.get(i).isIsdst(), + timeZoneList.get(i).getText(), + getCityFromFullString(timeZoneList.get(i).getUtc().get(j)), + null)); + } + } + + return cityTimeZoneList; + } + + public static double getOffset() { + TimeZone timezone = TimeZone.getDefault(); + int seconds = timezone.getOffset(Calendar.ZONE_OFFSET) / 1000; + double minutes = seconds / 60; + return minutes / 60; + } + + private static String loadJSONFromAsset(Context context) { + String json; + try { + InputStream is = context.getAssets().open("timezones.json"); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + json = new String(buffer, "UTF-8"); + } catch (IOException ex) { + ex.printStackTrace(); + return null; + } + return json; + } + + public static String getCityFromFullString(String origin) { + String[] full = origin.split("/"); + return (full.length > 1) ? full[1] : full[0]; + } +} diff --git a/app/src/main/res/drawable/scroller_bagground.xml b/app/src/main/res/drawable/scroller_bagground.xml new file mode 100644 index 0000000000000000000000000000000000000000..0bb4f3f5f279465b53d8a73265e634cdd20801ac --- /dev/null +++ b/app/src/main/res/drawable/scroller_bagground.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/search_bg.xml b/app/src/main/res/drawable/search_bg.xml new file mode 100644 index 0000000000000000000000000000000000000000..0201822748cb0a410162394cfe9e8c7bdada80fd --- /dev/null +++ b/app/src/main/res/drawable/search_bg.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/section_indicator_background_default_rounded.xml b/app/src/main/res/drawable/section_indicator_background_default_rounded.xml new file mode 100644 index 0000000000000000000000000000000000000000..41d520de4935d6586b718d855a31e73071a2bb95 --- /dev/null +++ b/app/src/main/res/drawable/section_indicator_background_default_rounded.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/time_zone_search_shape.xml b/app/src/main/res/drawable/time_zone_search_shape.xml new file mode 100644 index 0000000000000000000000000000000000000000..015474249627e23ce88799312d705cf663a477e5 --- /dev/null +++ b/app/src/main/res/drawable/time_zone_search_shape.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_calendar_layout.xml b/app/src/main/res/layout/custom_calendar_layout.xml new file mode 100644 index 0000000000000000000000000000000000000000..816e0d13a0bcdd09624a5ef2ef5a2c01bf802960 --- /dev/null +++ b/app/src/main/res/layout/custom_calendar_layout.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_calendar_weeks_layout.xml b/app/src/main/res/layout/custom_calendar_weeks_layout.xml new file mode 100644 index 0000000000000000000000000000000000000000..468f18fdb3dafc62eb86b8ecdf07835f843b449e --- /dev/null +++ b/app/src/main/res/layout/custom_calendar_weeks_layout.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_view_more_send_message.xml b/app/src/main/res/layout/custom_view_more_send_message.xml new file mode 100644 index 0000000000000000000000000000000000000000..387bfa7de706dba251463988670bf21b26c3a31d --- /dev/null +++ b/app/src/main/res/layout/custom_view_more_send_message.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml index a6cc58deacc811ff7b2be1ed1d2918a9f25cce80..6d017b2cd172d2ec996edc47c20d7569f1f64bfe 100644 --- a/app/src/main/res/layout/fragment_chat.xml +++ b/app/src/main/res/layout/fragment_chat.xml @@ -1,8 +1,8 @@ - @@ -53,10 +53,10 @@ tools:ignore="ContentDescription" /> + android:layout_marginEnd="40dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +