SDF_THREAD_LOCAL_DAY = new ThreadLocal<>();
+
+ //获取日期的时间格式
+ private static SimpleDateFormat getDayFormat() {
+ SimpleDateFormat simpleDayDateFormat = SDF_THREAD_LOCAL_DAY.get();
+ if (simpleDayDateFormat == null) {
+ simpleDayDateFormat = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
+ SDF_THREAD_LOCAL_DAY.set(simpleDayDateFormat);
+ }
+ return simpleDayDateFormat;
+ }
+
+ private static SimpleDateFormat getDefaultFormat() {
+ SimpleDateFormat simpleDateFormat = SDF_THREAD_LOCAL.get();
+ if (simpleDateFormat == null) {
+ simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm", Locale.getDefault());
+ SDF_THREAD_LOCAL.set(simpleDateFormat);
+ }
+ return simpleDateFormat;
+ }
+
+ private TimeUtils() {
+ throw new UnsupportedOperationException("u can't instantiate me...");
+ }
+
+ /**
+ * 从时间(毫秒)中提取出时间(时:分)
+ * 时间格式: 时:分
+ */
+ public static String timeParse(Long duration) {
+ String time = "" ;
+ long minute = duration / 60000 ;
+ long seconds = duration % 60000 ;
+ long second = Math.round((float)seconds/1000) ;
+ if( minute < 10 ){
+ time += "0" ;
+ }
+ time += minute+":" ;
+ if( second < 10 ){
+ time += "0" ;
+ }
+ time += second ;
+ return time ;
+ }
+
+ /**
+ * Milliseconds to the formatted time string.
+ * 将时间戳转为时间字符串
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param millis The milliseconds.
+ * @return the formatted time string
+ */
+ public static String millis2String(final long millis) {
+ return millis2String(millis, getDefaultFormat());
+ }
+
+ /**
+ * Milliseconds to the formatted time string.
+ * 将时间戳转为时间字符串
+ * @param millis The milliseconds.
+ * @param format The format.
+ * @return the formatted time string
+ */
+ public static String millis2String(final long millis, @NonNull final DateFormat format) {
+ return format.format(new Date(millis));
+ }
+
+ /**
+ * Formatted time string to the milliseconds.
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ * 将时间字符串转为时间戳
+ * @param time The formatted time string.
+ * @return the milliseconds
+ */
+ public static long string2Millis(final String time) {
+ return string2Millis(time, getDefaultFormat());
+ }
+
+ /**
+ * Formatted time string to the milliseconds.
+ * 将时间字符串转为时间戳
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the milliseconds
+ */
+ public static long string2Millis(final String time, @NonNull final DateFormat format) {
+ try {
+ return format.parse(time).getTime();
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
+ /**
+ * Formatted time string to the date.
+ * 将时间字符串转为 Date 类型
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return the date
+ */
+ public static Date string2Date(final String time) {
+ return string2Date(time, getDefaultFormat());
+ }
+
+ /**
+ * Formatted time string to the date.
+ * 将时间字符串转为 Date 类型
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the date
+ */
+ public static Date string2Date(final String time, @NonNull final DateFormat format) {
+ try {
+ return format.parse(time);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ * Date to the formatted time string.
+ * 将 Date 类型转为时间字符串
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param date The date.
+ * @return the formatted time string
+ */
+ public static String date2String(final Date date) {
+ return date2String(date, getDefaultFormat());
+ }
+
+ /**
+ * Date to the formatted time string.
+ * 将 Date 类型转为时间字符串
+ * @param date The date.
+ * @param format The format.
+ * @return the formatted time string
+ */
+ public static String date2String(final Date date, @NonNull final DateFormat format) {
+ return format.format(date);
+ }
+
+ /**
+ * Date to the milliseconds.
+ * 将 Date 类型转为时间戳
+ * @param date The date.
+ * @return the milliseconds
+ */
+ public static long date2Millis(final Date date) {
+ return date.getTime();
+ }
+
+ /**
+ * Milliseconds to the date.
+ * 将时间戳转为 Date 类型
+ * @param millis The milliseconds.
+ * @return the date
+ */
+ public static Date millis2Date(final long millis) {
+ return new Date(millis);
+ }
+
+ /**
+ * Return the time span, in unit.
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ * 获取两个时间差(单位:unit)
+ * @param time1 The first formatted time string.
+ * @param time2 The second formatted time string.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span, in unit
+ */
+ public static long getTimeSpan(final String time1,
+ final String time2,
+ @TimeConstants.Unit final int unit) {
+ return getTimeSpan(time1, time2, getDefaultFormat(), unit);
+ }
+
+ /**
+ * Return the time span, in unit.
+ * 获取两个时间差(单位:unit)
+ * @param time1 The first formatted time string.
+ * @param time2 The second formatted time string.
+ * @param format The format.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span, in unit
+ */
+ public static long getTimeSpan(final String time1,
+ final String time2,
+ @NonNull final DateFormat format,
+ @TimeConstants.Unit final int unit) {
+ return millis2TimeSpan(string2Millis(time1, format) - string2Millis(time2, format), unit);
+ }
+
+ /**
+ * Return the time span, in unit.
+ * 获取两个时间差(单位:unit)
+ * @param date1 The first date.
+ * @param date2 The second date.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span, in unit
+ */
+ public static long getTimeSpan(final Date date1,
+ final Date date2,
+ @TimeConstants.Unit final int unit) {
+ return millis2TimeSpan(date2Millis(date1) - date2Millis(date2), unit);
+ }
+
+ /**
+ * Return the time span, in unit.
+ * 获取两个时间差(单位:unit)
+ * @param millis1 The first milliseconds.
+ * @param millis2 The second milliseconds.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span, in unit
+ */
+ public static long getTimeSpan(final long millis1,
+ final long millis2,
+ @TimeConstants.Unit final int unit) {
+ return millis2TimeSpan(millis1 - millis2, unit);
+ }
+
+ /**
+ * Return the fit time span.
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ * 获取合适型两个时间差
+ * @param time1 The first formatted time string.
+ * @param time2 The second formatted time string.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0, return null
+ * - precision = 1, return 天
+ * - precision = 2, return 天, 小时
+ * - precision = 3, return 天, 小时, 分钟
+ * - precision = 4, return 天, 小时, 分钟, 秒
+ * - precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
+ *
+ * @return the fit time span
+ */
+ public static String getFitTimeSpan(final String time1,
+ final String time2,
+ final int precision) {
+ long delta = string2Millis(time1, getDefaultFormat()) - string2Millis(time2, getDefaultFormat());
+ return millis2FitTimeSpan(delta, precision);
+ }
+
+ /**
+ * Return the fit time span.
+ * 获取合适型两个时间差
+ * @param time1 The first formatted time string.
+ * @param time2 The second formatted time string.
+ * @param format The format.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0, return null
+ * - precision = 1, return 天
+ * - precision = 2, return 天, 小时
+ * - precision = 3, return 天, 小时, 分钟
+ * - precision = 4, return 天, 小时, 分钟, 秒
+ * - precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
+ *
+ * @return the fit time span
+ */
+ public static String getFitTimeSpan(final String time1,
+ final String time2,
+ @NonNull final DateFormat format,
+ final int precision) {
+ long delta = string2Millis(time1, format) - string2Millis(time2, format);
+ return millis2FitTimeSpan(delta, precision);
+ }
+
+ /**
+ * Return the fit time span.
+ * 获取合适型两个时间差
+ * @param date1 The first date.
+ * @param date2 The second date.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0, return null
+ * - precision = 1, return 天
+ * - precision = 2, return 天, 小时
+ * - precision = 3, return 天, 小时, 分钟
+ * - precision = 4, return 天, 小时, 分钟, 秒
+ * - precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
+ *
+ * @return the fit time span
+ */
+ public static String getFitTimeSpan(final Date date1, final Date date2, final int precision) {
+ return millis2FitTimeSpan(date2Millis(date1) - date2Millis(date2), precision);
+ }
+
+ /**
+ * Return the fit time span.
+ * 获取合适型两个时间差
+ * @param millis1 The first milliseconds.
+ * @param millis2 The second milliseconds.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0, return null
+ * - precision = 1, return 天
+ * - precision = 2, return 天, 小时
+ * - precision = 3, return 天, 小时, 分钟
+ * - precision = 4, return 天, 小时, 分钟, 秒
+ * - precision >= 5,return 天, 小时, 分钟, 秒, 毫秒
+ *
+ * @return the fit time span
+ */
+ public static String getFitTimeSpan(final long millis1,
+ final long millis2,
+ final int precision) {
+ return millis2FitTimeSpan(millis1 - millis2, precision);
+ }
+
+ /**
+ * Return the current time in milliseconds.
+ * 获取当前毫秒时间戳
+ * @return the current time in milliseconds
+ */
+ public static long getNowMills() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * Return the current formatted time string.
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ * 获取当前时间字符串
+ * @return the current formatted time string
+ */
+ public static String getNowString() {
+ return millis2String(System.currentTimeMillis(), getDefaultFormat());
+ }
+
+ /**
+ * Return the current formatted time string.
+ * 获取当前时间字符串
+ * @param format The format.
+ * @return the current formatted time string
+ */
+ public static String getNowString(@NonNull final DateFormat format) {
+ return millis2String(System.currentTimeMillis(), format);
+ }
+
+ /**
+ * Return the current date.
+ * 获取当前 Date
+ * @return the current date
+ */
+ public static Date getNowDate() {
+ return new Date();
+ }
+
+ /**
+ * Return the time span by now, in unit.
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ * 获取与当前时间的差(单位:unit)
+ * @param time The formatted time string.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span by now, in unit
+ */
+ public static long getTimeSpanByNow(final String time, @TimeConstants.Unit final int unit) {
+ return getTimeSpan(time, getNowString(), getDefaultFormat(), unit);
+ }
+
+ /**
+ * Return the time span by now, in unit.
+ * 获取与当前时间的差(单位:unit)
+ * @param time The formatted time string.
+ * @param format The format.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span by now, in unit
+ */
+ public static long getTimeSpanByNow(final String time,
+ @NonNull final DateFormat format,
+ @TimeConstants.Unit final int unit) {
+ return getTimeSpan(time, getNowString(format), format, unit);
+ }
+
+ /**
+ * Return the time span by now, in unit.
+ * 获取与当前时间的差(单位:unit)
+ * @param date The date.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span by now, in unit
+ */
+ public static long getTimeSpanByNow(final Date date, @TimeConstants.Unit final int unit) {
+ return getTimeSpan(date, new Date(), unit);
+ }
+
+ /**
+ * Return the time span by now, in unit.
+ * 获取与当前时间的差(单位:unit)
+ * @param millis The milliseconds.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the time span by now, in unit
+ */
+ public static long getTimeSpanByNow(final long millis, @TimeConstants.Unit final int unit) {
+ return getTimeSpan(millis, System.currentTimeMillis(), unit);
+ }
+
+ /**
+ * Return the fit time span by now.
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ * 获取合适型与当前时间的差
+ * @param time The formatted time string.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0,返回 null
+ * - precision = 1,返回天
+ * - precision = 2,返回天和小时
+ * - precision = 3,返回天、小时和分钟
+ * - precision = 4,返回天、小时、分钟和秒
+ * - precision >= 5,返回天、小时、分钟、秒和毫秒
+ *
+ * @return the fit time span by now
+ */
+ public static String getFitTimeSpanByNow(final String time, final int precision) {
+ return getFitTimeSpan(time, getNowString(), getDefaultFormat(), precision);
+ }
+
+ /**
+ * Return the fit time span by now.
+ * 获取合适型与当前时间的差
+ * @param time The formatted time string.
+ * @param format The format.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0,返回 null
+ * - precision = 1,返回天
+ * - precision = 2,返回天和小时
+ * - precision = 3,返回天、小时和分钟
+ * - precision = 4,返回天、小时、分钟和秒
+ * - precision >= 5,返回天、小时、分钟、秒和毫秒
+ *
+ * @return the fit time span by now
+ */
+ public static String getFitTimeSpanByNow(final String time,
+ @NonNull final DateFormat format,
+ final int precision) {
+ return getFitTimeSpan(time, getNowString(format), format, precision);
+ }
+
+ /**
+ * Return the fit time span by now.
+ * 获取合适型与当前时间的差
+ * @param date The date.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0,返回 null
+ * - precision = 1,返回天
+ * - precision = 2,返回天和小时
+ * - precision = 3,返回天、小时和分钟
+ * - precision = 4,返回天、小时、分钟和秒
+ * - precision >= 5,返回天、小时、分钟、秒和毫秒
+ *
+ * @return the fit time span by now
+ */
+ public static String getFitTimeSpanByNow(final Date date, final int precision) {
+ return getFitTimeSpan(date, getNowDate(), precision);
+ }
+
+ /**
+ * Return the fit time span by now.
+ * 获取合适型与当前时间的差
+ * @param millis The milliseconds.
+ * @param precision The precision of time span.
+ *
+ * - precision = 0,返回 null
+ * - precision = 1,返回天
+ * - precision = 2,返回天和小时
+ * - precision = 3,返回天、小时和分钟
+ * - precision = 4,返回天、小时、分钟和秒
+ * - precision >= 5,返回天、小时、分钟、秒和毫秒
+ *
+ * @return the fit time span by now
+ */
+ public static String getFitTimeSpanByNow(final long millis, final int precision) {
+ return getFitTimeSpan(millis, System.currentTimeMillis(), precision);
+ }
+
+ /**
+ * Return the friendly time span by now.
+ * 获取友好型与当前时间的差
+ *
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return the friendly time span by now
+ *
+ * - 如果小于 1 秒钟内,显示刚刚
+ * - 如果在 1 分钟内,显示 XXX秒前
+ * - 如果在 1 小时内,显示 XXX分钟前
+ * - 如果在 1 小时外的今天内,显示今天15:32
+ * - 如果是昨天的,显示昨天15:32
+ * - 其余显示,2016-10-15
+ * - 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
+ *
+ */
+ public static String getFriendlyTimeSpanByNow(final String time) {
+ return getFriendlyTimeSpanByNow(time, getDefaultFormat());
+ }
+
+ /**
+ * Return the friendly time span by now.
+ * 获取友好型与当前时间的差
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the friendly time span by now
+ *
+ * - 如果小于 1 秒钟内,显示刚刚
+ * - 如果在 1 分钟内,显示 XXX秒前
+ * - 如果在 1 小时内,显示 XXX分钟前
+ * - 如果在 1 小时外的今天内,显示今天15:32
+ * - 如果是昨天的,显示昨天15:32
+ * - 其余显示,2016-10-15
+ * - 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
+ *
+ */
+ public static String getFriendlyTimeSpanByNow(final String time,
+ @NonNull final DateFormat format) {
+ return getFriendlyTimeSpanByNow(string2Millis(time, format));
+ }
+
+ /**
+ * Return the friendly time span by now.
+ * 获取友好型与当前时间的差
+ * @param date The date.
+ * @return the friendly time span by now
+ *
+ * - 如果小于 1 秒钟内,显示刚刚
+ * - 如果在 1 分钟内,显示 XXX秒前
+ * - 如果在 1 小时内,显示 XXX分钟前
+ * - 如果在 1 小时外的今天内,显示今天15:32
+ * - 如果是昨天的,显示昨天15:32
+ * - 其余显示,2016-10-15
+ * - 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
+ *
+ */
+ public static String getFriendlyTimeSpanByNow(final Date date) {
+ return getFriendlyTimeSpanByNow(date.getTime());
+ }
+
+ /**
+ * Return the friendly time span by now.
+ * 获取友好型与当前时间的差
+ * @param millis The milliseconds.
+ * @return the friendly time span by now
+ *
+ * - 如果小于 1 秒钟内,显示刚刚
+ * - 如果在 1 分钟内,显示 XXX秒前
+ * - 如果在 1 小时内,显示 XXX分钟前
+ * - 如果在 1 小时外的今天内,显示今天15:32
+ * - 如果是昨天的,显示昨天15:32
+ * - 其余显示,2016-10-15
+ * - 时间不合法的情况全部日期和时间信息,如星期六 十月 27 14:21:20 CST 2007
+ *
+ */
+ public static String getFriendlyTimeSpanByNow(final long millis) {
+ long now = System.currentTimeMillis();
+ long span = now - millis;
+ if (span < 0)
+ // U can read http://www.apihome.cn/api/java/Formatter.html to understand it.
+ return String.format("%tc", millis);
+ if (span < 1000) {
+ return "刚刚";
+ } else if (span < TimeConstants.MIN) {
+// return String.format(Locale.getDefault(), "%d秒前", span / TimeConstants.SEC);
+ return "刚刚";
+ } else if (span < TimeConstants.HOUR) {
+ return String.format(Locale.getDefault(), "%d分钟前", span / TimeConstants.MIN);
+ }
+ // 获取当天 00:00
+ long wee = getWeeOfToday();
+ if (millis >= wee) {
+ return String.format("今天%tR", millis);
+ } else if (millis >= wee - TimeConstants.DAY) {
+ return String.format("昨天%tR", millis);
+ } else {
+// return millis2String(millis);
+ return millis2String(millis,getDayFormat());
+// return String.format("%tc%n", millis);
+ }
+ }
+
+ private static long getWeeOfToday() {
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ return cal.getTimeInMillis();
+ }
+
+ /**
+ * Return the milliseconds differ time span.
+ * 获取与给定时间等于时间差的时间戳
+ * @param millis The milliseconds.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the milliseconds differ time span
+ */
+ public static long getMillis(final long millis,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis + timeSpan2Millis(timeSpan, unit);
+ }
+
+ /**
+ * Return the milliseconds differ time span.
+ * 获取与给定时间等于时间差的时间戳
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the milliseconds differ time span
+ */
+ public static long getMillis(final String time,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return getMillis(time, getDefaultFormat(), timeSpan, unit);
+ }
+
+ /**
+ * Return the milliseconds differ time span.
+ * 获取与给定时间等于时间差的时间戳
+ * @param time The formatted time string.
+ * @param format The format.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the milliseconds differ time span.
+ */
+ public static long getMillis(final String time,
+ @NonNull final DateFormat format,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return string2Millis(time, format) + timeSpan2Millis(timeSpan, unit);
+ }
+
+ /**
+ * Return the milliseconds differ time span.
+ * 获取与给定时间等于时间差的时间戳
+ * @param date The date.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the milliseconds differ time span.
+ */
+ public static long getMillis(final Date date,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return date2Millis(date) + timeSpan2Millis(timeSpan, unit);
+ }
+
+ /**
+ * Return the formatted time string differ time span.
+ * 获取与给定时间等于时间差的时间字符串
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param millis The milliseconds.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span
+ */
+ public static String getString(final long millis,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return getString(millis, getDefaultFormat(), timeSpan, unit);
+ }
+
+ /**
+ * Return the formatted time string differ time span.
+ * 获取与给定时间等于时间差的时间字符串
+ * @param millis The milliseconds.
+ * @param format The format.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span
+ */
+ public static String getString(final long millis,
+ @NonNull final DateFormat format,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis2String(millis + timeSpan2Millis(timeSpan, unit), format);
+ }
+
+ /**
+ * Return the formatted time string differ time span.
+ * 获取与给定时间等于时间差的时间字符串
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span
+ */
+ public static String getString(final String time,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return getString(time, getDefaultFormat(), timeSpan, unit);
+ }
+
+ /**
+ * Return the formatted time string differ time span.
+ * 获取与给定时间等于时间差的时间字符串
+ * @param time The formatted time string.
+ * @param format The format.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span
+ */
+ public static String getString(final String time,
+ @NonNull final DateFormat format,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis2String(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit), format);
+ }
+
+ /**
+ * Return the formatted time string differ time span.
+ * 获取与给定时间等于时间差的时间字符串
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param date The date.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span
+ */
+ public static String getString(final Date date,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return getString(date, getDefaultFormat(), timeSpan, unit);
+ }
+
+ /**
+ * Return the formatted time string differ time span.
+ * 获取与给定时间等于时间差的时间字符串
+ * @param date The date.
+ * @param format The format.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span
+ */
+ public static String getString(final Date date,
+ @NonNull final DateFormat format,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis2String(date2Millis(date) + timeSpan2Millis(timeSpan, unit), format);
+ }
+
+ /**
+ * Return the date differ time span.
+ * 获取与给定时间等于时间差的 Date
+ * @param millis The milliseconds.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the date differ time span
+ */
+ public static Date getDate(final long millis,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis2Date(millis + timeSpan2Millis(timeSpan, unit));
+ }
+
+ /**
+ * Return the date differ time span.
+ * 获取与给定时间等于时间差的 Date
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the date differ time span
+ */
+ public static Date getDate(final String time,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return getDate(time, getDefaultFormat(), timeSpan, unit);
+ }
+
+ /**
+ * Return the date differ time span.
+ * 获取与给定时间等于时间差的 Date
+ * @param time The formatted time string.
+ * @param format The format.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the date differ time span
+ */
+ public static Date getDate(final String time,
+ @NonNull final DateFormat format,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis2Date(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit));
+ }
+
+ /**
+ * Return the date differ time span.
+ * 获取与给定时间等于时间差的 Date
+ * @param date The date.
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the date differ time span
+ */
+ public static Date getDate(final Date date,
+ final long timeSpan,
+ @TimeConstants.Unit final int unit) {
+ return millis2Date(date2Millis(date) + timeSpan2Millis(timeSpan, unit));
+ }
+
+ /**
+ * Return the milliseconds differ time span by now.
+ * 获取与当前时间等于时间差的时间戳
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the milliseconds differ time span by now
+ */
+ public static long getMillisByNow(final long timeSpan, @TimeConstants.Unit final int unit) {
+ return getMillis(getNowMills(), timeSpan, unit);
+ }
+
+ /**
+ * Return the formatted time string differ time span by now.
+ * 获取与当前时间等于时间差的时间字符串
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span by now
+ */
+ public static String getStringByNow(final long timeSpan, @TimeConstants.Unit final int unit) {
+ return getStringByNow(timeSpan, getDefaultFormat(), unit);
+ }
+
+ /**
+ * Return the formatted time string differ time span by now.
+ * 获取与当前时间等于时间差的时间字符串
+ * @param timeSpan The time span.
+ * @param format The format.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the formatted time string differ time span by now
+ */
+ public static String getStringByNow(final long timeSpan,
+ @NonNull final DateFormat format,
+ @TimeConstants.Unit final int unit) {
+ return getString(getNowMills(), format, timeSpan, unit);
+ }
+
+ /**
+ * Return the date differ time span by now.
+ * 获取与当前时间等于时间差的 Date
+ * @param timeSpan The time span.
+ * @param unit The unit of time span.
+ *
+ * - {@link TimeConstants#MSEC}
+ * - {@link TimeConstants#SEC }
+ * - {@link TimeConstants#MIN }
+ * - {@link TimeConstants#HOUR}
+ * - {@link TimeConstants#DAY }
+ *
+ * @return the date differ time span by now
+ */
+ public static Date getDateByNow(final long timeSpan, @TimeConstants.Unit final int unit) {
+ return getDate(getNowMills(), timeSpan, unit);
+ }
+
+ /**
+ * Return whether it is today.
+ * 判断是否今天
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isToday(final String time) {
+ return isToday(string2Millis(time, getDefaultFormat()));
+ }
+
+ /**
+ * Return whether it is today.
+ * 判断是否今天
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isToday(final String time, @NonNull final DateFormat format) {
+ return isToday(string2Millis(time, format));
+ }
+
+ /**
+ * Return whether it is today.
+ * 判断是否今天
+ * @param date The date.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isToday(final Date date) {
+ return isToday(date.getTime());
+ }
+
+ /**
+ * Return whether it is today.
+ * 判断是否今天
+ * @param millis The milliseconds.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isToday(final long millis) {
+ long wee = getWeeOfToday();
+ return millis >= wee && millis < wee + TimeConstants.DAY;
+ }
+
+ /**
+ * Return whether it is leap year.
+ * 判断是否闰年
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isLeapYear(final String time) {
+ return isLeapYear(string2Date(time, getDefaultFormat()));
+ }
+
+ /**
+ * Return whether it is leap year.
+ * 判断是否闰年
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isLeapYear(final String time, @NonNull final DateFormat format) {
+ return isLeapYear(string2Date(time, format));
+ }
+
+ /**
+ * Return whether it is leap year.
+ * 判断是否闰年
+ * @param date The date.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isLeapYear(final Date date) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ int year = cal.get(Calendar.YEAR);
+ return isLeapYear(year);
+ }
+
+ /**
+ * Return whether it is leap year.
+ * 判断是否闰年
+ * @param millis The milliseconds.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isLeapYear(final long millis) {
+ return isLeapYear(millis2Date(millis));
+ }
+
+ /**
+ * Return whether it is leap year.
+ * 判断是否闰年
+ * @param year The year.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isLeapYear(final int year) {
+ return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
+ }
+
+ /**
+ * Return the day of week in Chinese.
+ * 获取中式星期
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return the day of week in Chinese
+ */
+ public static String getChineseWeek(final String time) {
+ return getChineseWeek(string2Date(time, getDefaultFormat()));
+ }
+
+ /**
+ * Return the day of week in Chinese.
+ * 获取中式星期
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the day of week in Chinese
+ */
+ public static String getChineseWeek(final String time, @NonNull final DateFormat format) {
+ return getChineseWeek(string2Date(time, format));
+ }
+
+ /**
+ * Return the day of week in Chinese.
+ * 获取中式星期
+ * @param date The date.
+ * @return the day of week in Chinese
+ */
+ public static String getChineseWeek(final Date date) {
+ return new SimpleDateFormat("E", Locale.CHINA).format(date);
+ }
+
+ /**
+ * Return the day of week in Chinese.
+ * 获取中式星期
+ * @param millis The milliseconds.
+ * @return the day of week in Chinese
+ */
+ public static String getChineseWeek(final long millis) {
+ return getChineseWeek(new Date(millis));
+ }
+
+ /**
+ * Return the day of week in US.
+ * 获取美式式星期
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return the day of week in US
+ */
+ public static String getUSWeek(final String time) {
+ return getUSWeek(string2Date(time, getDefaultFormat()));
+ }
+
+ /**
+ * Return the day of week in US.
+ * 获取美式式星期
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the day of week in US
+ */
+ public static String getUSWeek(final String time, @NonNull final DateFormat format) {
+ return getUSWeek(string2Date(time, format));
+ }
+
+ /**
+ * Return the day of week in US.
+ * 获取美式式星期
+ * @param date The date.
+ * @return the day of week in US
+ */
+ public static String getUSWeek(final Date date) {
+ return new SimpleDateFormat("EEEE", Locale.US).format(date);
+ }
+
+ /**
+ * Return the day of week in US.
+ * 获取美式式星期
+ * @param millis The milliseconds.
+ * @return the day of week in US
+ */
+ public static String getUSWeek(final long millis) {
+ return getUSWeek(new Date(millis));
+ }
+
+ /**
+ * Returns the value of the given calendar field.
+ * 返回给定日历字段的值。
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @param field The given calendar field.
+ *
+ * - {@link Calendar#ERA}
+ * - {@link Calendar#YEAR}
+ * - {@link Calendar#MONTH}
+ * - ...
+ * - {@link Calendar#DST_OFFSET}
+ *
+ * @return the value of the given calendar field
+ */
+ public static int getValueByCalendarField(final String time, final int field) {
+ return getValueByCalendarField(string2Date(time, getDefaultFormat()), field);
+ }
+
+ /**
+ * Returns the value of the given calendar field.
+ * 返回给定日历字段的值。
+ * @param time The formatted time string.
+ * @param format The format.
+ * @param field The given calendar field.
+ *
+ * - {@link Calendar#ERA}
+ * - {@link Calendar#YEAR}
+ * - {@link Calendar#MONTH}
+ * - ...
+ * - {@link Calendar#DST_OFFSET}
+ *
+ * @return the value of the given calendar field
+ */
+ public static int getValueByCalendarField(final String time,
+ @NonNull final DateFormat format,
+ final int field) {
+ return getValueByCalendarField(string2Date(time, format), field);
+ }
+
+ /**
+ * Returns the value of the given calendar field.
+ * 返回给定日历字段的值。
+ * @param date The date.
+ * @param field The given calendar field.
+ *
+ * - {@link Calendar#ERA}
+ * - {@link Calendar#YEAR}
+ * - {@link Calendar#MONTH}
+ * - ...
+ * - {@link Calendar#DST_OFFSET}
+ *
+ * @return the value of the given calendar field
+ */
+ public static int getValueByCalendarField(final Date date, final int field) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ return cal.get(field);
+ }
+
+ /**
+ * Returns the value of the given calendar field.
+ * 返回给定日历字段的值。
+ * @param millis The milliseconds.
+ * @param field The given calendar field.
+ *
+ * - {@link Calendar#ERA}
+ * - {@link Calendar#YEAR}
+ * - {@link Calendar#MONTH}
+ * - ...
+ * - {@link Calendar#DST_OFFSET}
+ *
+ * @return the value of the given calendar field
+ */
+ public static int getValueByCalendarField(final long millis, final int field) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(millis);
+ return cal.get(field);
+ }
+
+ private static final String[] CHINESE_ZODIAC =
+ {"猴", "鸡", "狗", "猪", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊"};
+
+ /**
+ * Return the Chinese zodiac.
+ * 获取生肖
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return the Chinese zodiac
+ */
+ public static String getChineseZodiac(final String time) {
+ return getChineseZodiac(string2Date(time, getDefaultFormat()));
+ }
+
+ /**
+ * Return the Chinese zodiac.
+ * 获取生肖
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the Chinese zodiac
+ */
+ public static String getChineseZodiac(final String time, @NonNull final DateFormat format) {
+ return getChineseZodiac(string2Date(time, format));
+ }
+
+ /**
+ * Return the Chinese zodiac.
+ * 获取生肖
+ * @param date The date.
+ * @return the Chinese zodiac
+ */
+ public static String getChineseZodiac(final Date date) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ return CHINESE_ZODIAC[cal.get(Calendar.YEAR) % 12];
+ }
+
+ /**
+ * Return the Chinese zodiac.
+ * 获取生肖
+ * @param millis The milliseconds.
+ * @return the Chinese zodiac
+ */
+ public static String getChineseZodiac(final long millis) {
+ return getChineseZodiac(millis2Date(millis));
+ }
+
+ /**
+ * Return the Chinese zodiac.
+ * 获取生肖
+ * @param year The year.
+ * @return the Chinese zodiac
+ */
+ public static String getChineseZodiac(final int year) {
+ return CHINESE_ZODIAC[year % 12];
+ }
+
+ private static final int[] ZODIAC_FLAGS = {20, 19, 21, 21, 21, 22, 23, 23, 23, 24, 23, 22};
+ private static final String[] ZODIAC = {
+ "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座",
+ "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "魔羯座"
+ };
+
+ /**
+ * Return the zodiac.
+ * 获取星座
+ * The pattern is {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param time The formatted time string.
+ * @return the zodiac
+ */
+ public static String getZodiac(final String time) {
+ return getZodiac(string2Date(time, getDefaultFormat()));
+ }
+
+ /**
+ * Return the zodiac.
+ * 获取星座
+ * @param time The formatted time string.
+ * @param format The format.
+ * @return the zodiac
+ */
+ public static String getZodiac(final String time, @NonNull final DateFormat format) {
+ return getZodiac(string2Date(time, format));
+ }
+
+ /**
+ * Return the zodiac.
+ * 获取星座
+ * @param date The date.
+ * @return the zodiac
+ */
+ public static String getZodiac(final Date date) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ int month = cal.get(Calendar.MONTH) + 1;
+ int day = cal.get(Calendar.DAY_OF_MONTH);
+ return getZodiac(month, day);
+ }
+
+ /**
+ * Return the zodiac.
+ * 获取星座
+ * @param millis The milliseconds.
+ * @return the zodiac
+ */
+ public static String getZodiac(final long millis) {
+ return getZodiac(millis2Date(millis));
+ }
+
+ /**
+ * Return the zodiac.
+ * 获取星座
+ * @param month The month.
+ * @param day The day.
+ * @return the zodiac
+ */
+ public static String getZodiac(final int month, final int day) {
+ return ZODIAC[day >= ZODIAC_FLAGS[month - 1]
+ ? month - 1
+ : (month + 10) % 12];
+ }
+
+ private static long timeSpan2Millis(final long timeSpan, @TimeConstants.Unit final int unit) {
+ return timeSpan * unit;
+ }
+
+ private static long millis2TimeSpan(final long millis, @TimeConstants.Unit final int unit) {
+ return millis / unit;
+ }
+
+ private static String millis2FitTimeSpan(long millis, int precision) {
+ if (precision <= 0) return null;
+ precision = Math.min(precision, 5);
+ String[] units = {"天", "小时", "分钟", "秒", "毫秒"};
+ if (millis == 0) return 0 + units[precision - 1];
+ StringBuilder sb = new StringBuilder();
+ if (millis < 0) {
+ sb.append("-");
+ millis = -millis;
+ }
+ int[] unitLen = {86400000, 3600000, 60000, 1000, 1};
+ for (int i = 0; i < precision; i++) {
+ if (millis >= unitLen[i]) {
+ long mode = millis / unitLen[i];
+ millis -= mode * unitLen[i];
+ sb.append(mode).append(units[i]);
+ }
+ }
+ return sb.toString();
+ }
+
+
+ public static final class TimeConstants {
+
+ public static final int MSEC = 1;
+ public static final int SEC = 1000;
+ public static final int MIN = 60000;
+ public static final int HOUR = 3600000;
+ public static final int DAY = 86400000;
+
+ @IntDef({MSEC, SEC, MIN, HOUR, DAY})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Unit {
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/example/alcoholic/widget/HintLayout.java b/app/src/main/java/com/example/alcoholic/widget/HintLayout.java
new file mode 100644
index 0000000..1b3e913
--- /dev/null
+++ b/app/src/main/java/com/example/alcoholic/widget/HintLayout.java
@@ -0,0 +1,155 @@
+package com.example.alcoholic.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+
+import com.example.alcoholic.R;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RawRes;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/04/18
+ * desc : 状态布局(网络错误,异常错误,空数据)
+ */
+public final class HintLayout extends FrameLayout {
+
+ /** 提示布局 */
+ private ViewGroup mMainLayout;
+ /** 提示图标 */
+// private LottieAnimationView mImageView;
+ /** 提示文本 */
+ private TextView mTextView;
+
+ public HintLayout(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public HintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HintLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public HintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setClickable(true);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ }
+
+ /**
+ * 显示
+ */
+ public void show() {
+
+ if (mMainLayout == null) {
+ //初始化布局
+ initLayout();
+ }
+
+ if (!isShow()) {
+ // 显示布局
+ mMainLayout.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * 隐藏
+ */
+ public void hide() {
+
+ if (mMainLayout != null && isShow()) {
+ //隐藏布局
+ mMainLayout.setVisibility(INVISIBLE);
+ }
+ }
+
+ /**
+ * 是否显示了
+ */
+ public boolean isShow() {
+ return mMainLayout != null && mMainLayout.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * 设置提示图标,请在show方法之后调用
+ */
+ public void setIcon(@DrawableRes int id) {
+ setIcon(ContextCompat.getDrawable(getContext(), id));
+ }
+
+ public void setIcon(Drawable drawable) {
+// if (mImageView != null) {
+// mImageView.setImageDrawable(drawable);
+// }
+ }
+
+ /**
+ * 设置提示动画
+ */
+ public void setAnim(@RawRes int id) {
+// mImageView.setAnimation(id);
+// 这里需要调用播放动画,否则会出现第一次显示动画效果正常,第二次显示动画会不动
+// mImageView.playAnimation();
+ }
+
+ /**
+ * 设置提示文本,请在show方法之后调用
+ */
+ public void setHint(@StringRes int id) {
+ setHint(getResources().getString(id));
+ }
+
+ public void setHint(CharSequence text) {
+ if (mTextView != null && text != null) {
+ mTextView.setText(text);
+ }
+ }
+
+ /**
+ * 初始化提示的布局
+ */
+ private void initLayout() {
+
+ mMainLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.layout_widget_hint, this, false);
+
+// mImageView = mMainLayout.findViewById(R.id.iv_hint_icon);
+ mTextView = mMainLayout.findViewById(R.id.layout_widgetHint_tv_text);
+
+ if (mMainLayout.getBackground() == null) {
+ // 默认使用 windowBackground 作为背景
+ TypedArray ta = getContext().obtainStyledAttributes(new int[]{android.R.attr.windowBackground});
+ mMainLayout.setBackground(ta.getDrawable(0));
+ ta.recycle();
+ }
+
+ addView(mMainLayout);
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ if (isShow()) {
+ mMainLayout.setOnClickListener(l);
+ } else {
+ super.setOnClickListener(l);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/alcoholic/widget/PhotoViewPager.java b/app/src/main/java/com/example/alcoholic/widget/PhotoViewPager.java
new file mode 100644
index 0000000..d67d548
--- /dev/null
+++ b/app/src/main/java/com/example/alcoholic/widget/PhotoViewPager.java
@@ -0,0 +1,35 @@
+package com.example.alcoholic.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.viewpager.widget.ViewPager;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/05/07
+ * desc : ViewPager 中使用 PhotoView 时出现 pointerIndex out of range 异常
+ */
+public final class PhotoViewPager extends ViewPager {
+
+ public PhotoViewPager(Context context) {
+ super(context);
+ }
+
+ public PhotoViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // 当 PhotoView 和 ViewPager 组合时 ,用双指进行放大时 是没有问题的,但是用双指进行缩小的时候,程序就会崩掉
+ // 并且抛出java.lang.IllegalArgumentException: pointerIndex out of range
+ try {
+ return super.onInterceptTouchEvent(ev);
+ } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ignored) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/alcoholic/widget/ProgressView.java b/app/src/main/java/com/example/alcoholic/widget/ProgressView.java
new file mode 100644
index 0000000..fbe85e6
--- /dev/null
+++ b/app/src/main/java/com/example/alcoholic/widget/ProgressView.java
@@ -0,0 +1,702 @@
+package com.example.alcoholic.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+
+import com.example.alcoholic.R;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * author : Todd-Davies
+ * github : https://github.com/Todd-Davies/ProgressWheel
+ * time : 2019/07/13
+ * desc : 进度条控件
+ */
+public final class ProgressView extends View {
+
+ private final static int BAR_LENGTH = 16;
+ private final static int BAR_MAX_LENGTH = 270;
+ private final static long PAUSE_GROWING_TIME = 200;
+
+ /** Sizes (with defaults in DP) */
+ private int mCircleRadius = 28;
+ private int mBarWidth = 4;
+ private int mRimWidth = 4;
+ private boolean mFillRadius;
+ private double mTimeStartGrowing = 0;
+ private double mBarSpinCycleTime = 400;
+ private float mBarExtraLength = 0;
+ private boolean mBarGrowingFromFront = true;
+ private long mPausedTimeWithoutGrowing = 0;
+ /** Colors (with defaults) */
+ private int mBarColor = 0xAA000000;
+ private int mRimColor = 0x00FFFFFF;
+
+ /** Paints */
+ private final Paint mBarPaint = new Paint();
+ private final Paint mRimPaint = new Paint();
+
+ /** Rectangles */
+ private RectF mCircleBounds = new RectF();
+
+ /** Animation The amount of degrees per second */
+ private float mSpinSpeed = 230.0f;
+ // private float mSpinSpeed = 120.0f;
+ /** The last time the spinner was animated */
+ private long mLastTimeAnimated = 0;
+
+ private boolean mLinearProgress;
+
+ private float mProgress = 0.0f;
+ private float mTargetProgress = 0.0f;
+ private boolean isSpinning = false;
+
+ private ProgressCallback mCallback;
+
+ private final boolean mShouldAnimate;
+
+ public ProgressView(Context context) {
+ this(context, null, 0);
+ }
+
+ public ProgressView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
+ mBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mBarWidth, getResources().getDisplayMetrics());
+ mRimWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mRimWidth, getResources().getDisplayMetrics());
+ mCircleRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mCircleRadius, getResources().getDisplayMetrics());
+ mCircleRadius = (int) array.getDimension(R.styleable.ProgressView_circleRadius, mCircleRadius);
+ mFillRadius = array.getBoolean(R.styleable.ProgressView_fillRadius, false);
+ mBarWidth = (int) array.getDimension(R.styleable.ProgressView_barWidth, mBarWidth);
+ mRimWidth = (int) array.getDimension(R.styleable.ProgressView_rimWidth, mRimWidth);
+ float baseSpinSpeed = array.getFloat(R.styleable.ProgressView_spinSpeed, mSpinSpeed / 360.0f);
+ mSpinSpeed = baseSpinSpeed * 360;
+ mBarSpinCycleTime = array.getInt(R.styleable.ProgressView_barSpinCycleTime, (int) mBarSpinCycleTime);
+ mBarColor = array.getColor(R.styleable.ProgressView_barColor, mBarColor);
+ mRimColor = array.getColor(R.styleable.ProgressView_rimColor, mRimColor);
+ mLinearProgress = array.getBoolean(R.styleable.ProgressView_linearProgress, false);
+ if (array.getBoolean(R.styleable.ProgressView_progressIndeterminate, false)) {
+ spin();
+ }
+ array.recycle();
+
+ float animationValue = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1);
+ mShouldAnimate = animationValue != 0;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width;
+ int height;
+
+ int viewWidth = mCircleRadius + this.getPaddingLeft() + this.getPaddingRight();
+ int viewHeight = mCircleRadius + this.getPaddingTop() + this.getPaddingBottom();
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ switch (widthMode) {
+ case MeasureSpec.EXACTLY:
+ width = widthSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.UNSPECIFIED:
+ width = Math.min(viewWidth, widthSize);
+ break;
+ default:
+ width = viewWidth;
+ break;
+ }
+
+ if (heightMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.EXACTLY) {
+ height = heightSize;
+ } else if (heightMode == MeasureSpec.AT_MOST) {
+ height = Math.min(viewHeight, heightSize);
+ } else {
+ height = viewHeight;
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view,
+ * because this method is called after measuring the dimensions of MATCH_PARENT & WRAP_CONTENT.
+ * Use this dimensions to setup the bounds and paints.
+ */
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+
+ setupBounds(width, height);
+ setupPaints();
+ invalidate();
+ }
+
+ /**
+ * Set the properties of the paints we're using to
+ * draw the progress wheel
+ */
+ private void setupPaints() {
+ mBarPaint.setColor(mBarColor);
+ mBarPaint.setAntiAlias(true);
+ mBarPaint.setStyle(Style.STROKE);
+ mBarPaint.setStrokeWidth(mBarWidth);
+
+ mRimPaint.setColor(mRimColor);
+ mRimPaint.setAntiAlias(true);
+ mRimPaint.setStyle(Style.STROKE);
+ mRimPaint.setStrokeWidth(mRimWidth);
+ }
+
+ /**
+ * Set the bounds of the component
+ */
+ private void setupBounds(int layoutWidth, int layoutHeight) {
+ int paddingTop = getPaddingTop();
+ int paddingBottom = getPaddingBottom();
+ int paddingLeft = getPaddingLeft();
+ int paddingRight = getPaddingRight();
+
+ if (!mFillRadius) {
+ // Width should equal to Height, find the min value to setup the circle
+ int minValue = Math.min(layoutWidth - paddingLeft - paddingRight,
+ layoutHeight - paddingBottom - paddingTop);
+
+ int circleDiameter = Math.min(minValue, mCircleRadius * 2 - mBarWidth * 2);
+
+ // Calc the Offset if needed for centering the wheel in the available space
+ int xOffset = (layoutWidth - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft;
+ int yOffset = (layoutHeight - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop;
+
+ mCircleBounds = new RectF(xOffset + mBarWidth, yOffset + mBarWidth, xOffset + circleDiameter - mBarWidth,
+ yOffset + circleDiameter - mBarWidth);
+ } else {
+ mCircleBounds = new RectF(paddingLeft + mBarWidth, paddingTop + mBarWidth,
+ layoutWidth - paddingRight - mBarWidth, layoutHeight - paddingBottom - mBarWidth);
+ }
+ }
+
+ public void setCallback(ProgressCallback progressCallback) {
+ mCallback = progressCallback;
+
+ if (!isSpinning) {
+ runCallback();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawArc(mCircleBounds, 360, 360, false, mRimPaint);
+
+ boolean mustInvalidate = false;
+
+ if (!mShouldAnimate) {
+ return;
+ }
+
+ if (isSpinning) {
+ //Draw the spinning bar
+ mustInvalidate = true;
+
+ long deltaTime = (SystemClock.uptimeMillis() - mLastTimeAnimated);
+ float deltaNormalized = deltaTime * mSpinSpeed / 1000.0f;
+
+ updateBarLength(deltaTime);
+
+ mProgress += deltaNormalized;
+ if (mProgress > 360) {
+ mProgress -= 360f;
+
+ // A full turn has been completed
+ // we run the callback with -1 in case we want to
+ // do something, like changing the color
+ runCallback(-1.0f);
+ }
+ mLastTimeAnimated = SystemClock.uptimeMillis();
+
+ float from = mProgress - 90;
+ float length = BAR_LENGTH + mBarExtraLength;
+
+ if (isInEditMode()) {
+ from = 0;
+ length = 135;
+ }
+
+ canvas.drawArc(mCircleBounds, from, length, false, mBarPaint);
+ } else {
+ float oldProgress = mProgress;
+
+ if (mProgress != mTargetProgress) {
+ //We smoothly increase the progress bar
+ mustInvalidate = true;
+
+ float deltaTime = (float) (SystemClock.uptimeMillis() - mLastTimeAnimated) / 1000;
+ float deltaNormalized = deltaTime * mSpinSpeed;
+
+ mProgress = Math.min(mProgress + deltaNormalized, mTargetProgress);
+ mLastTimeAnimated = SystemClock.uptimeMillis();
+ }
+
+ if (oldProgress != mProgress) {
+ runCallback();
+ }
+
+ float offset = 0.0f;
+ float progress = mProgress;
+ if (!mLinearProgress) {
+ float factor = 2.0f;
+ offset = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, 2.0f * factor)) * 360.0f;
+ progress = (float) (1.0f - Math.pow(1.0f - mProgress / 360.0f, factor)) * 360.0f;
+ }
+
+ if (isInEditMode()) {
+ progress = 360;
+ }
+
+ canvas.drawArc(mCircleBounds, offset - 90, progress, false, mBarPaint);
+ }
+
+ if (mustInvalidate) {
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ if (visibility == VISIBLE) {
+ mLastTimeAnimated = SystemClock.uptimeMillis();
+ }
+ }
+
+ private void updateBarLength(long deltaTimeInMilliSeconds) {
+ if (mPausedTimeWithoutGrowing >= PAUSE_GROWING_TIME) {
+ mTimeStartGrowing += deltaTimeInMilliSeconds;
+
+ if (mTimeStartGrowing > mBarSpinCycleTime) {
+ // We completed a size change cycle
+ // (growing or shrinking)
+ mTimeStartGrowing -= mBarSpinCycleTime;
+ //if(mBarGrowingFromFront) {
+ mPausedTimeWithoutGrowing = 0;
+ //}
+ mBarGrowingFromFront = !mBarGrowingFromFront;
+ }
+
+ float distance =
+ (float) Math.cos((mTimeStartGrowing / mBarSpinCycleTime + 1) * Math.PI) / 2 + 0.5f;
+ float destLength = (BAR_MAX_LENGTH - BAR_LENGTH);
+
+ if (mBarGrowingFromFront) {
+ mBarExtraLength = distance * destLength;
+ } else {
+ float newLength = destLength * (1 - distance);
+ mProgress += (mBarExtraLength - newLength);
+ mBarExtraLength = newLength;
+ }
+ } else {
+ mPausedTimeWithoutGrowing += deltaTimeInMilliSeconds;
+ }
+ }
+
+ /**
+ * Check if the wheel is currently spinning
+ */
+
+ public boolean isSpinning() {
+ return isSpinning;
+ }
+
+ /**
+ * Reset the count (in increment mode)
+ */
+ public void resetCount() {
+ mProgress = 0.0f;
+ mTargetProgress = 0.0f;
+ invalidate();
+ }
+
+ /**
+ * Turn off spin mode
+ */
+ public void stopSpinning() {
+ isSpinning = false;
+ mProgress = 0.0f;
+ mTargetProgress = 0.0f;
+ invalidate();
+ }
+
+ /**
+ * Puts the view on spin mode
+ */
+ public void spin() {
+ mLastTimeAnimated = SystemClock.uptimeMillis();
+ isSpinning = true;
+ invalidate();
+ }
+
+ private void runCallback(float value) {
+ if (mCallback != null) {
+ mCallback.onProgressUpdate(value);
+ }
+ }
+
+ private void runCallback() {
+ if (mCallback != null) {
+ float normalizedProgress = (float) Math.round(mProgress * 100 / 360.0f) / 100;
+ mCallback.onProgressUpdate(normalizedProgress);
+ }
+ }
+
+ /**
+ * Set the progress to a specific value,
+ * the bar will be set instantly to that value
+ *
+ * @param progress the progress between 0 and 1
+ */
+ public void setInstantProgress(float progress) {
+ if (isSpinning) {
+ mProgress = 0.0f;
+ isSpinning = false;
+ }
+
+ if (progress > 1.0f) {
+ progress -= 1.0f;
+ } else if (progress < 0) {
+ progress = 0;
+ }
+
+ if (progress == mTargetProgress) {
+ return;
+ }
+
+ mTargetProgress = Math.min(progress * 360.0f, 360.0f);
+ mProgress = mTargetProgress;
+ mLastTimeAnimated = SystemClock.uptimeMillis();
+ invalidate();
+ }
+
+ // Great way to save a view's state http://stackoverflow.com/a/7089687/1991053
+ @Override
+ public Parcelable onSaveInstanceState() {
+ WheelSavedState savedState = new WheelSavedState(super.onSaveInstanceState());
+ // We save everything that can be changed at runtime
+ savedState.mProgress = this.mProgress;
+ savedState.mTargetProgress = this.mTargetProgress;
+ savedState.isSpinning = this.isSpinning;
+ savedState.spinSpeed = this.mSpinSpeed;
+ savedState.barWidth = this.mBarWidth;
+ savedState.barColor = this.mBarColor;
+ savedState.rimWidth = this.mRimWidth;
+ savedState.rimColor = this.mRimColor;
+ savedState.circleRadius = this.mCircleRadius;
+ savedState.linearProgress = this.mLinearProgress;
+ savedState.fillRadius = this.mFillRadius;
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof WheelSavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ WheelSavedState savedState = (WheelSavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+
+ this.mProgress = savedState.mProgress;
+ this.mTargetProgress = savedState.mTargetProgress;
+ this.isSpinning = savedState.isSpinning;
+ this.mSpinSpeed = savedState.spinSpeed;
+ this.mBarWidth = savedState.barWidth;
+ this.mBarColor = savedState.barColor;
+ this.mRimWidth = savedState.rimWidth;
+ this.mRimColor = savedState.rimColor;
+ this.mCircleRadius = savedState.circleRadius;
+ this.mLinearProgress = savedState.linearProgress;
+ this.mFillRadius = savedState.fillRadius;
+
+ this.mLastTimeAnimated = SystemClock.uptimeMillis();
+ }
+
+ /**
+ * @return the current progress between 0.0 and 1.0,
+ * if the wheel is indeterminate, then the result is -1
+ */
+ public float getProgress() {
+ return isSpinning ? -1 : mProgress / 360.0f;
+ }
+
+ //----------------------------------
+ //Getters + setters
+ //----------------------------------
+
+ /**
+ * Set the progress to a specific value,
+ * the bar will smoothly animate until that value
+ *
+ * @param progress the progress between 0 and 1
+ */
+ public void setProgress(float progress) {
+ if (isSpinning) {
+ mProgress = 0.0f;
+ isSpinning = false;
+
+ runCallback();
+ }
+
+ if (progress > 1.0f) {
+ progress -= 1.0f;
+ } else if (progress < 0) {
+ progress = 0;
+ }
+
+ if (progress == mTargetProgress) {
+ return;
+ }
+
+ // If we are currently in the right position
+ // we set again the last time animated so the
+ // animation starts smooth from here
+ if (mProgress == mTargetProgress) {
+ mLastTimeAnimated = SystemClock.uptimeMillis();
+ }
+
+ mTargetProgress = Math.min(progress * 360.0f, 360.0f);
+
+ invalidate();
+ }
+
+ /**
+ * Sets the determinate progress mode
+ *
+ * @param isLinear if the progress should increase linearly
+ */
+ public void setLinearProgress(boolean isLinear) {
+ mLinearProgress = isLinear;
+ if (!isSpinning) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the radius of the wheel in pixels
+ */
+ public int getCircleRadius() {
+ return mCircleRadius;
+ }
+
+ /**
+ * Sets the radius of the wheel
+ *
+ * @param circleRadius the expected radius, in pixels
+ */
+ public void setCircleRadius(int circleRadius) {
+ this.mCircleRadius = circleRadius;
+ if (!isSpinning) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the width of the spinning bar
+ */
+ public int getBarWidth() {
+ return mBarWidth;
+ }
+
+ /**
+ * Sets the width of the spinning bar
+ *
+ * @param barWidth the spinning bar width in pixels
+ */
+ public void setBarWidth(int barWidth) {
+ this.mBarWidth = barWidth;
+ if (!isSpinning) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the color of the spinning bar
+ */
+ public int getBarColor() {
+ return mBarColor;
+ }
+
+ /**
+ * Sets the color of the spinning bar
+ *
+ * @param barColor The spinning bar color
+ */
+ public void setBarColor(int barColor) {
+ this.mBarColor = barColor;
+ setupPaints();
+ if (!isSpinning) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the color of the wheel's contour
+ */
+ public int getRimColor() {
+ return mRimColor;
+ }
+
+ /**
+ * Sets the color of the wheel's contour
+ *
+ * @param rimColor the color for the wheel
+ */
+ public void setRimColor(int rimColor) {
+ this.mRimColor = rimColor;
+ setupPaints();
+ if (!isSpinning) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the base spinning speed, in full circle turns per second
+ * (1.0 equals on full turn in one second), this value also is applied for
+ * the smoothness when setting a progress
+ */
+ public float getSpinSpeed() {
+ return mSpinSpeed / 360.0f;
+ }
+
+ /**
+ * Sets the base spinning speed, in full circle turns per second
+ * (1.0 equals on full turn in one second), this value also is applied for
+ * the smoothness when setting a progress
+ *
+ * @param spinSpeed the desired base speed in full turns per second
+ */
+ public void setSpinSpeed(float spinSpeed) {
+ this.mSpinSpeed = spinSpeed * 360.0f;
+ }
+
+ /**
+ * @return the width of the wheel's contour in pixels
+ */
+ public int getRimWidth() {
+ return mRimWidth;
+ }
+
+ /**
+ * Sets the width of the wheel's contour
+ *
+ * @param rimWidth the width in pixels
+ */
+ public void setRimWidth(int rimWidth) {
+ this.mRimWidth = rimWidth;
+ if (!isSpinning) {
+ invalidate();
+ }
+ }
+
+ public interface ProgressCallback {
+ /**
+ * Method to call when the progress reaches a value
+ * in order to avoid float precision issues, the progress
+ * is rounded to a float with two decimals.
+ *
+ * In indeterminate mode, the callback is called each time
+ * the wheel completes an animation cycle, with, the progress value is -1.0f
+ *
+ * @param progress a double value between 0.00 and 1.00 both included
+ */
+ void onProgressUpdate(float progress);
+ }
+
+ static class WheelSavedState extends BaseSavedState {
+ // required field that makes Parcelables from a Parcel
+ public static final Creator CREATOR =
+ new Creator() {
+ @Override
+ public WheelSavedState createFromParcel(Parcel in) {
+ return new WheelSavedState(in);
+ }
+
+ @Override
+ public WheelSavedState[] newArray(int size) {
+ return new WheelSavedState[size];
+ }
+ };
+ float mProgress;
+ float mTargetProgress;
+ boolean isSpinning;
+ float spinSpeed;
+ int barWidth;
+ int barColor;
+ int rimWidth;
+ int rimColor;
+ int circleRadius;
+ boolean linearProgress;
+ boolean fillRadius;
+
+ WheelSavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private WheelSavedState(Parcel in) {
+ super(in);
+ this.mProgress = in.readFloat();
+ this.mTargetProgress = in.readFloat();
+ this.isSpinning = in.readByte() != 0;
+ this.spinSpeed = in.readFloat();
+ this.barWidth = in.readInt();
+ this.barColor = in.readInt();
+ this.rimWidth = in.readInt();
+ this.rimColor = in.readInt();
+ this.circleRadius = in.readInt();
+ this.linearProgress = in.readByte() != 0;
+ this.fillRadius = in.readByte() != 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeFloat(this.mProgress);
+ out.writeFloat(this.mTargetProgress);
+ out.writeByte((byte) (isSpinning ? 1 : 0));
+ out.writeFloat(this.spinSpeed);
+ out.writeInt(this.barWidth);
+ out.writeInt(this.barColor);
+ out.writeInt(this.rimWidth);
+ out.writeInt(this.rimColor);
+ out.writeInt(this.circleRadius);
+ out.writeByte((byte) (linearProgress ? 1 : 0));
+ out.writeByte((byte) (fillRadius ? 1 : 0));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/alcoholic/widget/decoration/ChatListDecoration.java b/app/src/main/java/com/example/alcoholic/widget/decoration/ChatListDecoration.java
new file mode 100644
index 0000000..4846e50
--- /dev/null
+++ b/app/src/main/java/com/example/alcoholic/widget/decoration/ChatListDecoration.java
@@ -0,0 +1,87 @@
+package com.example.alcoholic.widget.decoration;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.example.alcoholic.utils.SizeUtils;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Created by
+ * Description:聊天列表分割线
+ * on 2020/11/13.
+ */
+public class ChatListDecoration extends RecyclerView.ItemDecoration {
+
+ private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
+
+ private Context mContext;
+ private Paint mPaint;
+ private Drawable mDivider;
+
+ private int mDividerHeight = 1;//分割线高度,默认为1px
+
+ public ChatListDecoration(Context context) {
+ this.mContext = context;
+
+ final TypedArray a = context.obtainStyledAttributes(ATTRS);
+ mDivider = a.getDrawable(0);
+ a.recycle();
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setColor(0XFFCCCCCC);
+ mPaint.setStyle(Paint.Style.FILL);
+ }
+
+ @Override
+ public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ super.onDraw(c, parent, state);
+ if (parent.getLayoutManager() == null) {
+ return;
+ }
+
+ drawHorizontal(c, parent);
+ }
+
+ @Override
+ public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ super.onDrawOver(c, parent, state);
+
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+
+ outRect.set(0, 0, 0, mDividerHeight);
+ }
+
+
+ //绘制横向 item 分割线
+ private void drawHorizontal(Canvas canvas, RecyclerView parent) {
+ final int left = parent.getPaddingLeft()+ SizeUtils.dp2px(mContext,15+50+10);
+ final int right = parent.getMeasuredWidth() - parent.getPaddingRight();
+ final int childSize = parent.getChildCount();
+ for (int i = 0; i < childSize; i++) {
+ final View child = parent.getChildAt(i);
+ RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
+ final int top = child.getBottom() + layoutParams.bottomMargin;
+ final int bottom = top + mDividerHeight;
+ if (mDivider != null) {
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(canvas);
+ }
+ if (mPaint != null) {
+ canvas.drawRect(left, top, right, bottom, mPaint);
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/anim/left_in_activity.xml b/app/src/main/res/anim/left_in_activity.xml
new file mode 100644
index 0000000..e03efb4
--- /dev/null
+++ b/app/src/main/res/anim/left_in_activity.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/left_out_activity.xml b/app/src/main/res/anim/left_out_activity.xml
new file mode 100644
index 0000000..464168e
--- /dev/null
+++ b/app/src/main/res/anim/left_out_activity.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/right_in_activity.xml b/app/src/main/res/anim/right_in_activity.xml
new file mode 100644
index 0000000..253a11f
--- /dev/null
+++ b/app/src/main/res/anim/right_in_activity.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/right_out_activity.xml b/app/src/main/res/anim/right_out_activity.xml
new file mode 100644
index 0000000..912cc35
--- /dev/null
+++ b/app/src/main/res/anim/right_out_activity.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..fde1368
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/arrows_bottom_ic.xml b/app/src/main/res/drawable/arrows_bottom_ic.xml
new file mode 100644
index 0000000..11e2987
--- /dev/null
+++ b/app/src/main/res/drawable/arrows_bottom_ic.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/arrows_left_ic.xml b/app/src/main/res/drawable/arrows_left_ic.xml
new file mode 100644
index 0000000..d2958cb
--- /dev/null
+++ b/app/src/main/res/drawable/arrows_left_ic.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/arrows_right_ic.xml b/app/src/main/res/drawable/arrows_right_ic.xml
new file mode 100644
index 0000000..d2a3f87
--- /dev/null
+++ b/app/src/main/res/drawable/arrows_right_ic.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/arrows_top_ic.xml b/app/src/main/res/drawable/arrows_top_ic.xml
new file mode 100644
index 0000000..7c4ed34
--- /dev/null
+++ b/app/src/main/res/drawable/arrows_top_ic.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_btn_chat_send.xml b/app/src/main/res/drawable/bg_btn_chat_send.xml
new file mode 100644
index 0000000..4c96143
--- /dev/null
+++ b/app/src/main/res/drawable/bg_btn_chat_send.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_btn_exit_login.xml b/app/src/main/res/drawable/bg_btn_exit_login.xml
new file mode 100644
index 0000000..8e48480
--- /dev/null
+++ b/app/src/main/res/drawable/bg_btn_exit_login.xml
@@ -0,0 +1,28 @@
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_et_chat_input.xml b/app/src/main/res/drawable/bg_et_chat_input.xml
new file mode 100644
index 0000000..a181f8a
--- /dev/null
+++ b/app/src/main/res/drawable/bg_et_chat_input.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_left_grey.xml b/app/src/main/res/drawable/bg_left_grey.xml
new file mode 100644
index 0000000..97a86b2
--- /dev/null
+++ b/app/src/main/res/drawable/bg_left_grey.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/camera_ic.xml b/app/src/main/res/drawable/camera_ic.xml
new file mode 100644
index 0000000..e9615ee
--- /dev/null
+++ b/app/src/main/res/drawable/camera_ic.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/checkbox_checked_ic.xml b/app/src/main/res/drawable/checkbox_checked_ic.xml
new file mode 100644
index 0000000..d8ce448
--- /dev/null
+++ b/app/src/main/res/drawable/checkbox_checked_ic.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/checkbox_disable_ic.xml b/app/src/main/res/drawable/checkbox_disable_ic.xml
new file mode 100644
index 0000000..a73bee9
--- /dev/null
+++ b/app/src/main/res/drawable/checkbox_disable_ic.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/checkbox_selector.xml b/app/src/main/res/drawable/checkbox_selector.xml
new file mode 100644
index 0000000..0d9bdc7
--- /dev/null
+++ b/app/src/main/res/drawable/checkbox_selector.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/compound_normal_ic.xml b/app/src/main/res/drawable/compound_normal_ic.xml
new file mode 100644
index 0000000..f9789e3
--- /dev/null
+++ b/app/src/main/res/drawable/compound_normal_ic.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dialog_rounded_corner_bg.xml b/app/src/main/res/drawable/dialog_rounded_corner_bg.xml
new file mode 100644
index 0000000..820f8c2
--- /dev/null
+++ b/app/src/main/res/drawable/dialog_rounded_corner_bg.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/hint_empty_ic.xml b/app/src/main/res/drawable/hint_empty_ic.xml
new file mode 100644
index 0000000..18ae238
--- /dev/null
+++ b/app/src/main/res/drawable/hint_empty_ic.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/hint_error_ic.xml b/app/src/main/res/drawable/hint_error_ic.xml
new file mode 100644
index 0000000..4c44126
--- /dev/null
+++ b/app/src/main/res/drawable/hint_error_ic.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/hint_nerwork_ic.xml b/app/src/main/res/drawable/hint_nerwork_ic.xml
new file mode 100644
index 0000000..472af06
--- /dev/null
+++ b/app/src/main/res/drawable/hint_nerwork_ic.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_found_off_ic.xml b/app/src/main/res/drawable/home_found_off_ic.xml
new file mode 100644
index 0000000..26557c2
--- /dev/null
+++ b/app/src/main/res/drawable/home_found_off_ic.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_found_on_ic.xml b/app/src/main/res/drawable/home_found_on_ic.xml
new file mode 100644
index 0000000..8bd0fa2
--- /dev/null
+++ b/app/src/main/res/drawable/home_found_on_ic.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_found_selector.xml b/app/src/main/res/drawable/home_found_selector.xml
new file mode 100644
index 0000000..ed38d27
--- /dev/null
+++ b/app/src/main/res/drawable/home_found_selector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_home_off_ic.xml b/app/src/main/res/drawable/home_home_off_ic.xml
new file mode 100644
index 0000000..fac197b
--- /dev/null
+++ b/app/src/main/res/drawable/home_home_off_ic.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_home_on_ic.xml b/app/src/main/res/drawable/home_home_on_ic.xml
new file mode 100644
index 0000000..ce01600
--- /dev/null
+++ b/app/src/main/res/drawable/home_home_on_ic.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_home_selector.xml b/app/src/main/res/drawable/home_home_selector.xml
new file mode 100644
index 0000000..6674dd5
--- /dev/null
+++ b/app/src/main/res/drawable/home_home_selector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_me_off_ic.xml b/app/src/main/res/drawable/home_me_off_ic.xml
new file mode 100644
index 0000000..1d841a9
--- /dev/null
+++ b/app/src/main/res/drawable/home_me_off_ic.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_me_on_ic.xml b/app/src/main/res/drawable/home_me_on_ic.xml
new file mode 100644
index 0000000..c3d1155
--- /dev/null
+++ b/app/src/main/res/drawable/home_me_on_ic.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_me_selector.xml b/app/src/main/res/drawable/home_me_selector.xml
new file mode 100644
index 0000000..84f81d8
--- /dev/null
+++ b/app/src/main/res/drawable/home_me_selector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_message_off_ic.xml b/app/src/main/res/drawable/home_message_off_ic.xml
new file mode 100644
index 0000000..b1d0b38
--- /dev/null
+++ b/app/src/main/res/drawable/home_message_off_ic.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_message_on_ic.xml b/app/src/main/res/drawable/home_message_on_ic.xml
new file mode 100644
index 0000000..cc46c7f
--- /dev/null
+++ b/app/src/main/res/drawable/home_message_on_ic.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_message_selector.xml b/app/src/main/res/drawable/home_message_selector.xml
new file mode 100644
index 0000000..cbe8bd9
--- /dev/null
+++ b/app/src/main/res/drawable/home_message_selector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_navigation_color_selector.xml b/app/src/main/res/drawable/home_navigation_color_selector.xml
new file mode 100644
index 0000000..aa66a67
--- /dev/null
+++ b/app/src/main/res/drawable/home_navigation_color_selector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_more_horiz_24.xml b/app/src/main/res/drawable/ic_baseline_more_horiz_24.xml
new file mode 100644
index 0000000..e4ab25c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_more_horiz_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..1e4408c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/image_error_bg.xml b/app/src/main/res/drawable/image_error_bg.xml
new file mode 100644
index 0000000..88586df
--- /dev/null
+++ b/app/src/main/res/drawable/image_error_bg.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/image_loading_bg.xml b/app/src/main/res/drawable/image_loading_bg.xml
new file mode 100644
index 0000000..9d47e5b
--- /dev/null
+++ b/app/src/main/res/drawable/image_loading_bg.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/radiobutton_checked_ic.xml b/app/src/main/res/drawable/radiobutton_checked_ic.xml
new file mode 100644
index 0000000..fd19909
--- /dev/null
+++ b/app/src/main/res/drawable/radiobutton_checked_ic.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/radiobutton_disable_ic.xml b/app/src/main/res/drawable/radiobutton_disable_ic.xml
new file mode 100644
index 0000000..834d84e
--- /dev/null
+++ b/app/src/main/res/drawable/radiobutton_disable_ic.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/radiobutton_selector.xml b/app/src/main/res/drawable/radiobutton_selector.xml
new file mode 100644
index 0000000..f9a9ae3
--- /dev/null
+++ b/app/src/main/res/drawable/radiobutton_selector.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/setting_update_bg.xml b/app/src/main/res/drawable/setting_update_bg.xml
new file mode 100644
index 0000000..6a80771
--- /dev/null
+++ b/app/src/main/res/drawable/setting_update_bg.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/succeed_ic.xml b/app/src/main/res/drawable/succeed_ic.xml
new file mode 100644
index 0000000..5ef7046
--- /dev/null
+++ b/app/src/main/res/drawable/succeed_ic.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/transparent_selector.xml b/app/src/main/res/drawable/transparent_selector.xml
new file mode 100644
index 0000000..e10d84f
--- /dev/null
+++ b/app/src/main/res/drawable/transparent_selector.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml
new file mode 100644
index 0000000..765137b
--- /dev/null
+++ b/app/src/main/res/layout/activity_chat.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_contacts.xml b/app/src/main/res/layout/activity_contacts.xml
new file mode 100644
index 0000000..ca31f0e
--- /dev/null
+++ b/app/src/main/res/layout/activity_contacts.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_friend_add.xml b/app/src/main/res/layout/activity_friend_add.xml
new file mode 100644
index 0000000..e7d4378
--- /dev/null
+++ b/app/src/main/res/layout/activity_friend_add.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_group_add.xml b/app/src/main/res/layout/activity_group_add.xml
new file mode 100644
index 0000000..981d472
--- /dev/null
+++ b/app/src/main/res/layout/activity_group_add.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_group_add_member.xml b/app/src/main/res/layout/activity_group_add_member.xml
new file mode 100644
index 0000000..8869f70
--- /dev/null
+++ b/app/src/main/res/layout/activity_group_add_member.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_group_home.xml b/app/src/main/res/layout/activity_group_home.xml
new file mode 100644
index 0000000..3de431b
--- /dev/null
+++ b/app/src/main/res/layout/activity_group_home.xml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_group_info.xml b/app/src/main/res/layout/activity_group_info.xml
new file mode 100644
index 0000000..fafa211
--- /dev/null
+++ b/app/src/main/res/layout/activity_group_info.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_group_manage.xml b/app/src/main/res/layout/activity_group_manage.xml
new file mode 100644
index 0000000..e1de9a7
--- /dev/null
+++ b/app/src/main/res/layout/activity_group_manage.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_help.xml b/app/src/main/res/layout/activity_help.xml
new file mode 100644
index 0000000..a634788
--- /dev/null
+++ b/app/src/main/res/layout/activity_help.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml
new file mode 100644
index 0000000..c1585f8
--- /dev/null
+++ b/app/src/main/res/layout/activity_home.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_image_preview.xml b/app/src/main/res/layout/activity_image_preview.xml
new file mode 100644
index 0000000..ac40cb2
--- /dev/null
+++ b/app/src/main/res/layout/activity_image_preview.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_image_select.xml b/app/src/main/res/layout/activity_image_select.xml
new file mode 100644
index 0000000..9ee2519
--- /dev/null
+++ b/app/src/main/res/layout/activity_image_select.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..671669f
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml
new file mode 100644
index 0000000..97d051e
--- /dev/null
+++ b/app/src/main/res/layout/activity_setting.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..23699d4
--- /dev/null
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_user_home.xml b/app/src/main/res/layout/activity_user_home.xml
new file mode 100644
index 0000000..4059aba
--- /dev/null
+++ b/app/src/main/res/layout/activity_user_home.xml
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_user_info_setting.xml b/app/src/main/res/layout/activity_user_info_setting.xml
new file mode 100644
index 0000000..be1a5bd
--- /dev/null
+++ b/app/src/main/res/layout/activity_user_info_setting.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_user_login.xml b/app/src/main/res/layout/activity_user_login.xml
new file mode 100644
index 0000000..31e53cd
--- /dev/null
+++ b/app/src/main/res/layout/activity_user_login.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_user_register.xml b/app/src/main/res/layout/activity_user_register.xml
new file mode 100644
index 0000000..0ab2160
--- /dev/null
+++ b/app/src/main/res/layout/activity_user_register.xml
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_album.xml b/app/src/main/res/layout/dialog_album.xml
new file mode 100644
index 0000000..bf9fcc8
--- /dev/null
+++ b/app/src/main/res/layout/dialog_album.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_contacts_sync.xml b/app/src/main/res/layout/dialog_contacts_sync.xml
new file mode 100644
index 0000000..8749a78
--- /dev/null
+++ b/app/src/main/res/layout/dialog_contacts_sync.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_input.xml b/app/src/main/res/layout/dialog_input.xml
new file mode 100644
index 0000000..1e20989
--- /dev/null
+++ b/app/src/main/res/layout/dialog_input.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_message.xml b/app/src/main/res/layout/dialog_message.xml
new file mode 100644
index 0000000..5091b35
--- /dev/null
+++ b/app/src/main/res/layout/dialog_message.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_ui.xml b/app/src/main/res/layout/dialog_ui.xml
new file mode 100644
index 0000000..1ba7ecb
--- /dev/null
+++ b/app/src/main/res/layout/dialog_ui.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_wait.xml b/app/src/main/res/layout/dialog_wait.xml
new file mode 100644
index 0000000..44e2dfa
--- /dev/null
+++ b/app/src/main/res/layout/dialog_wait.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_find.xml b/app/src/main/res/layout/fragment_find.xml
new file mode 100644
index 0000000..dc0afeb
--- /dev/null
+++ b/app/src/main/res/layout/fragment_find.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 0000000..4fb5065
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_me.xml b/app/src/main/res/layout/fragment_me.xml
new file mode 100644
index 0000000..44ec8d8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_me.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml
new file mode 100644
index 0000000..4f8399b
--- /dev/null
+++ b/app/src/main/res/layout/fragment_message.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_album.xml b/app/src/main/res/layout/item_album.xml
new file mode 100644
index 0000000..5970bde
--- /dev/null
+++ b/app/src/main/res/layout/item_album.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_chat_list.xml b/app/src/main/res/layout/item_chat_list.xml
new file mode 100644
index 0000000..7d3c221
--- /dev/null
+++ b/app/src/main/res/layout/item_chat_list.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_contacts.xml b/app/src/main/res/layout/item_contacts.xml
new file mode 100644
index 0000000..919e1b2
--- /dev/null
+++ b/app/src/main/res/layout/item_contacts.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_group_members_simple.xml b/app/src/main/res/layout/item_group_members_simple.xml
new file mode 100644
index 0000000..2ff6d11
--- /dev/null
+++ b/app/src/main/res/layout/item_group_members_simple.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_image_select.xml b/app/src/main/res/layout/item_image_select.xml
new file mode 100644
index 0000000..1c8c902
--- /dev/null
+++ b/app/src/main/res/layout/item_image_select.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_msg_left.xml b/app/src/main/res/layout/item_msg_left.xml
new file mode 100644
index 0000000..33aa693
--- /dev/null
+++ b/app/src/main/res/layout/item_msg_left.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_msg_right.xml b/app/src/main/res/layout/item_msg_right.xml
new file mode 100644
index 0000000..c09b8b3
--- /dev/null
+++ b/app/src/main/res/layout/item_msg_right.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_empty.xml b/app/src/main/res/layout/layout_empty.xml
new file mode 100644
index 0000000..3fc107c
--- /dev/null
+++ b/app/src/main/res/layout/layout_empty.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_widget_hint.xml b/app/src/main/res/layout/layout_widget_hint.xml
new file mode 100644
index 0000000..45a1729
--- /dev/null
+++ b/app/src/main/res/layout/layout_widget_hint.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_home_bottom_nav.xml b/app/src/main/res/menu/menu_home_bottom_nav.xml
new file mode 100644
index 0000000..b062132
--- /dev/null
+++ b/app/src/main/res/menu/menu_home_bottom_nav.xml
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_circle.png b/app/src/main/res/mipmap-xxhdpi/ic_add_circle.png
new file mode 100644
index 0000000..a1dd20f
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_circle.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_friend.png b/app/src/main/res/mipmap-xxhdpi/ic_add_friend.png
new file mode 100644
index 0000000..c931186
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_friend.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_black_list.png b/app/src/main/res/mipmap-xxhdpi/ic_black_list.png
new file mode 100644
index 0000000..e526bb3
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_black_list.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_group.png b/app/src/main/res/mipmap-xxhdpi/ic_group.png
new file mode 100644
index 0000000..0135222
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_group.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_run_sync.png b/app/src/main/res/mipmap-xxhdpi/ic_run_sync.png
new file mode 100644
index 0000000..d943390
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_run_sync.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/message_text_receive.9.png b/app/src/main/res/mipmap-xxhdpi/message_text_receive.9.png
new file mode 100644
index 0000000..22acb35
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/message_text_receive.9.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/message_text_send.9.png b/app/src/main/res/mipmap-xxhdpi/message_text_send.9.png
new file mode 100644
index 0000000..770a820
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/message_text_send.9.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/bg_chat_0.jpg b/app/src/main/res/mipmap-xxxhdpi/bg_chat_0.jpg
new file mode 100644
index 0000000..119337b
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/bg_chat_0.jpg differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/bg_chat_1.jpg b/app/src/main/res/mipmap-xxxhdpi/bg_chat_1.jpg
new file mode 100644
index 0000000..2c3f59e
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/bg_chat_1.jpg differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/de_head_1.jpg b/app/src/main/res/mipmap-xxxhdpi/de_head_1.jpg
new file mode 100644
index 0000000..6c80130
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/de_head_1.jpg differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/de_head_2.jpg b/app/src/main/res/mipmap-xxxhdpi/de_head_2.jpg
new file mode 100644
index 0000000..941fce2
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/de_head_2.jpg differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..257270c
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..dcd8dbc
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,25 @@
+
+
+ @color/white
+ @color/black
+ #FFCB57
+ #F4F4F4
+ #333333
+ #757575
+
+
+ #EEEEEE
+
+ #CCCCCC
+
+
+ #7C7C7C
+
+ #F13D20
+ #1592E6
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..be97cf1
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,143 @@
+
+
+
+ 1px
+
+
+ 999dp
+
+
+ 4dp
+
+
+ 4dp
+
+
+ 15dp
+
+
+ 1dp
+ 2dp
+ 3dp
+ 4dp
+ 5dp
+ 6dp
+ 7dp
+ 8dp
+ 9dp
+ 10dp
+ 11dp
+ 12dp
+ 13dp
+ 14dp
+ 15dp
+ 16dp
+ 17dp
+ 18dp
+ 19dp
+ 20dp
+ 21dp
+ 22dp
+ 23dp
+ 24dp
+ 25dp
+ 26dp
+ 27dp
+ 28dp
+ 29dp
+ 30dp
+ 31dp
+ 32dp
+ 33dp
+ 34dp
+ 35dp
+ 36dp
+ 37dp
+ 38dp
+ 39dp
+ 40dp
+ 41dp
+ 42dp
+ 43dp
+ 44dp
+ 45dp
+ 46dp
+ 47dp
+ 48dp
+ 49dp
+ 50dp
+ 51dp
+ 52dp
+ 53dp
+ 54dp
+ 55dp
+ 56dp
+ 57dp
+ 58dp
+ 59dp
+ 60dp
+ 61dp
+ 62dp
+ 63dp
+ 64dp
+ 65dp
+ 66dp
+ 67dp
+ 68dp
+ 69dp
+ 70dp
+ 71dp
+ 72dp
+ 73dp
+ 74dp
+ 75dp
+ 76dp
+ 77dp
+ 78dp
+ 79dp
+ 80dp
+ 81dp
+ 82dp
+ 83dp
+ 84dp
+ 85dp
+ 86dp
+ 87dp
+ 88dp
+ 89dp
+ 90dp
+ 91dp
+ 92dp
+ 93dp
+ 94dp
+ 95dp
+ 96dp
+ 97dp
+ 98dp
+ 99dp
+ 100dp
+ 120dp
+ 140dp
+ 160dp
+ 180dp
+ 190dp
+ 200dp
+ 300dp
+ 400dp
+ 500dp
+
+
+
+
+ 8sp
+ 10sp
+ 12sp
+ 14sp
+ 16sp
+ 18sp
+ 20sp
+ 22sp
+ 24sp
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
new file mode 100644
index 0000000..5baad8d
--- /dev/null
+++ b/app/src/main/res/values/integers.xml
@@ -0,0 +1,8 @@
+
+
+
+ 200
+
+
+ 4
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..44d759c
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,213 @@
+
+ 大呲花
+
+
+ 输入手机号码
+ 手机号输入不正确
+
+ 请输入密码
+ 两次密码输入不一致,请重新输入
+
+ 输入验证码
+ 发送验证码
+ 验证码已发送,请注意查收
+ 验证码错误,请检查输入
+
+ 下一步
+ 完成
+ 提交
+
+ 请先授予权限
+ 授权失败,请手动授予权限
+
+ 加载中…
+
+ 确定
+ 完成
+ 取消
+
+ 年
+ 月
+ 日
+
+ 时
+ 分
+ 秒
+
+ 当前没有网络连接,请检查网络设置
+
+
+ 加载中…
+
+ 请求出错,未知错误
+ 账号异常,请重新登录
+ 数据解析异常,请稍后
+ 服务器请求超时,请稍后再试
+ 请求失败,请检查网络设置
+ 服务器响应异常,请稍后再试
+ 服务器连接异常,请稍后再试
+ 请求被中断,请重试
+
+
+ 空空如也
+ 请求出错,点击重试
+ 网络错误,点击重试
+
+
+ 提示
+ 错误
+
+
+ 请选择地区
+ 请选择
+
+
+ 请选择日期
+
+
+ 请选择时间
+
+
+ 发现新版本
+
+ 更新内容
+
+ 下次再说
+ 立即更新
+
+ 必须先要授予权限才能正常下载更新哦
+
+ 正在下载
+ 下载中 %d%%
+ 下载完成,点击安装
+ 下载失败,点击重试
+
+ 当前已是最新版本
+
+
+ 分享到…
+
+ 微信
+ 朋友圈
+ QQ
+ QQ空间
+ 复制链接
+
+ 已复制到剪贴板
+
+
+ 请输入支付密码
+
+
+ 至少要选择 %d 项
+ 最多只能选择 %d 项
+
+
+ 身份校验
+
+
+ 再按一次退出
+ 首页
+ 发现
+ 消息
+ 我的
+
+
+ 注册
+ 忘记密码?
+ 登录
+
+ 其他登录方式
+
+
+ 注册
+ 手机号仅用于登录和保护账号安全
+ 设置6–18位登录密码
+ 请再次输入一次密码
+ 注册成功
+
+
+ 设置
+
+ 语言切换
+ 简体中文
+ 繁体中文
+ 检查更新
+
+ 修改密码
+ 修改手机
+
+ 自动登录
+ 清空缓存
+
+ 隐私协议
+ 关于我们
+
+ 退出登录
+
+
+ 关于我们
+ Android 轮子哥
+
+ Copyright © 2018 – 2020
+
+
+ 忘记密码
+
+
+ 设置登录密码
+ 设置6–18位登录密码
+ 重新输入一次密码
+ 两次密码输入不一致,请重新输入
+ 密码重置成功
+
+
+ 设置手机号
+ 下次登录请使用更换后的新手机号登录
+ 立即绑定
+
+ 绑定成功,下次登录请使用新手机号登录
+
+
+ 个人资料
+ 头像
+
+ 用户ID
+ 昵称
+ 设置昵称
+
+ 地区
+ 请选择
+
+ 手机号码
+ 立即绑定
+
+
+ 网页加载中…
+
+
+ 图片选择
+ 所有图片
+
+ 共 %d 张
+
+ 本次最多只能选择 %d 张图片
+
+
+ 视频选择
+ 所有视频
+
+ 共 %d 个
+
+ 本次最多只能选择 %d 个视频
+
+
+ 无法启动相机
+ 目标地址错误
+
+
+
+ 发送
+ qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM1234567890
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..a1348c6
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..dca93c0
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/alcoholic/ExampleUnitTest.java b/app/src/test/java/com/example/alcoholic/ExampleUnitTest.java
new file mode 100644
index 0000000..752e158
--- /dev/null
+++ b/app/src/test/java/com/example/alcoholic/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.alcoholic;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/base/.gitignore b/base/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/base/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/base/build.gradle b/base/build.gradle
new file mode 100644
index 0000000..469c26d
--- /dev/null
+++ b/base/build.gradle
@@ -0,0 +1,6 @@
+apply plugin: 'com.android.library'
+apply from: '../config.gradle'
+
+
+android {
+}
\ No newline at end of file
diff --git a/base/consumer-rules.pro b/base/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/base/proguard-rules.pro b/base/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/base/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/base/src/androidTest/java/com/example/base/ExampleInstrumentedTest.java b/base/src/androidTest/java/com/example/base/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..98ec5a1
--- /dev/null
+++ b/base/src/androidTest/java/com/example/base/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.example.base;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.example.base.test", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1ef51aa
--- /dev/null
+++ b/base/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+ /
+
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/BaseActivity.java b/base/src/main/java/com/example/base/BaseActivity.java
new file mode 100644
index 0000000..6811a53
--- /dev/null
+++ b/base/src/main/java/com/example/base/BaseActivity.java
@@ -0,0 +1,181 @@
+package com.example.base;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.inputmethod.InputMethodManager;
+
+import com.example.base.action.ActivityAction;
+import com.example.base.action.BundleAction;
+import com.example.base.action.ClickAction;
+import com.example.base.action.HandlerAction;
+
+import java.util.Random;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : Activity 基类
+ */
+public abstract class BaseActivity extends AppCompatActivity
+ implements ActivityAction, ClickAction, HandlerAction, BundleAction {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ initActivity();
+ }
+
+ protected void initActivity() {
+ initLayout();
+ initView();
+ initData();
+ }
+
+ /**
+ * 获取布局 ID
+ */
+ protected abstract int getLayoutId();
+
+ /**
+ * 初始化控件
+ */
+ protected abstract void initView();
+
+ /**
+ * 初始化数据
+ */
+ protected abstract void initData();
+
+ /**
+ * 初始化布局
+ */
+ protected void initLayout() {
+ if (getLayoutId() > 0) {
+ setContentView(getLayoutId());
+ initSoftKeyboard();
+ }
+ }
+
+ /**
+ * 初始化软键盘
+ */
+ protected void initSoftKeyboard() {
+ // 点击外部隐藏软键盘,提升用户体验
+ getContentView().setOnClickListener(v -> hideSoftKeyboard());
+ }
+
+ @Override
+ protected void onDestroy() {
+ removeCallbacks();
+ super.onDestroy();
+ }
+
+ @Override
+ public void finish() {
+ hideSoftKeyboard();
+ super.finish();
+ }
+
+ /**
+ * 如果当前的 Activity(singleTop 启动模式) 被复用时会回调
+ */
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ // 设置为当前的 Intent,避免 Activity 被杀死后重启 Intent 还是最原先的那个
+ setIntent(intent);
+ }
+
+ @Override
+ public Bundle getBundle() {
+ return getIntent().getExtras();
+ }
+
+ /**
+ * 和 setContentView 对应的方法
+ */
+ public ViewGroup getContentView() {
+ return findViewById(Window.ID_ANDROID_CONTENT);
+ }
+
+ @Override
+ public Context getContext() {
+ return this;
+ }
+
+ /**
+ * startActivityForResult 方法优化
+ */
+
+ private OnActivityCallback mActivityCallback;
+ private int mActivityRequestCode;
+
+ public void startActivityForResult(Class extends Activity> clazz, OnActivityCallback callback) {
+ startActivityForResult(new Intent(this, clazz), null, callback);
+ }
+
+ public void startActivityForResult(Intent intent, OnActivityCallback callback) {
+ startActivityForResult(intent, null, callback);
+ }
+
+ public void startActivityForResult(Intent intent, @Nullable Bundle options, OnActivityCallback callback) {
+ // 回调还没有结束,所以不能再次调用此方法,这个方法只适合一对一回调,其他需求请使用原生的方法实现
+ if (mActivityCallback == null) {
+ mActivityCallback = callback;
+ // 随机生成请求码,这个请求码必须在 2 的 16 次幂以内,也就是 0 - 65535
+ mActivityRequestCode = new Random().nextInt((int) Math.pow(2, 16));
+ startActivityForResult(intent, mActivityRequestCode, options);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (mActivityCallback != null && mActivityRequestCode == requestCode) {
+ mActivityCallback.onActivityResult(resultCode, data);
+ mActivityCallback = null;
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
+ hideSoftKeyboard();
+ // 查看源码得知 startActivity 最终也会调用 startActivityForResult
+ super.startActivityForResult(intent, requestCode, options);
+ }
+
+ /**
+ * 隐藏软键盘
+ */
+ private void hideSoftKeyboard() {
+ // 隐藏软键盘,避免软键盘引发的内存泄露
+ View view = getCurrentFocus();
+ if (view != null) {
+ InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (manager != null && manager.isActive(view)) {
+ manager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+ }
+
+ public interface OnActivityCallback {
+
+ /**
+ * 结果回调
+ *
+ * @param resultCode 结果码
+ * @param data 数据
+ */
+ void onActivityResult(int resultCode, @Nullable Intent data);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/BaseAdapter.java b/base/src/main/java/com/example/base/BaseAdapter.java
new file mode 100644
index 0000000..d5b2c25
--- /dev/null
+++ b/base/src/main/java/com/example/base/BaseAdapter.java
@@ -0,0 +1,398 @@
+package com.example.base;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+
+
+import com.example.base.action.ResourcesAction;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : RecyclerView 适配器基类
+ */
+public abstract class BaseAdapter
+ extends RecyclerView.Adapter implements ResourcesAction {
+
+ /** 上下文对象 */
+ private final Context mContext;
+
+ /** RecyclerView 对象 */
+ private RecyclerView mRecyclerView;
+
+ /** 条目点击监听器 */
+ private OnItemClickListener mItemClickListener;
+ /** 条目长按监听器 */
+ private OnItemLongClickListener mItemLongClickListener;
+ /** RecyclerView 滚动事件 */
+ private OnScrollingListener mScrollingListener;
+
+ /** 条目子 View 点击监听器 */
+ private SparseArray mChildClickListeners;
+ /** 条目子 View 长按监听器 */
+ private SparseArray mChildLongClickListeners;
+
+ /** ViewHolder 位置偏移值 */
+ private int mPositionOffset = 0;
+
+ public BaseAdapter(Context context) {
+ mContext = context;
+ if (mContext == null) {
+ throw new IllegalArgumentException("are you ok?");
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public final void onBindViewHolder(@NonNull VH holder, int position) {
+ // 根据 ViewHolder 绑定的位置和传入的位置进行对比
+ // 一般情况下这两个位置值是相等的,但是有一种特殊的情况
+ // 在外层添加头部 View 的情况下,这两个位置值是不对等的
+ mPositionOffset = position - holder.getAdapterPosition();
+ holder.onBindView(position);
+ }
+
+ /**
+ * 获取RecyclerView 对象,需要在setAdapter之后绑定
+ */
+ public RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 条目 ViewHolder,需要子类 ViewHolder 继承
+ */
+ public abstract class ViewHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener, View.OnLongClickListener {
+
+ public ViewHolder(@LayoutRes int id) {
+ this(LayoutInflater.from(getContext()).inflate(id, getRecyclerView(), false));
+ }
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+
+ // 设置条目的点击和长按事件
+ if (mItemClickListener != null) {
+ itemView.setOnClickListener(this);
+ }
+ if (mItemLongClickListener != null) {
+ itemView.setOnLongClickListener(this);
+ }
+
+ // 设置条目子 View 点击事件
+ if (mChildClickListeners != null) {
+ for (int i = 0; i < mChildClickListeners.size(); i++) {
+ View childView = findViewById(mChildClickListeners.keyAt(i));
+ if (childView != null) {
+ childView.setOnClickListener(this);
+ }
+ }
+ }
+
+ // 设置条目子 View 长按事件
+ if (mChildLongClickListeners != null) {
+ for (int i = 0; i < mChildLongClickListeners.size(); i++) {
+ View childView = findViewById(mChildLongClickListeners.keyAt(i));
+ if (childView != null) {
+ childView.setOnLongClickListener(this);
+ }
+ }
+ }
+ }
+
+ public abstract void onBindView(int position);
+
+ /**
+ * 获取 ViewHolder 位置
+ */
+ protected final int getViewHolderPosition() {
+ // 这里解释一下为什么用 getLayoutPosition 而不用 getAdapterPosition
+ // 如果是使用 getAdapterPosition 会导致一个问题,那就是快速点击删除条目的时候会出现 -1 的情况,因为这个 ViewHolder 已经解绑了
+ // 而使用 getLayoutPosition 则不会出现位置为 -1 的情况,因为解绑之后在布局中不会立马消失,所以不用担心在动画执行中获取位置有异常的情况
+ return getLayoutPosition() + mPositionOffset;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int position = getViewHolderPosition();
+ if (position >= 0 && position < getItemCount()) {
+ if (v == getItemView()) {
+ if(mItemClickListener != null) {
+ mItemClickListener.onItemClick(mRecyclerView, v, position);
+ }
+ } else {
+ if (mChildClickListeners != null) {
+ OnChildClickListener listener = mChildClickListeners.get(v.getId());
+ if (listener != null) {
+ listener.onChildClick(mRecyclerView, v, position);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * {@link View.OnLongClickListener}
+ */
+
+ @Override
+ public boolean onLongClick(View v) {
+ int position = getViewHolderPosition();
+ if (position >= 0 && position < getItemCount()) {
+ if (v == getItemView()) {
+ if (mItemLongClickListener != null) {
+ return mItemLongClickListener.onItemLongClick(mRecyclerView, v, position);
+ }
+ } else {
+ if (mChildLongClickListeners != null) {
+ OnChildLongClickListener listener = mChildLongClickListeners.get(v.getId());
+ if (listener != null) {
+ return listener.onChildLongClick(mRecyclerView, v, position);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public final View getItemView() {
+ return itemView;
+ }
+
+ public final V findViewById(@IdRes int id) {
+ return getItemView().findViewById(id);
+ }
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ // 用户设置了滚动监听,需要给 RecyclerView 设置监听
+ if (mScrollListener != null) {
+ // 添加滚动监听
+ mRecyclerView.addOnScrollListener(mScrollListener);
+ }
+ // 判断当前的布局管理器是否为空,如果为空则设置默认的布局管理器
+ if (mRecyclerView.getLayoutManager() == null) {
+ RecyclerView.LayoutManager layoutManager = generateDefaultLayoutManager(mContext);
+ if (layoutManager != null) {
+ mRecyclerView.setLayoutManager(layoutManager);
+ }
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ // 移除滚动监听
+ if (mScrollListener != null) {
+ mRecyclerView.removeOnScrollListener(mScrollListener);
+ }
+ mRecyclerView = null;
+ }
+
+ /**
+ * 生成默认的布局摆放器
+ */
+ protected RecyclerView.LayoutManager generateDefaultLayoutManager(Context context) {
+ return new LinearLayoutManager(context);
+ }
+
+ /**
+ * 设置 RecyclerView 条目点击监听
+ */
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ checkRecyclerViewState();
+ mItemClickListener = listener;
+ }
+
+ /**
+ * 设置 RecyclerView 条目子 View 点击监听
+ */
+ public void setOnChildClickListener(@IdRes int id, OnChildClickListener listener) {
+ checkRecyclerViewState();
+ if (mChildClickListeners == null) {
+ mChildClickListeners = new SparseArray<>();
+ }
+ mChildClickListeners.put(id, listener);
+ }
+
+ /**
+ * 设置RecyclerView条目长按监听
+ */
+ public void setOnItemLongClickListener(OnItemLongClickListener listener) {
+ checkRecyclerViewState();
+ mItemLongClickListener = listener;
+ }
+
+ /**
+ * 设置 RecyclerView 条目子 View 长按监听
+ */
+ public void setOnChildLongClickListener(@IdRes int id, OnChildLongClickListener listener) {
+ checkRecyclerViewState();
+ if (mChildLongClickListeners == null) {
+ mChildLongClickListeners = new SparseArray<>();
+ }
+ mChildLongClickListeners.put(id, listener);
+ }
+
+ private void checkRecyclerViewState() {
+ if (mRecyclerView != null) {
+ // 必须在 RecyclerView.setAdapter() 之前设置监听
+ throw new IllegalStateException("are you ok?");
+ }
+ }
+
+ /**
+ * 设置 RecyclerView 条目滚动监听
+ */
+ public void setOnScrollingListener(OnScrollingListener listener) {
+ mScrollingListener = listener;
+
+ //如果当前已经有设置滚动监听,再次设置需要移除原有的监听器
+ if (mScrollListener == null) {
+ mScrollListener = new ScrollListener();
+ } else {
+ mRecyclerView.removeOnScrollListener(mScrollListener);
+ }
+ //用户设置了滚动监听,需要给RecyclerView设置监听
+ if (mRecyclerView != null) {
+ //添加滚动监听
+ mRecyclerView.addOnScrollListener(mScrollListener);
+ }
+ }
+
+ /** 自定义滚动监听器 */
+ private ScrollListener mScrollListener;
+
+ private class ScrollListener extends RecyclerView.OnScrollListener {
+
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ if (mScrollingListener == null) {
+ return;
+ }
+
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+
+ if (!recyclerView.canScrollVertically(1)) {
+ // 已经到底了
+ mScrollingListener.onScrollDown(recyclerView);
+ } else if (!recyclerView.canScrollVertically(-1)) {
+ // 已经到顶了
+ mScrollingListener.onScrollTop(recyclerView);
+ }
+
+ } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ // 正在滚动中
+ mScrollingListener.onScrolling(recyclerView);
+ }
+ }
+ }
+
+ /**
+ * RecyclerView 滚动监听类
+ */
+ public interface OnScrollingListener {
+
+ /**
+ * 列表滚动到最顶部
+ *
+ * @param recyclerView RecyclerView 对象
+ */
+ void onScrollTop(RecyclerView recyclerView);
+
+ /**
+ * 列表滚动中
+ *
+ * @param recyclerView RecyclerView 对象
+ */
+ void onScrolling(RecyclerView recyclerView);
+
+ /**
+ * 列表滚动到最底部
+ *
+ * @param recyclerView RecyclerView 对象
+ */
+ void onScrollDown(RecyclerView recyclerView);
+ }
+
+ /**
+ * RecyclerView 条目点击监听类
+ */
+ public interface OnItemClickListener{
+
+ /**
+ * 当 RecyclerView 某个条目被点击时回调
+ *
+ * @param recyclerView RecyclerView 对象
+ * @param itemView 被点击的条目对象
+ * @param position 被点击的条目位置
+ */
+ void onItemClick(RecyclerView recyclerView, View itemView, int position);
+ }
+
+ /**
+ * RecyclerView 条目长按监听类
+ */
+ public interface OnItemLongClickListener {
+
+ /**
+ * 当 RecyclerView 某个条目被长按时回调
+ *
+ * @param recyclerView RecyclerView 对象
+ * @param itemView 被点击的条目对象
+ * @param position 被点击的条目位置
+ * @return 是否拦截事件
+ */
+ boolean onItemLongClick(RecyclerView recyclerView, View itemView, int position);
+ }
+
+ /**
+ * RecyclerView 条目子 View 点击监听类
+ */
+ public interface OnChildClickListener {
+
+ /**
+ * 当 RecyclerView 某个条目 子 View 被点击时回调
+ *
+ * @param recyclerView RecyclerView 对象
+ * @param childView 被点击的条目子 View
+ * @param position 被点击的条目位置
+ */
+ void onChildClick(RecyclerView recyclerView, View childView, int position);
+ }
+
+ /**
+ * RecyclerView 条目子 View 长按监听类
+ */
+ public interface OnChildLongClickListener {
+
+ /**
+ * 当 RecyclerView 某个条目子 View 被长按时回调
+ *
+ * @param recyclerView RecyclerView 对象
+ * @param childView 被点击的条目子 View
+ * @param position 被点击的条目位置
+ */
+ boolean onChildLongClick(RecyclerView recyclerView, View childView, int position);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/BaseDialog.java b/base/src/main/java/com/example/base/BaseDialog.java
new file mode 100644
index 0000000..fec4e7e
--- /dev/null
+++ b/base/src/main/java/com/example/base/BaseDialog.java
@@ -0,0 +1,1350 @@
+package com.example.base;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.base.action.ActivityAction;
+import com.example.base.action.AnimAction;
+import com.example.base.action.ClickAction;
+import com.example.base.action.HandlerAction;
+import com.example.base.action.ResourcesAction;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IdRes;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.StyleRes;
+import androidx.appcompat.app.AppCompatDialog;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/11/24
+ * desc : Dialog 基类
+ */
+public class BaseDialog extends AppCompatDialog implements LifecycleOwner,
+ ActivityAction, ResourcesAction, HandlerAction, ClickAction, AnimAction,
+ DialogInterface.OnShowListener, DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
+
+ private final ListenersWrapper mListeners = new ListenersWrapper<>(this);
+ private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
+
+ private List mShowListeners;
+ private List mCancelListeners;
+ private List mDismissListeners;
+
+ public BaseDialog(Context context) {
+ this(context, R.style.BaseDialogStyle);
+ }
+
+ public BaseDialog(Context context, @StyleRes int themeResId) {
+ super(context, themeResId);
+ }
+
+ /**
+ * 获取 Dialog 的根布局
+ */
+ public View getContentView() {
+ return findViewById(Window.ID_ANDROID_CONTENT);
+ }
+
+ /**
+ * 获取当前设置重心
+ */
+ public int getGravity() {
+ Window window = getWindow();
+ if (window != null) {
+ WindowManager.LayoutParams params = window.getAttributes();
+ return params.gravity;
+ }
+ return Gravity.NO_GRAVITY;
+ }
+
+ /**
+ * 设置宽度
+ */
+ public void setWidth(int width) {
+ Window window = getWindow();
+ if (window != null) {
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.width = width;
+ window.setAttributes(params);
+ }
+ }
+
+ /**
+ * 设置高度
+ */
+ public void setHeight(int height) {
+ Window window = getWindow();
+ if (window != null) {
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.height = height;
+ window.setAttributes(params);
+ }
+ }
+
+ /**
+ * 设置 Dialog 重心
+ */
+ public void setGravity(int gravity) {
+ Window window = getWindow();
+ if (window != null) {
+ window.setGravity(gravity);
+ }
+ }
+
+ /**
+ * 设置 Dialog 的动画
+ */
+ public void setWindowAnimations(@StyleRes int id) {
+ Window window = getWindow();
+ if (window != null) {
+ window.setWindowAnimations(id);
+ }
+ }
+
+ /**
+ * 获取 Dialog 的动画
+ */
+ public int getWindowAnimations() {
+ Window window = getWindow();
+ if (window != null) {
+ return window.getAttributes().windowAnimations;
+ }
+ return BaseDialog.ANIM_DEFAULT;
+ }
+
+ /**
+ * 设置背景遮盖层开关
+ */
+ public void setBackgroundDimEnabled(boolean enabled) {
+ Window window = getWindow();
+ if (window != null) {
+ if (enabled) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ }
+ }
+ }
+
+ /**
+ * 设置背景遮盖层的透明度(前提条件是背景遮盖层开关必须是为开启状态)
+ */
+ public void setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) {
+ Window window = getWindow();
+ if (window != null) {
+ window.setDimAmount(dimAmount);
+ }
+ }
+
+ @Override
+ public void dismiss() {
+ removeCallbacks();
+ View focusView = getCurrentFocus();
+ if (focusView != null) {
+ getSystemService(InputMethodManager.class).hideSoftInputFromWindow(focusView.getWindowToken(), 0);
+ }
+ super.dismiss();
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycle;
+ }
+
+ /**
+ * 设置一个显示监听器
+ *
+ * @param listener 显示监听器对象
+ * @deprecated 请使用 {@link #addOnShowListener(BaseDialog.OnShowListener)}}
+ */
+ @Deprecated
+ @Override
+ public void setOnShowListener(@Nullable DialogInterface.OnShowListener listener) {
+ if (listener == null) {
+ return;
+ }
+ addOnShowListener(new ShowListenerWrapper(listener));
+ }
+
+ /**
+ * 设置一个取消监听器
+ *
+ * @param listener 取消监听器对象
+ * @deprecated 请使用 {@link #addOnCancelListener(BaseDialog.OnCancelListener)}
+ */
+ @Deprecated
+ @Override
+ public void setOnCancelListener(@Nullable DialogInterface.OnCancelListener listener) {
+ if (listener == null) {
+ return;
+ }
+ addOnCancelListener(new CancelListenerWrapper(listener));
+ }
+
+ /**
+ * 设置一个销毁监听器
+ *
+ * @param listener 销毁监听器对象
+ * @deprecated 请使用 {@link #addOnDismissListener(BaseDialog.OnDismissListener)}
+ */
+ @Deprecated
+ @Override
+ public void setOnDismissListener(@Nullable DialogInterface.OnDismissListener listener) {
+ if (listener == null) {
+ return;
+ }
+ addOnDismissListener(new DismissListenerWrapper(listener));
+ }
+
+ /**
+ * 设置一个按键监听器
+ *
+ * @param listener 按键监听器对象
+ * @deprecated 请使用 {@link #setOnKeyListener(BaseDialog.OnKeyListener)}
+ */
+ @Deprecated
+ @Override
+ public void setOnKeyListener(@Nullable DialogInterface.OnKeyListener listener) {
+ super.setOnKeyListener(listener);
+ }
+
+ public void setOnKeyListener(@Nullable BaseDialog.OnKeyListener listener) {
+ super.setOnKeyListener(new KeyListenerWrapper(listener));
+ }
+
+ /**
+ * 添加一个显示监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void addOnShowListener(@Nullable BaseDialog.OnShowListener listener) {
+ if (mShowListeners == null) {
+ mShowListeners = new ArrayList<>();
+ super.setOnShowListener(mListeners);
+ }
+ mShowListeners.add(listener);
+ }
+
+ /**
+ * 添加一个取消监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void addOnCancelListener(@Nullable BaseDialog.OnCancelListener listener) {
+ if (mCancelListeners == null) {
+ mCancelListeners = new ArrayList<>();
+ super.setOnCancelListener(mListeners);
+ }
+ mCancelListeners.add(listener);
+ }
+
+ /**
+ * 添加一个销毁监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void addOnDismissListener(@Nullable BaseDialog.OnDismissListener listener) {
+ if (mDismissListeners == null) {
+ mDismissListeners = new ArrayList<>();
+ super.setOnDismissListener(mListeners);
+ }
+ mDismissListeners.add(listener);
+ }
+
+ /**
+ * 移除一个显示监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void removeOnShowListener(@Nullable BaseDialog.OnShowListener listener) {
+ if (mShowListeners != null) {
+ mShowListeners.remove(listener);
+ }
+ }
+
+ /**
+ * 移除一个取消监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void removeOnCancelListener(@Nullable BaseDialog.OnCancelListener listener) {
+ if (mCancelListeners != null) {
+ mCancelListeners.remove(listener);
+ }
+ }
+
+ /**
+ * 移除一个销毁监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void removeOnDismissListener(@Nullable BaseDialog.OnDismissListener listener) {
+ if (mDismissListeners != null) {
+ mDismissListeners.remove(listener);
+ }
+ }
+
+ /**
+ * 设置显示监听器集合
+ */
+ private void setOnShowListeners(@Nullable List listeners) {
+ super.setOnShowListener(mListeners);
+ mShowListeners = listeners;
+ }
+
+ /**
+ * 设置取消监听器集合
+ */
+ private void setOnCancelListeners(@Nullable List listeners) {
+ super.setOnCancelListener(mListeners);
+ mCancelListeners = listeners;
+ }
+
+ /**
+ * 设置销毁监听器集合
+ */
+ private void setOnDismissListeners(@Nullable List listeners) {
+ super.setOnDismissListener(mListeners);
+ mDismissListeners = listeners;
+ }
+
+ /**
+ * {@link DialogInterface.OnShowListener}
+ */
+ @Override
+ public void onShow(DialogInterface dialog) {
+ mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+
+ if (mShowListeners != null) {
+ for (int i = 0; i < mShowListeners.size(); i++) {
+ mShowListeners.get(i).onShow(this);
+ }
+ }
+ }
+
+ /**
+ * {@link DialogInterface.OnCancelListener}
+ */
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (mCancelListeners != null) {
+ for (int i = 0; i < mCancelListeners.size(); i++) {
+ mCancelListeners.get(i).onCancel(this);
+ }
+ }
+ }
+
+ /**
+ * {@link DialogInterface.OnDismissListener}
+ */
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ if (mDismissListeners != null) {
+ for (int i = 0; i < mDismissListeners.size(); i++) {
+ mDismissListeners.get(i).onDismiss(this);
+ }
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static class Builder implements LifecycleOwner, ActivityAction, ResourcesAction, ClickAction {
+
+ /** 上下文对象 */
+ private final Context mContext;
+ /** Dialog 对象 */
+ private BaseDialog mDialog;
+ /** Dialog 布局 */
+ private View mContentView;
+
+ /** 主题样式 */
+ private int mThemeId = R.style.BaseDialogStyle;
+ /** 动画样式 */
+ private int mAnimStyle = BaseDialog.ANIM_DEFAULT;
+ /** 重心位置 */
+ private int mGravity = Gravity.NO_GRAVITY;
+
+ /** 水平偏移 */
+ private int mXOffset;
+ /** 垂直偏移 */
+ private int mYOffset;
+
+ /** 宽度和高度 */
+ private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ /** 背景遮盖层开关 */
+ private boolean mBackgroundDimEnabled = true;
+ /** 背景遮盖层透明度 */
+ private float mBackgroundDimAmount = 0.5f;
+
+ /** 是否能够被取消 */
+ private boolean mCancelable = true;
+ /** 点击空白是否能够取消 前提是这个对话框可以被取消 */
+ private boolean mCanceledOnTouchOutside = true;
+
+ /** Dialog Show 监听 */
+ private List mOnShowListeners;
+ /** Dialog Cancel 监听 */
+ private List mOnCancelListeners;
+ /** Dialog Dismiss 监听 */
+ private List mOnDismissListeners;
+ /** Dialog Key 监听 */
+ private BaseDialog.OnKeyListener mOnKeyListener;
+
+ /** 点击事件集合 */
+ private SparseArray mClickArray;
+
+ public Builder(Activity activity) {
+ this((Context) activity);
+ }
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * 设置主题 id
+ */
+ public B setThemeStyle(@StyleRes int id) {
+ if (isCreated()) {
+ // Dialog 创建之后不能再设置主题 id
+ throw new IllegalStateException("are you ok?");
+ }
+ mThemeId = id;
+ return (B) this;
+ }
+
+ /**
+ * 设置布局
+ */
+ public B setContentView(@LayoutRes int id) {
+ // 这里解释一下,为什么要传 new FrameLayout,因为如果不传的话,XML 的根布局获取到的 LayoutParams 对象会为空,也就会导致宽高参数解析不出来
+ return setContentView(LayoutInflater.from(mContext).inflate(id, new FrameLayout(mContext), false));
+ }
+ public B setContentView(View view) {
+ mContentView = view;
+
+ if (isCreated()) {
+ mDialog.setContentView(view);
+ } else {
+ if (mContentView != null) {
+ ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
+ if (layoutParams != null && mWidth == ViewGroup.LayoutParams.WRAP_CONTENT && mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ // 如果当前 Dialog 的宽高设置了自适应,就以布局中设置的宽高为主
+ setWidth(layoutParams.width);
+ setHeight(layoutParams.height);
+ }
+
+ // 如果当前没有设置重心,就自动获取布局重心
+ if (mGravity == Gravity.NO_GRAVITY) {
+ if (layoutParams instanceof FrameLayout.LayoutParams) {
+ setGravity(((FrameLayout.LayoutParams) layoutParams).gravity);
+ } else if (layoutParams instanceof LinearLayout.LayoutParams) {
+ setGravity(((LinearLayout.LayoutParams) layoutParams).gravity);
+ } else {
+ // 默认重心是居中
+ setGravity(Gravity.CENTER);
+ }
+ }
+ }
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置重心位置
+ */
+ public B setGravity(int gravity) {
+ // 适配 Android 4.2 新特性,布局反方向(开发者选项 - 强制使用从右到左的布局方向)
+ mGravity = gravity;
+ if (isCreated()) {
+ mDialog.setGravity(gravity);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置水平偏移
+ */
+ public B setXOffset(int offset) {
+ mXOffset = offset;
+ return (B) this;
+ }
+
+ /**
+ * 设置垂直偏移
+ */
+ public B setYOffset(int offset) {
+ mYOffset = offset;
+ return (B) this;
+ }
+
+ /**
+ * 设置宽度
+ */
+ public B setWidth(int width) {
+ mWidth = width;
+ if (isCreated()) {
+ mDialog.setWidth(width);
+ } else {
+ ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null;
+ if (params != null) {
+ params.width = width;
+ mContentView.setLayoutParams(params);
+ }
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置高度
+ */
+ public B setHeight(int height) {
+ mHeight = height;
+ if (isCreated()) {
+ mDialog.setHeight(height);
+ } else {
+ // 这里解释一下为什么要重新设置 LayoutParams
+ // 因为如果不这样设置的话,第一次显示的时候会按照 Dialog 宽高显示
+ // 但是 Layout 内容变更之后就不会按照之前的设置宽高来显示
+ // 所以这里我们需要对 View 的 LayoutParams 也进行设置
+ ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null;
+ if (params != null) {
+ params.height = height;
+ mContentView.setLayoutParams(params);
+ }
+ }
+ return (B) this;
+ }
+
+ /**
+ * 是否可以取消
+ */
+ public B setCancelable(boolean cancelable) {
+ mCancelable = cancelable;
+ if (isCreated()) {
+ mDialog.setCancelable(cancelable);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 是否可以通过点击空白区域取消
+ */
+ public B setCanceledOnTouchOutside(boolean cancel) {
+ mCanceledOnTouchOutside = cancel;
+ if (isCreated() && mCancelable) {
+ mDialog.setCanceledOnTouchOutside(cancel);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置动画,已经封装好几种样式,具体可见{@link AnimAction}类
+ */
+ public B setAnimStyle(@StyleRes int id) {
+ mAnimStyle = id;
+ if (isCreated()) {
+ mDialog.setWindowAnimations(id);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置背景遮盖层开关
+ */
+ public B setBackgroundDimEnabled(boolean enabled) {
+ mBackgroundDimEnabled = enabled;
+ if (isCreated()) {
+ mDialog.setBackgroundDimEnabled(enabled);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置背景遮盖层的透明度(前提条件是背景遮盖层开关必须是为开启状态)
+ */
+ public B setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) {
+ mBackgroundDimAmount = dimAmount;
+ if (isCreated()) {
+ mDialog.setBackgroundDimAmount(dimAmount);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 添加显示监听
+ */
+ public B addOnShowListener(@NonNull BaseDialog.OnShowListener listener) {
+ if (isCreated()) {
+ mDialog.addOnShowListener(listener);
+ } else {
+ if (mOnShowListeners == null) {
+ mOnShowListeners = new ArrayList<>();
+ }
+ mOnShowListeners.add(listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 添加取消监听
+ */
+ public B addOnCancelListener(@NonNull BaseDialog.OnCancelListener listener) {
+ if (isCreated()) {
+ mDialog.addOnCancelListener(listener);
+ } else {
+ if (mOnCancelListeners == null) {
+ mOnCancelListeners = new ArrayList<>();
+ }
+ mOnCancelListeners.add(listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 添加销毁监听
+ */
+ public B addOnDismissListener(@NonNull BaseDialog.OnDismissListener listener) {
+ if (isCreated()) {
+ mDialog.addOnDismissListener(listener);
+ } else {
+ if (mOnDismissListeners == null) {
+ mOnDismissListeners = new ArrayList<>();
+ }
+ mOnDismissListeners.add(listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置按键监听
+ */
+ public B setOnKeyListener(@NonNull BaseDialog.OnKeyListener listener) {
+ if (isCreated()) {
+ mDialog.setOnKeyListener(listener);
+ } else {
+ mOnKeyListener = listener;
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置文本
+ */
+ public B setText(@IdRes int viewId, @StringRes int stringId) {
+ return setText(viewId, getString(stringId));
+ }
+ public B setText(@IdRes int id, CharSequence text) {
+ ((TextView) findViewById(id)).setText(text);
+ return (B) this;
+ }
+
+ /**
+ * 设置文本颜色
+ */
+ public B setTextColor(@IdRes int id, @ColorInt int color) {
+ ((TextView) findViewById(id)).setTextColor(color);
+ return (B) this;
+ }
+
+ /**
+ * 设置提示
+ */
+ public B setHint(@IdRes int viewId, @StringRes int stringId) {
+ return setHint(viewId, getString(stringId));
+ }
+ public B setHint(@IdRes int id, CharSequence text) {
+ ((TextView) findViewById(id)).setHint(text);
+ return (B) this;
+ }
+
+ /**
+ * 设置可见状态
+ */
+ public B setVisibility(@IdRes int id, int visibility) {
+ findViewById(id).setVisibility(visibility);
+ return (B) this;
+ }
+
+ /**
+ * 设置背景
+ */
+ public B setBackground(@IdRes int viewId, @DrawableRes int drawableId) {
+ return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId));
+ }
+ public B setBackground(@IdRes int id, Drawable drawable) {
+ findViewById(id).setBackground(drawable);
+ return (B) this;
+ }
+
+ /**
+ * 设置图片
+ */
+ public B setImageDrawable(@IdRes int viewId, @DrawableRes int drawableId) {
+ return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId));
+ }
+ public B setImageDrawable(@IdRes int id, Drawable drawable) {
+ ((ImageView) findViewById(id)).setImageDrawable(drawable);
+ return (B) this;
+ }
+
+ /**
+ * 设置点击事件
+ */
+ public B setOnClickListener(@IdRes int id, @NonNull BaseDialog.OnClickListener listener) {
+ if (isCreated()) {
+ View view = mDialog.findViewById(id);
+ if (view != null) {
+ view.setOnClickListener(new ViewClickWrapper(mDialog, listener));
+ }
+ } else {
+ if (mClickArray == null) {
+ mClickArray = new SparseArray<>();
+ }
+ mClickArray.put(id, listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 创建
+ */
+ @SuppressLint("RtlHardcoded")
+ public BaseDialog create() {
+
+ // 判断布局是否为空
+ if (mContentView == null) {
+ throw new IllegalArgumentException("are you ok?");
+ }
+
+ // 如果当前没有设置重心,就设置一个默认的重心
+ if (mGravity == Gravity.NO_GRAVITY) {
+ mGravity = Gravity.CENTER;
+ }
+
+ // 如果当前没有设置动画效果,就设置一个默认的动画效果
+ if (mAnimStyle == BaseDialog.ANIM_DEFAULT) {
+ switch (mGravity) {
+ case Gravity.TOP:
+ mAnimStyle = BaseDialog.ANIM_TOP;
+ break;
+ case Gravity.BOTTOM:
+ mAnimStyle = BaseDialog.ANIM_BOTTOM;
+ break;
+ case Gravity.LEFT:
+ mAnimStyle = BaseDialog.ANIM_LEFT;
+ break;
+ case Gravity.RIGHT:
+ mAnimStyle = BaseDialog.ANIM_RIGHT;
+ break;
+ default:
+ mAnimStyle = BaseDialog.ANIM_DEFAULT;
+ break;
+ }
+ }
+
+ mDialog = createDialog(mContext, mThemeId);
+
+ mDialog.setContentView(mContentView);
+ mDialog.setCancelable(mCancelable);
+ if (mCancelable) {
+ mDialog.setCanceledOnTouchOutside(mCanceledOnTouchOutside);
+ }
+
+ // 设置参数
+ Window window = mDialog.getWindow();
+ if (window != null) {
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.width = mWidth;
+ params.height = mHeight;
+ params.gravity = mGravity;
+ params.x = mXOffset;
+ params.y = mYOffset;
+ params.windowAnimations = mAnimStyle;
+ window.setAttributes(params);
+ if (mBackgroundDimEnabled) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ window.setDimAmount(mBackgroundDimAmount);
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ }
+ }
+
+ if (mOnShowListeners != null) {
+ mDialog.setOnShowListeners(mOnShowListeners);
+ }
+
+ if (mOnCancelListeners != null) {
+ mDialog.setOnCancelListeners(mOnCancelListeners);
+ }
+
+ if (mOnDismissListeners != null) {
+ mDialog.setOnDismissListeners(mOnDismissListeners);
+ }
+
+ if (mOnKeyListener != null) {
+ mDialog.setOnKeyListener(mOnKeyListener);
+ }
+
+ for (int i = 0; mClickArray != null && i < mClickArray.size(); i++) {
+ mContentView.findViewById(mClickArray.keyAt(i)).setOnClickListener(new ViewClickWrapper(mDialog, mClickArray.valueAt(i)));
+ }
+
+ Activity activity = getActivity();
+ if (activity != null) {
+ DialogLifecycle.with(activity, mDialog);
+ }
+
+ return mDialog;
+ }
+
+ /**
+ * 显示
+ */
+ public BaseDialog show() {
+ if (!isCreated()) {
+ create();
+ }
+ mDialog.show();
+ return mDialog;
+ }
+
+ /**
+ * 销毁当前 Dialog
+ */
+ public void dismiss() {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 当前 Dialog 是否创建了
+ */
+ public boolean isCreated() {
+ return mDialog != null;
+ }
+
+ /**
+ * 当前 Dialog 是否显示了
+ */
+ public boolean isShowing() {
+ return mDialog != null && mDialog.isShowing();
+ }
+
+ /**
+ * 创建 Dialog 对象(子类可以重写此方法来改变 Dialog 类型)
+ */
+ protected BaseDialog createDialog(Context context, @StyleRes int themeId) {
+ return new BaseDialog(context, themeId);
+ }
+
+ /**
+ * 延迟执行
+ */
+ public final void post(Runnable r) {
+ if (isShowing()) {
+ mDialog.post(r);
+ } else {
+ addOnShowListener(new ShowPostWrapper(r));
+ }
+ }
+
+ /**
+ * 延迟一段时间执行
+ */
+ public final void postDelayed(Runnable r, long delayMillis) {
+ if (isShowing()) {
+ mDialog.postDelayed(r, delayMillis);
+ } else {
+ addOnShowListener(new ShowPostDelayedWrapper(r, delayMillis));
+ }
+ }
+
+ /**
+ * 在指定的时间执行
+ */
+ public final void postAtTime(Runnable r, long uptimeMillis) {
+ if (isShowing()) {
+ mDialog.postAtTime(r, uptimeMillis);
+ } else {
+ addOnShowListener(new ShowPostAtTimeWrapper(r, uptimeMillis));
+ }
+ }
+
+ /**
+ * 获取 Dialog 的根布局
+ */
+ public View getContentView() {
+ return mContentView;
+ }
+
+ /**
+ * 根据 id 查找 View
+ */
+ @Override
+ public V findViewById(@IdRes int id) {
+ if (mContentView == null) {
+ // 没有 setContentView 就想 findViewById ?
+ throw new IllegalStateException("are you ok?");
+ }
+ return mContentView.findViewById(id);
+ }
+
+ /**
+ * 获取当前 Dialog 对象
+ */
+ @Nullable
+ public BaseDialog getDialog() {
+ return mDialog;
+ }
+
+ @Nullable
+ @Override
+ public Lifecycle getLifecycle() {
+ if (mDialog != null) {
+ return mDialog.getLifecycle();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Dialog 生命周期管理
+ */
+ private static final class DialogLifecycle implements
+ Application.ActivityLifecycleCallbacks,
+ BaseDialog.OnShowListener,
+ BaseDialog.OnDismissListener {
+
+ private static void with(Activity activity, BaseDialog dialog) {
+ new DialogLifecycle(activity, dialog);
+ }
+
+ private BaseDialog mDialog;
+ private Activity mActivity;
+
+ /** Dialog 动画样式(避免 Dialog 从后台返回到前台后再次触发动画效果) */
+ private int mDialogAnim;
+
+ private DialogLifecycle(Activity activity, BaseDialog dialog) {
+ mActivity = activity;
+ dialog.addOnShowListener(this);
+ dialog.addOnDismissListener(this);
+ }
+
+ @Override
+ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}
+
+ @Override
+ public void onActivityStarted(@NonNull Activity activity) {}
+
+ @Override
+ public void onActivityResumed(@NonNull Activity activity) {
+ if (mActivity != activity) {
+ return;
+ }
+
+ if (mDialog != null && mDialog.isShowing()) {
+ // 还原 Dialog 动画样式(这里必须要使用延迟设置,否则还是有一定几率会出现)
+ mDialog.postDelayed(() -> {
+ if (mDialog != null && mDialog.isShowing()) {
+ mDialog.setWindowAnimations(mDialogAnim);
+ }
+ }, 100);
+ }
+ }
+
+ @Override
+ public void onActivityPaused(@NonNull Activity activity) {
+ if (mActivity != activity) {
+ return;
+ }
+
+ if (mDialog != null && mDialog.isShowing()) {
+ // 获取 Dialog 动画样式
+ mDialogAnim = mDialog.getWindowAnimations();
+ // 设置 Dialog 无动画效果
+ mDialog.setWindowAnimations(BaseDialog.ANIM_EMPTY);
+ }
+ }
+
+ @Override
+ public void onActivityStopped(@NonNull Activity activity) {}
+
+ @Override
+ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
+
+ @Override
+ public void onActivityDestroyed(@NonNull Activity activity) {
+ if (mActivity != activity) {
+ return;
+ }
+
+ if (mDialog != null) {
+ mDialog.removeOnShowListener(this);
+ mDialog.removeOnDismissListener(this);
+ if (mDialog.isShowing()) {
+ mDialog.dismiss();
+ }
+ mDialog = null;
+ }
+ unregisterActivityLifecycleCallbacks();
+ // 释放 Activity 对象
+ mActivity = null;
+ }
+
+ @Override
+ public void onShow(BaseDialog dialog) {
+ mDialog = dialog;
+ registerActivityLifecycleCallbacks();
+ }
+
+ @Override
+ public void onDismiss(BaseDialog dialog) {
+ mDialog = null;
+ unregisterActivityLifecycleCallbacks();
+ }
+
+ /**
+ * 注册 Activity 生命周期监听
+ */
+ private void registerActivityLifecycleCallbacks() {
+ if (mActivity == null) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ mActivity.registerActivityLifecycleCallbacks(this);
+ } else {
+ mActivity.getApplication().registerActivityLifecycleCallbacks(this);
+ }
+ }
+
+ /**
+ * 反注册 Activity 生命周期监听
+ */
+ private void unregisterActivityLifecycleCallbacks() {
+ if (mActivity == null) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ mActivity.unregisterActivityLifecycleCallbacks(this);
+ } else {
+ mActivity.getApplication().unregisterActivityLifecycleCallbacks(this);
+ }
+ }
+ }
+
+ /**
+ * Dialog 监听包装类(修复原生 Dialog 监听器对象导致的内存泄漏)
+ */
+ private static final class ListenersWrapper
+ extends SoftReference implements DialogInterface.OnShowListener, DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
+
+ private ListenersWrapper(T referent) {
+ super(referent);
+ }
+
+ @Override
+ public void onShow(DialogInterface dialog) {
+ if (get() != null) {
+ get().onShow(dialog);
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (get() != null) {
+ get().onCancel(dialog);
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (get() != null) {
+ get().onDismiss(dialog);
+ }
+ }
+ }
+
+ /**
+ * 点击事件包装类
+ */
+ private static final class ViewClickWrapper
+ implements View.OnClickListener {
+
+ private final BaseDialog mDialog;
+ private final BaseDialog.OnClickListener mListener;
+
+ private ViewClickWrapper(BaseDialog dialog, BaseDialog.OnClickListener listener) {
+ mDialog = dialog;
+ mListener = listener;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final void onClick(View v) {
+ mListener.onClick(mDialog, v);
+ }
+ }
+
+ /**
+ * 显示监听包装类
+ */
+ private static final class ShowListenerWrapper
+ extends SoftReference
+ implements BaseDialog.OnShowListener {
+
+ private ShowListenerWrapper(DialogInterface.OnShowListener referent) {
+ super(referent);
+ }
+
+ @Override
+ public void onShow(BaseDialog dialog) {
+ // 在横竖屏切换后监听对象会为空
+ if (get() != null) {
+ get().onShow(dialog);
+ }
+ }
+ }
+
+ /**
+ * 取消监听包装类
+ */
+ private static final class CancelListenerWrapper
+ extends SoftReference
+ implements BaseDialog.OnCancelListener {
+
+ private CancelListenerWrapper(DialogInterface.OnCancelListener referent) {
+ super(referent);
+ }
+
+ @Override
+ public void onCancel(BaseDialog dialog) {
+ // 在横竖屏切换后监听对象会为空
+ if (get() != null) {
+ get().onCancel(dialog);
+ }
+ }
+ }
+
+ /**
+ * 销毁监听包装类
+ */
+ private static final class DismissListenerWrapper
+ extends SoftReference
+ implements BaseDialog.OnDismissListener {
+
+ private DismissListenerWrapper(DialogInterface.OnDismissListener referent) {
+ super(referent);
+ }
+
+ @Override
+ public void onDismiss(BaseDialog dialog) {
+ // 在横竖屏切换后监听对象会为空
+ if (get() != null) {
+ get().onDismiss(dialog);
+ }
+ }
+ }
+
+ /**
+ * 按键监听包装类
+ */
+ private static final class KeyListenerWrapper
+ implements DialogInterface.OnKeyListener {
+
+ private final BaseDialog.OnKeyListener mListener;
+
+ private KeyListenerWrapper(BaseDialog.OnKeyListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+ // 在横竖屏切换后监听对象会为空
+ if (mListener != null && dialog instanceof BaseDialog) {
+ mListener.onKey((BaseDialog) dialog, event);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * post 任务包装类
+ */
+ private static final class ShowPostWrapper implements OnShowListener {
+
+ private final Runnable mRunnable;
+
+ private ShowPostWrapper(Runnable r) {
+ mRunnable = r;
+ }
+
+ @Override
+ public void onShow(BaseDialog dialog) {
+ if (mRunnable != null) {
+ dialog.removeOnShowListener(this);
+ dialog.post(mRunnable);
+ }
+ }
+ }
+
+ /**
+ * postDelayed 任务包装类
+ */
+ private static final class ShowPostDelayedWrapper implements OnShowListener {
+
+ private final Runnable mRunnable;
+ private final long mDelayMillis;
+
+ private ShowPostDelayedWrapper(Runnable r, long delayMillis) {
+ mRunnable = r;
+ mDelayMillis = delayMillis;
+ }
+
+ @Override
+ public void onShow(BaseDialog dialog) {
+ if (mRunnable != null) {
+ dialog.removeOnShowListener(this);
+ dialog.postDelayed(mRunnable, mDelayMillis);
+ }
+ }
+ }
+
+ /**
+ * postAtTime 任务包装类
+ */
+ private static final class ShowPostAtTimeWrapper implements OnShowListener {
+
+ private final Runnable mRunnable;
+ private final long mUptimeMillis;
+
+ private ShowPostAtTimeWrapper(Runnable r, long uptimeMillis) {
+ mRunnable = r;
+ mUptimeMillis = uptimeMillis;
+ }
+
+ @Override
+ public void onShow(BaseDialog dialog) {
+ if (mRunnable != null) {
+ dialog.removeOnShowListener(this);
+ dialog.postAtTime(mRunnable, mUptimeMillis);
+ }
+ }
+ }
+
+ /**
+ * 点击监听器
+ */
+ public interface OnClickListener {
+ void onClick(BaseDialog dialog, V view);
+ }
+
+ /**
+ * 显示监听器
+ */
+ public interface OnShowListener {
+
+ /**
+ * Dialog 显示了
+ */
+ void onShow(BaseDialog dialog);
+ }
+
+ /**
+ * 取消监听器
+ */
+ public interface OnCancelListener {
+
+ /**
+ * Dialog 取消了
+ */
+ void onCancel(BaseDialog dialog);
+ }
+
+ /**
+ * 销毁监听器
+ */
+ public interface OnDismissListener {
+
+ /**
+ * Dialog 销毁了
+ */
+ void onDismiss(BaseDialog dialog);
+ }
+
+ /**
+ * 按键监听器
+ */
+ public interface OnKeyListener {
+
+ /**
+ * 触发了按键
+ */
+ boolean onKey(BaseDialog dialog, KeyEvent event);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/BaseFragment.java b/base/src/main/java/com/example/base/BaseFragment.java
new file mode 100644
index 0000000..619aed3
--- /dev/null
+++ b/base/src/main/java/com/example/base/BaseFragment.java
@@ -0,0 +1,186 @@
+package com.example.base;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.base.action.ActivityAction;
+import com.example.base.action.BundleAction;
+import com.example.base.action.ClickAction;
+import com.example.base.action.HandlerAction;
+import com.example.base.action.ResourcesAction;
+
+import java.util.Random;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : Fragment 基类
+ */
+public abstract class BaseFragment extends Fragment implements
+ ActivityAction, ResourcesAction, HandlerAction, ClickAction, BundleAction {
+
+ /** Activity 对象 */
+ private A mActivity;
+ /** 根布局 */
+ private View mRootView;
+ /** 当前是否加载过 */
+ private boolean mLoading;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ // 获得全局的 Activity
+ mActivity = (A) requireActivity();
+ }
+
+ @Override
+ public void onDetach() {
+ removeCallbacks();
+ mActivity = null;
+ super.onDetach();
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mLoading = false;
+ if (getLayoutId() > 0) {
+ return mRootView = inflater.inflate(getLayoutId(), null);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ mLoading = false;
+ mRootView = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (!mLoading) {
+ mLoading = true;
+ initFragment();
+ }
+ }
+
+ /**
+ * 这个 Fragment 是否已经加载过了
+ */
+ public boolean isLoading() {
+ return mLoading;
+ }
+
+ @NonNull
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ /**
+ * 获取绑定的 Activity,防止出现 getActivity 为空
+ */
+ public A getAttachActivity() {
+ return mActivity;
+ }
+
+ protected void initFragment() {
+ initView();
+ initData();
+ }
+
+ /**
+ * 获取布局 ID
+ */
+ protected abstract int getLayoutId();
+
+ /**
+ * 初始化控件
+ */
+ protected abstract void initView();
+
+ /**
+ * 初始化数据
+ */
+ protected abstract void initData();
+
+ /**
+ * 根据资源 id 获取一个 View 对象
+ */
+ @Override
+ public V findViewById(@IdRes int id) {
+ return mRootView.findViewById(id);
+ }
+
+ @Override
+ public Bundle getBundle() {
+ return getArguments();
+ }
+
+ /**
+ * startActivityForResult 方法优化
+ */
+
+ private BaseActivity.OnActivityCallback mActivityCallback;
+ private int mActivityRequestCode;
+
+ public void startActivityForResult(Class extends Activity> clazz, BaseActivity.OnActivityCallback callback) {
+ startActivityForResult(new Intent(mActivity, clazz), null, callback);
+ }
+
+ public void startActivityForResult(Intent intent, BaseActivity.OnActivityCallback callback) {
+ startActivityForResult(intent, null, callback);
+ }
+
+ public void startActivityForResult(Intent intent, Bundle options, BaseActivity.OnActivityCallback callback) {
+ // 回调还没有结束,所以不能再次调用此方法,这个方法只适合一对一回调,其他需求请使用原生的方法实现
+ if (mActivityCallback == null) {
+ mActivityCallback = callback;
+ // 随机生成请求码,这个请求码必须在 2 的 16 次幂以内,也就是 0 - 65535
+ mActivityRequestCode = new Random().nextInt((int) Math.pow(2, 16));
+ startActivityForResult(intent, mActivityRequestCode, options);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (mActivityCallback != null && mActivityRequestCode == requestCode) {
+ mActivityCallback.onActivityResult(resultCode, data);
+ mActivityCallback = null;
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ /**
+ * 销毁当前 Fragment 所在的 Activity
+ */
+ public void finish() {
+ if (mActivity != null && !mActivity.isFinishing()) {
+ mActivity.finish();
+ }
+ }
+
+ /**
+ * Fragment 返回键被按下时回调
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // 默认不拦截按键事件,回传给 Activity
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/BaseFragmentAdapter.java b/base/src/main/java/com/example/base/BaseFragmentAdapter.java
new file mode 100644
index 0000000..68d346f
--- /dev/null
+++ b/base/src/main/java/com/example/base/BaseFragmentAdapter.java
@@ -0,0 +1,135 @@
+package com.example.base;
+
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : FragmentPagerAdapter 基类
+ */
+public class BaseFragmentAdapter extends FragmentPagerAdapter {
+
+ /** Fragment 集合 */
+ private final List mFragmentSet = new ArrayList<>();
+ /** Fragment 标题 */
+ private final List mFragmentTitle = new ArrayList<>();
+
+ /** 当前显示的Fragment */
+ private F mShowFragment;
+
+ /** 当前 ViewPager */
+ private ViewPager mViewPager;
+
+ /** 设置成懒加载模式 */
+ private boolean mLazyMode = true;
+
+ public BaseFragmentAdapter(FragmentActivity activity) {
+ this(activity.getSupportFragmentManager());
+ }
+
+ public BaseFragmentAdapter(Fragment fragment) {
+ this(fragment.getChildFragmentManager());
+ }
+
+ public BaseFragmentAdapter(FragmentManager manager) {
+ super(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
+ }
+
+ @NonNull
+ @Override
+ public F getItem(int position) {
+ return mFragmentSet.get(position);
+ }
+
+ @Override
+ public int getCount() {
+ return mFragmentSet.size();
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mFragmentTitle.get(position);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
+ if (getShowFragment() != object) {
+ // 记录当前的Fragment对象
+ mShowFragment = (F) object;
+ }
+ super.setPrimaryItem(container, position, object);
+ }
+
+ /**
+ * 添加 Fragment
+ */
+ public void addFragment(F fragment) {
+ addFragment(fragment, null);
+ }
+
+ public void addFragment(F fragment, CharSequence title) {
+ mFragmentSet.add(fragment);
+ mFragmentTitle.add(title);
+ if (mViewPager != null) {
+ notifyDataSetChanged();
+ if (mLazyMode) {
+ mViewPager.setOffscreenPageLimit(getCount());
+ }
+ }
+ }
+
+ /**
+ * 获取当前的Fragment
+ */
+ public F getShowFragment() {
+ return mShowFragment;
+ }
+
+ @Override
+ public void startUpdate(@NonNull ViewGroup container) {
+ super.startUpdate(container);
+ if (container instanceof ViewPager) {
+ // 记录绑定 ViewPager
+ mViewPager = (ViewPager) container;
+ refreshLazyMode();
+ }
+ }
+
+ /**
+ * 设置懒加载模式
+ */
+ public void setLazyMode(boolean lazy) {
+ mLazyMode = lazy;
+ refreshLazyMode();
+ }
+
+ /**
+ * 刷新加载模式
+ */
+ private void refreshLazyMode() {
+ if (mViewPager == null) {
+ return;
+ }
+
+ if (mLazyMode) {
+ // 设置成懒加载模式(也就是不限制 Fragment 展示的数量)
+ mViewPager.setOffscreenPageLimit(getCount());
+ } else {
+ mViewPager.setOffscreenPageLimit(1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/BasePopupWindow.java b/base/src/main/java/com/example/base/BasePopupWindow.java
new file mode 100644
index 0000000..ed5857f
--- /dev/null
+++ b/base/src/main/java/com/example/base/BasePopupWindow.java
@@ -0,0 +1,904 @@
+package com.example.base;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.example.base.action.ActivityAction;
+import com.example.base.action.AnimAction;
+import com.example.base.action.ClickAction;
+import com.example.base.action.HandlerAction;
+import com.example.base.action.ResourcesAction;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IdRes;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.StyleRes;
+import androidx.core.content.ContextCompat;
+import androidx.core.widget.PopupWindowCompat;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/16
+ * desc : PopupWindow 基类
+ */
+public class BasePopupWindow extends PopupWindow
+ implements ActivityAction, HandlerAction, ClickAction,
+ AnimAction, PopupWindow.OnDismissListener {
+
+ private final Context mContext;
+ private PopupBackground mPopupBackground;
+
+ private List mShowListeners;
+ private List mDismissListeners;
+
+ public BasePopupWindow(@NonNull Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 设置一个销毁监听器
+ *
+ * @param listener 销毁监听器对象
+ * @deprecated 请使用 {@link #addOnDismissListener(BasePopupWindow.OnDismissListener)}
+ */
+ @Deprecated
+ @Override
+ public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
+ if (listener == null) {
+ return;
+ }
+ addOnDismissListener(new DismissListenerWrapper(listener));
+ }
+
+ /**
+ * 添加一个显示监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void addOnShowListener(@Nullable BasePopupWindow.OnShowListener listener) {
+ if (mShowListeners == null) {
+ mShowListeners = new ArrayList<>();
+ }
+ mShowListeners.add(listener);
+ }
+
+ /**
+ * 添加一个销毁监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void addOnDismissListener(@Nullable BasePopupWindow.OnDismissListener listener) {
+ if (mDismissListeners == null) {
+ mDismissListeners = new ArrayList<>();
+ super.setOnDismissListener(this);
+ }
+ mDismissListeners.add(listener);
+ }
+
+ /**
+ * 移除一个显示监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void removeOnShowListener(@Nullable BasePopupWindow.OnShowListener listener) {
+ if (mShowListeners != null) {
+ mShowListeners.remove(listener);
+ }
+ }
+
+ /**
+ * 移除一个销毁监听器
+ *
+ * @param listener 监听器对象
+ */
+ public void removeOnDismissListener(@Nullable BasePopupWindow.OnDismissListener listener) {
+ if (mDismissListeners != null) {
+ mDismissListeners.remove(listener);
+ }
+ }
+
+ /**
+ * 设置显示监听器集合
+ */
+ private void setOnShowListeners(@Nullable List listeners) {
+ mShowListeners = listeners;
+ }
+
+ /**
+ * 设置销毁监听器集合
+ */
+ private void setOnDismissListeners(@Nullable List listeners) {
+ super.setOnDismissListener(this);
+ mDismissListeners = listeners;
+ }
+
+ /**
+ * {@link PopupWindow.OnDismissListener}
+ */
+ @Override
+ public void onDismiss() {
+ if (mDismissListeners != null) {
+ for (BasePopupWindow.OnDismissListener listener : mDismissListeners) {
+ listener.onDismiss(this);
+ }
+ }
+ }
+
+ @Override
+ public void showAsDropDown(View anchor, int xOff, int yOff, int gravity) {
+ if (isShowing() || getContentView() == null) {
+ return;
+ }
+
+ if (mShowListeners != null) {
+ for (BasePopupWindow.OnShowListener listener : mShowListeners) {
+ listener.onShow(this);
+ }
+ }
+ super.showAsDropDown(anchor, xOff, yOff, gravity);
+ }
+
+ @Override
+ public void showAtLocation(View parent, int gravity, int x, int y) {
+ if (isShowing() || getContentView() == null) {
+ return;
+ }
+
+ if (mShowListeners != null) {
+ for (BasePopupWindow.OnShowListener listener : mShowListeners) {
+ listener.onShow(this);
+ }
+ }
+ super.showAtLocation(parent, gravity, x, y);
+ }
+
+ @Override
+ public void dismiss() {
+ removeCallbacks();
+ super.dismiss();
+ }
+
+ @Override
+ public V findViewById(@IdRes int id) {
+ return getContentView().findViewById(id);
+ }
+
+ @Override
+ public void setWindowLayoutType(int type) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ super.setWindowLayoutType(type);
+ } else {
+ PopupWindowCompat.setWindowLayoutType(this, type);
+ }
+ }
+
+ @Override
+ public int getWindowLayoutType() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return super.getWindowLayoutType();
+ } else {
+ return PopupWindowCompat.getWindowLayoutType(this);
+ }
+ }
+
+ @Override
+ public void setOverlapAnchor(boolean overlapAnchor) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ super.setOverlapAnchor(overlapAnchor);
+ } else {
+ PopupWindowCompat.setOverlapAnchor(this, overlapAnchor);
+ }
+ }
+
+ /**
+ * 设置背景遮盖层的透明度
+ */
+ public void setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) {
+ float alpha = 1 - dimAmount;
+ if (isShowing()) {
+ setActivityAlpha(alpha);
+ }
+ if (mPopupBackground == null && alpha != 1) {
+ mPopupBackground = new PopupBackground();
+ addOnShowListener(mPopupBackground);
+ addOnDismissListener(mPopupBackground);
+ }
+ if (mPopupBackground != null) {
+ mPopupBackground.setAlpha(alpha);
+ }
+ }
+
+ /**
+ * 设置 Activity 窗口透明度
+ */
+ private void setActivityAlpha(float alpha) {
+ if (mContext instanceof Activity) {
+ Activity activity = (Activity) mContext;
+ WindowManager.LayoutParams params = activity.getWindow().getAttributes();
+
+ final ValueAnimator animator = ValueAnimator.ofFloat(params.alpha, alpha);
+ animator.setDuration(300);
+ animator.addUpdateListener(animation -> {
+ float value = (float) animation.getAnimatedValue();
+ if (value != params.alpha) {
+ params.alpha = value;
+ activity.getWindow().setAttributes(params);
+ }
+ });
+ animator.start();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static class Builder implements ResourcesAction, ClickAction {
+
+ private static final int DEFAULT_ANCHORED_GRAVITY = Gravity.TOP | Gravity.START;
+
+ /** Context 对象 */
+ private final Context mContext;
+ /** PopupWindow 布局 */
+ private View mContentView;
+ /** PopupWindow 对象 */
+ private BasePopupWindow mPopupWindow;
+
+ /** PopupWindow Show 监听 */
+ private List mOnShowListeners;
+ /** PopupWindow Dismiss 监听 */
+ private List mOnDismissListeners;
+
+ /** 动画 */
+ private int mAnimations = BasePopupWindow.ANIM_DEFAULT;
+ /** 位置 */
+ private int mGravity = DEFAULT_ANCHORED_GRAVITY;
+ /** 宽度和高度 */
+ private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ /** 是否可触摸 */
+ private boolean mTouchable = true;
+ /** 是否有焦点 */
+ private boolean mFocusable = true;
+ /** 是否外层可触摸 */
+ private boolean mOutsideTouchable = false;
+
+ /** 背景遮盖层透明度 */
+ private float mBackgroundDimAmount;
+
+ /** X 轴偏移 */
+ private int mXOffset;
+ /** Y 轴偏移 */
+ private int mYOffset;
+
+ /** 点击事件集合 */
+ private SparseArray mClickArray;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * 设置布局
+ */
+ public B setContentView(@LayoutRes int id) {
+ // 这里解释一下,为什么要传 new FrameLayout,因为如果不传的话,XML 的根布局获取到的 LayoutParams 对象会为空,也就会导致宽高解析不出来
+ return setContentView(LayoutInflater.from(mContext).inflate(id, new FrameLayout(mContext), false));
+ }
+ public B setContentView(View view) {
+ mContentView = view;
+
+ if (isCreated()) {
+ mPopupWindow.setContentView(view);
+ } else {
+ if (mContentView != null) {
+ ViewGroup.LayoutParams params = mContentView.getLayoutParams();
+ if (params != null && mWidth == ViewGroup.LayoutParams.WRAP_CONTENT && mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ // 如果当前 PopupWindow 的宽高设置了自适应,就以布局中设置的宽高为主
+ setWidth(params.width);
+ setHeight(params.height);
+ }
+
+ // 如果当前没有设置重心,就自动获取布局重心
+ if (mGravity == DEFAULT_ANCHORED_GRAVITY) {
+ if (params instanceof FrameLayout.LayoutParams) {
+ setGravity(((FrameLayout.LayoutParams) params).gravity);
+ } else if (params instanceof LinearLayout.LayoutParams) {
+ setGravity(((LinearLayout.LayoutParams) params).gravity);
+ } else {
+ // 默认重心是居中
+ setGravity(Gravity.CENTER);
+ }
+ }
+ }
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置重心位置
+ */
+ public B setGravity(int gravity) {
+ // 适配 Android 4.2 新特性,布局反方向(开发者选项 - 强制使用从右到左的布局方向)
+ mGravity = Gravity.getAbsoluteGravity(gravity, getResources().getConfiguration().getLayoutDirection());
+ return (B) this;
+ }
+
+ /**
+ * 设置宽度
+ */
+ public B setWidth(int width) {
+ mWidth = width;
+ if (isCreated()) {
+ mPopupWindow.setWidth(width);
+ } else {
+ ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null;
+ if (params != null) {
+ params.width = width;
+ mContentView.setLayoutParams(params);
+ }
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置高度
+ */
+ public B setHeight(int height) {
+ mHeight = height;
+ if (isCreated()) {
+ mPopupWindow.setHeight(height);
+ } else {
+ // 这里解释一下为什么要重新设置 LayoutParams
+ // 因为如果不这样设置的话,第一次显示的时候会按照 PopupWindow 宽高显示
+ // 但是 Layout 内容变更之后就不会按照之前的设置宽高来显示
+ // 所以这里我们需要对 View 的 LayoutParams 也进行设置
+ ViewGroup.LayoutParams params = mContentView != null ? mContentView.getLayoutParams() : null;
+ if (params != null) {
+ params.height = height;
+ mContentView.setLayoutParams(params);
+ }
+ }
+ return (B) this;
+ }
+
+ /**
+ * 是否可触摸
+ */
+ public B setTouchable(boolean touchable) {
+ mTouchable = touchable;
+ return (B) this;
+ }
+
+ /**
+ * 是否有焦点
+ */
+ public B setFocusable(boolean focusable) {
+ mFocusable = focusable;
+ return (B) this;
+ }
+
+ /**
+ * 是否外层可触摸
+ */
+ public B setOutsideTouchable(boolean touchable) {
+ mOutsideTouchable = touchable;
+ return (B) this;
+ }
+
+ /**
+ * 设置水平偏移量
+ */
+ public B setXOffset(int offset) {
+ mXOffset = offset;
+ return (B) this;
+ }
+
+ /**
+ * 设置垂直偏移量
+ */
+ public B setYOffset(int offset) {
+ mYOffset = offset;
+ return (B) this;
+ }
+
+ /**
+ * 设置动画,已经封装好几种样式,具体可见{@link AnimAction}类
+ */
+ public B setAnimStyle(@StyleRes int id) {
+ mAnimations = id;
+ if (isCreated()) {
+ mPopupWindow.setAnimationStyle(id);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置背景遮盖层的透明度
+ */
+ public B setBackgroundDimAmount(@FloatRange(from = 0.0, to = 1.0) float dimAmount) {
+ mBackgroundDimAmount = dimAmount;
+ if (isShowing()) {
+ mPopupWindow.setBackgroundDimAmount(dimAmount);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 添加显示监听
+ */
+ public B addOnShowListener(@NonNull BasePopupWindow.OnShowListener listener) {
+ if (isCreated()) {
+ mPopupWindow.addOnShowListener(listener);
+ } else {
+ if (mOnShowListeners == null) {
+ mOnShowListeners = new ArrayList<>();
+ }
+ mOnShowListeners.add(listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 添加销毁监听
+ */
+ public B addOnDismissListener(@NonNull BasePopupWindow.OnDismissListener listener) {
+ if (isCreated()) {
+ mPopupWindow.addOnDismissListener(listener);
+ } else {
+ if (mOnDismissListeners == null) {
+ mOnDismissListeners = new ArrayList<>();
+ }
+ mOnDismissListeners.add(listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 设置文本
+ */
+ public B setText(@IdRes int viewId, @StringRes int stringId) {
+ return setText(viewId, getString(stringId));
+ }
+ public B setText(@IdRes int id, CharSequence text) {
+ ((TextView) findViewById(id)).setText(text);
+ return (B) this;
+ }
+
+ /**
+ * 设置文本颜色
+ */
+ public B setTextColor(@IdRes int id, @ColorInt int color) {
+ ((TextView) findViewById(id)).setTextColor(color);
+ return (B) this;
+ }
+
+ /**
+ * 设置提示
+ */
+ public B setHint(@IdRes int viewId, @StringRes int stringId) {
+ return setHint(viewId, getString(stringId));
+ }
+ public B setHint(@IdRes int id, CharSequence text) {
+ ((TextView) findViewById(id)).setHint(text);
+ return (B) this;
+ }
+
+ /**
+ * 设置可见状态
+ */
+ public B setVisibility(@IdRes int id, int visibility) {
+ findViewById(id).setVisibility(visibility);
+ return (B) this;
+ }
+
+ /**
+ * 设置背景
+ */
+ public B setBackground(@IdRes int viewId, @DrawableRes int drawableId) {
+ return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId));
+ }
+ public B setBackground(@IdRes int id, Drawable drawable) {
+ findViewById(id).setBackground(drawable);
+ return (B) this;
+ }
+
+ /**
+ * 设置图片
+ */
+ public B setImageDrawable(@IdRes int viewId, @DrawableRes int drawableId) {
+ return setBackground(viewId, ContextCompat.getDrawable(mContext, drawableId));
+ }
+ public B setImageDrawable(@IdRes int id, Drawable drawable) {
+ ((ImageView) findViewById(id)).setImageDrawable(drawable);
+ return (B) this;
+ }
+
+ /**
+ * 设置点击事件
+ */
+ public B setOnClickListener(@IdRes int id, @NonNull BasePopupWindow.OnClickListener listener) {
+ if (isCreated()) {
+ View view = mPopupWindow.findViewById(id);
+ if (view != null) {
+ view.setOnClickListener(new ViewClickWrapper(mPopupWindow, listener));
+ }
+ } else {
+ if (mClickArray == null) {
+ mClickArray = new SparseArray<>();
+ }
+ mClickArray.put(id, listener);
+ }
+ return (B) this;
+ }
+
+ /**
+ * 创建
+ */
+ @SuppressLint("RtlHardcoded")
+ public BasePopupWindow create() {
+
+ // 判断布局是否为空
+ if (mContentView == null) {
+ throw new IllegalArgumentException("are you ok?");
+ }
+
+ // 如果当前没有设置重心,就设置一个默认的重心
+ if (mGravity == DEFAULT_ANCHORED_GRAVITY) {
+ mGravity = Gravity.CENTER;
+ }
+
+ // 如果当前没有设置动画效果,就设置一个默认的动画效果
+ if (mAnimations == BasePopupWindow.ANIM_DEFAULT) {
+ switch (mGravity) {
+ case Gravity.TOP:
+ mAnimations = BasePopupWindow.ANIM_TOP;
+ break;
+ case Gravity.BOTTOM:
+ mAnimations = BasePopupWindow.ANIM_BOTTOM;
+ break;
+ case Gravity.LEFT:
+ mAnimations = BasePopupWindow.ANIM_LEFT;
+ break;
+ case Gravity.RIGHT:
+ mAnimations = BasePopupWindow.ANIM_RIGHT;
+ break;
+ default:
+ mAnimations = BasePopupWindow.ANIM_DEFAULT;
+ break;
+ }
+ }
+
+ mPopupWindow = createPopupWindow(mContext);
+ mPopupWindow.setContentView(mContentView);
+ mPopupWindow.setWidth(mWidth);
+ mPopupWindow.setHeight(mHeight);
+ mPopupWindow.setAnimationStyle(mAnimations);
+ mPopupWindow.setTouchable(mTouchable);
+ mPopupWindow.setFocusable(mFocusable);
+ mPopupWindow.setOutsideTouchable(mOutsideTouchable);
+ mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+
+ if (mOnShowListeners != null) {
+ mPopupWindow.setOnShowListeners(mOnShowListeners);
+ }
+
+ if (mOnDismissListeners != null) {
+ mPopupWindow.setOnDismissListeners(mOnDismissListeners);
+ }
+
+ mPopupWindow.setBackgroundDimAmount(mBackgroundDimAmount);
+
+ for (int i = 0; mClickArray != null && i < mClickArray.size(); i++) {
+ mContentView.findViewById(mClickArray.keyAt(i)).setOnClickListener(new BasePopupWindow.ViewClickWrapper(mPopupWindow, mClickArray.valueAt(i)));
+ }
+ return mPopupWindow;
+ }
+
+ /**
+ * 显示为下拉
+ */
+ public BasePopupWindow showAsDropDown(View anchor) {
+ if (!isCreated()) {
+ create();
+ }
+ mPopupWindow.showAsDropDown(anchor, mXOffset, mYOffset, mGravity);
+ return mPopupWindow;
+ }
+
+ /**
+ * 显示在指定位置
+ */
+ public BasePopupWindow showAtLocation(View parent) {
+ if (!isCreated()) {
+ create();
+ }
+ mPopupWindow.showAtLocation(parent, mGravity, mXOffset, mYOffset);
+ return mPopupWindow;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 当前 PopupWindow 是否创建了
+ */
+ public boolean isCreated() {
+ return mPopupWindow != null;
+ }
+
+ /**
+ * 当前 PopupWindow 是否显示了
+ */
+ public boolean isShowing() {
+ return mPopupWindow != null && mPopupWindow.isShowing();
+ }
+
+ /**
+ * 销毁当前 PopupWindow
+ */
+ public void dismiss() {
+ if (mPopupWindow != null) {
+ mPopupWindow.dismiss();
+ }
+ }
+
+ /**
+ * 创建 PopupWindow 对象(子类可以重写此方法来改变 PopupWindow 类型)
+ */
+ protected BasePopupWindow createPopupWindow(Context context) {
+ return new BasePopupWindow(context);
+ }
+
+ /**
+ * 获取 PopupWindow 的根布局
+ */
+ public View getContentView() {
+ return mContentView;
+ }
+
+ /**
+ * 根据 id 查找 View
+ */
+ @Override
+ public V findViewById(@IdRes int id) {
+ if (mContentView == null) {
+ // 没有 setContentView 就想 findViewById ?
+ throw new IllegalStateException("are you ok?");
+ }
+ return mContentView.findViewById(id);
+ }
+
+ /**
+ * 获取当前 PopupWindow 对象
+ */
+ @Nullable
+ public BasePopupWindow getPopupWindow() {
+ return mPopupWindow;
+ }
+
+ /**
+ * 延迟执行
+ */
+ public final void post(Runnable r) {
+ if (isShowing()) {
+ mPopupWindow.post(r);
+ } else {
+ addOnShowListener(new ShowPostWrapper(r));
+ }
+ }
+
+ /**
+ * 延迟一段时间执行
+ */
+ public final void postDelayed(Runnable r, long delayMillis) {
+ if (isShowing()) {
+ mPopupWindow.postDelayed(r, delayMillis);
+ } else {
+ addOnShowListener(new ShowPostDelayedWrapper(r, delayMillis));
+ }
+ }
+
+ /**
+ * 在指定的时间执行
+ */
+ public final void postAtTime(Runnable r, long uptimeMillis) {
+ if (isShowing()) {
+ mPopupWindow.postAtTime(r, uptimeMillis);
+ } else {
+ addOnShowListener(new ShowPostAtTimeWrapper(r, uptimeMillis));
+ }
+ }
+ }
+
+ /**
+ * PopupWindow 背景遮盖层实现类
+ */
+ private static class PopupBackground implements
+ BasePopupWindow.OnShowListener,
+ BasePopupWindow.OnDismissListener {
+
+ private float mAlpha;
+
+ private void setAlpha(float alpha) {
+ mAlpha = alpha;
+ }
+
+ @Override
+ public void onShow(BasePopupWindow popupWindow) {
+ popupWindow.setActivityAlpha(mAlpha);
+ }
+
+ @Override
+ public void onDismiss(BasePopupWindow popupWindow) {
+ popupWindow.setActivityAlpha(1);
+ }
+ }
+
+ /**
+ * 销毁监听包装类
+ */
+ private static final class DismissListenerWrapper
+ extends SoftReference
+ implements BasePopupWindow.OnDismissListener {
+
+ private DismissListenerWrapper(PopupWindow.OnDismissListener referent) {
+ super(referent);
+ }
+
+ @Override
+ public void onDismiss(BasePopupWindow popupWindow) {
+ // 在横竖屏切换后监听对象会为空
+ if (get() != null) {
+ get().onDismiss();
+ }
+ }
+ }
+
+ /**
+ * 点击事件包装类
+ */
+ private static final class ViewClickWrapper
+ implements View.OnClickListener {
+
+ private final BasePopupWindow mBasePopupWindow;
+ private final BasePopupWindow.OnClickListener mListener;
+
+ private ViewClickWrapper(BasePopupWindow popupWindow, BasePopupWindow.OnClickListener listener) {
+ mBasePopupWindow = popupWindow;
+ mListener = listener;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final void onClick(View v) {
+ mListener.onClick(mBasePopupWindow, v);
+ }
+ }
+
+ /**
+ * post 任务包装类
+ */
+ private static final class ShowPostWrapper implements OnShowListener {
+
+ private final Runnable mRunnable;
+
+ private ShowPostWrapper(Runnable r) {
+ mRunnable = r;
+ }
+
+ @Override
+ public void onShow(BasePopupWindow dialog) {
+ if (mRunnable != null) {
+ dialog.removeOnShowListener(this);
+ dialog.post(mRunnable);
+ }
+ }
+ }
+
+ /**
+ * postDelayed 任务包装类
+ */
+ private static final class ShowPostDelayedWrapper implements OnShowListener {
+
+ private final Runnable mRunnable;
+ private final long mDelayMillis;
+
+ private ShowPostDelayedWrapper(Runnable r, long delayMillis) {
+ mRunnable = r;
+ mDelayMillis = delayMillis;
+ }
+
+ @Override
+ public void onShow(BasePopupWindow dialog) {
+ if (mRunnable != null) {
+ dialog.removeOnShowListener(this);
+ dialog.postDelayed(mRunnable, mDelayMillis);
+ }
+ }
+ }
+
+ /**
+ * postAtTime 任务包装类
+ */
+ private static final class ShowPostAtTimeWrapper implements OnShowListener {
+
+ private final Runnable mRunnable;
+ private final long mUptimeMillis;
+
+ private ShowPostAtTimeWrapper(Runnable r, long uptimeMillis) {
+ mRunnable = r;
+ mUptimeMillis = uptimeMillis;
+ }
+
+ @Override
+ public void onShow(BasePopupWindow dialog) {
+ if (mRunnable != null) {
+ dialog.removeOnShowListener(this);
+ dialog.postAtTime(mRunnable, mUptimeMillis);
+ }
+ }
+ }
+
+ /**
+ * 点击监听器
+ */
+ public interface OnClickListener {
+ void onClick(BasePopupWindow popupWindow, V view);
+ }
+
+ /**
+ * 显示监听器
+ */
+ public interface OnShowListener {
+
+ /**
+ * PopupWindow 显示了
+ */
+ void onShow(BasePopupWindow popupWindow);
+ }
+
+ /**
+ * 销毁监听器
+ */
+ public interface OnDismissListener {
+
+ /**
+ * PopupWindow 销毁了
+ */
+ void onDismiss(BasePopupWindow popupWindow);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/action/ActivityAction.java b/base/src/main/java/com/example/base/action/ActivityAction.java
new file mode 100644
index 0000000..832865f
--- /dev/null
+++ b/base/src/main/java/com/example/base/action/ActivityAction.java
@@ -0,0 +1,55 @@
+package com.example.base.action;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2020/03/08
+ * desc : Activity 相关意图
+ */
+public interface ActivityAction {
+
+ /**
+ * 获取 Context
+ */
+ Context getContext();
+
+ /**
+ * 获取 Activity
+ */
+ default Activity getActivity() {
+ Context context = getContext();
+ do {
+ if (context instanceof Activity) {
+ return (Activity) context;
+ } else if (context instanceof ContextWrapper){
+ context = ((ContextWrapper) context).getBaseContext();
+ } else {
+ return null;
+ }
+ } while (context != null);
+ return null;
+ }
+
+ /**
+ * 启动一个 Activity(简化版)
+ */
+ default void startActivity(Class extends Activity> clazz) {
+ startActivity(new Intent(getContext(), clazz));
+ }
+
+ /**
+ * 启动一个 Activity
+ */
+ default void startActivity(Intent intent) {
+ if (!(getContext() instanceof Activity)) {
+ // 如果当前的上下文不是 Activity,调用 startActivity 必须加入新任务栈的标记
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ getContext().startActivity(intent);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/action/AnimAction.java b/base/src/main/java/com/example/base/action/AnimAction.java
new file mode 100644
index 0000000..3daa49c
--- /dev/null
+++ b/base/src/main/java/com/example/base/action/AnimAction.java
@@ -0,0 +1,40 @@
+package com.example.base.action;
+
+
+import com.example.base.R;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/21
+ * desc : 动画样式
+ */
+public interface AnimAction {
+
+ /** 默认动画效果 */
+ int ANIM_DEFAULT = -1;
+
+ /** 没有动画效果 */
+ int ANIM_EMPTY = 0;
+
+ /** 缩放动画 */
+ int ANIM_SCALE = R.style.ScaleAnimStyle;
+
+ /** IOS 动画 */
+ int ANIM_IOS = R.style.IOSAnimStyle;
+
+ /** 吐司动画 */
+ int ANIM_TOAST = android.R.style.Animation_Toast;
+
+ /** 顶部弹出动画 */
+ int ANIM_TOP = R.style.TopAnimStyle;
+
+ /** 底部弹出动画 */
+ int ANIM_BOTTOM = R.style.BottomAnimStyle;
+
+ /** 左边弹出动画 */
+ int ANIM_LEFT = R.style.LeftAnimStyle;
+
+ /** 右边弹出动画 */
+ int ANIM_RIGHT = R.style.RightAnimStyle;
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/action/BundleAction.java b/base/src/main/java/com/example/base/action/BundleAction.java
new file mode 100644
index 0000000..139120d
--- /dev/null
+++ b/base/src/main/java/com/example/base/action/BundleAction.java
@@ -0,0 +1,82 @@
+package com.example.base.action;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+import androidx.annotation.Nullable;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/10/23
+ * desc : 参数意图
+ */
+public interface BundleAction {
+
+ @Nullable
+ Bundle getBundle();
+
+ default int getInt(String name) {
+ return getInt(name, 0);
+ }
+
+ default int getInt(String name, int defaultValue) {
+ return getBundle() == null ? defaultValue : getBundle().getInt(name, defaultValue);
+ }
+
+ default long getLong(String name) {
+ return getLong(name, 0);
+ }
+
+ default long getLong(String name, int defaultValue) {
+ return getBundle() == null ? defaultValue : getBundle().getLong(name, defaultValue);
+ }
+
+ default float getFloat(String name) {
+ return getFloat(name, 0);
+ }
+
+ default float getFloat(String name, int defaultValue) {
+ return getBundle() == null ? defaultValue : getBundle().getFloat(name, defaultValue);
+ }
+
+ default double getDouble(String name) {
+ return getDouble(name, 0);
+ }
+
+ default double getDouble(String name, int defaultValue) {
+ return getBundle() == null ? defaultValue : getBundle().getDouble(name, defaultValue);
+ }
+
+ default boolean getBoolean(String name) {
+ return getBoolean(name, false);
+ }
+
+ default boolean getBoolean(String name, boolean defaultValue) {
+ return getBundle() == null ? defaultValue : getBundle().getBoolean(name, defaultValue);
+ }
+
+ default String getString(String name) {
+ return getBundle() == null ? null : getBundle().getString(name);
+ }
+
+ default P getParcelable(String name) {
+ return getBundle() == null ? null : getBundle().getParcelable(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ default S getSerializable(String name) {
+ return (S) (getBundle() == null ? null : getBundle().getSerializable(name));
+ }
+
+ default ArrayList getStringArrayList(String name) {
+ return getBundle() == null ? null : getBundle().getStringArrayList(name);
+ }
+
+ default ArrayList getIntegerArrayList(String name) {
+ return getBundle() == null ? null : getBundle().getIntegerArrayList(name);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/action/ClickAction.java b/base/src/main/java/com/example/base/action/ClickAction.java
new file mode 100644
index 0000000..67e738e
--- /dev/null
+++ b/base/src/main/java/com/example/base/action/ClickAction.java
@@ -0,0 +1,33 @@
+package com.example.base.action;
+
+import android.view.View;
+
+import androidx.annotation.IdRes;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/15
+ * desc : 点击事件意图
+ */
+public interface ClickAction extends View.OnClickListener {
+
+ V findViewById(@IdRes int id);
+
+ @Override
+ default void onClick(View v) {
+ // 默认不实现,让子类实现
+ }
+
+ default void setOnClickListener(@IdRes int... ids) {
+ for (int id : ids) {
+ findViewById(id).setOnClickListener(this);
+ }
+ }
+
+ default void setOnClickListener(View... views) {
+ for (View view : views) {
+ view.setOnClickListener(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/action/HandlerAction.java b/base/src/main/java/com/example/base/action/HandlerAction.java
new file mode 100644
index 0000000..000b867
--- /dev/null
+++ b/base/src/main/java/com/example/base/action/HandlerAction.java
@@ -0,0 +1,62 @@
+package com.example.base.action;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/15
+ * desc : Handler 意图处理
+ */
+public interface HandlerAction {
+
+ Handler HANDLER = new Handler(Looper.getMainLooper());
+
+ /**
+ * 获取 Handler
+ */
+ default Handler getHandler() {
+ return HANDLER;
+ }
+
+ /**
+ * 延迟执行
+ */
+ default boolean post(Runnable r) {
+ return postDelayed(r, 0);
+ }
+
+ /**
+ * 延迟一段时间执行
+ */
+ default boolean postDelayed(Runnable r, long delayMillis) {
+ if (delayMillis < 0) {
+ delayMillis = 0;
+ }
+ return postAtTime(r, SystemClock.uptimeMillis() + delayMillis);
+ }
+
+ /**
+ * 在指定的时间执行
+ */
+ default boolean postAtTime(Runnable r, long uptimeMillis) {
+ // 发送和这个 Activity 相关的消息回调
+ return HANDLER.postAtTime(r, this, uptimeMillis);
+ }
+
+ /**
+ * 移除单个消息回调
+ */
+ default void removeCallbacks(Runnable r) {
+ HANDLER.removeCallbacks(r);
+ }
+
+ /**
+ * 移除全部消息回调
+ */
+ default void removeCallbacks() {
+ HANDLER.removeCallbacksAndMessages(this);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/example/base/action/ResourcesAction.java b/base/src/main/java/com/example/base/action/ResourcesAction.java
new file mode 100644
index 0000000..2c87c0c
--- /dev/null
+++ b/base/src/main/java/com/example/base/action/ResourcesAction.java
@@ -0,0 +1,66 @@
+package com.example.base.action;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/15
+ * desc : Context 意图处理(扩展非 Context 类的方法,禁止 Context 类实现此接口)
+ */
+public interface ResourcesAction {
+
+ /**
+ * 获取 Context
+ */
+ Context getContext();
+
+ /**
+ * 获取资源对象(仅供子类调用)
+ */
+ default Resources getResources() {
+ return getContext().getResources();
+ }
+
+ /**
+ * 根据 id 获取一个文本
+ */
+ default String getString(@StringRes int id) {
+ return getContext().getString(id);
+ }
+
+ default String getString(@StringRes int id, Object... formatArgs) {
+ return getResources().getString(id, formatArgs);
+ }
+
+ /**
+ * 根据 id 获取一个 Drawable
+ */
+ default Drawable getDrawable(@DrawableRes int id) {
+ return ContextCompat.getDrawable(getContext(), id);
+ }
+
+ /**
+ * 根据 id 获取一个颜色
+ */
+ @ColorInt
+ default int getColor(@ColorRes int id) {
+ return ContextCompat.getColor(getContext(), id);
+ }
+
+ /**
+ * 获取系统服务
+ */
+ default S getSystemService(@NonNull Class serviceClass) {
+ return ContextCompat.getSystemService(getContext(), serviceClass);
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/res/anim/bottom_in_window.xml b/base/src/main/res/anim/bottom_in_window.xml
new file mode 100644
index 0000000..416cc3e
--- /dev/null
+++ b/base/src/main/res/anim/bottom_in_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/bottom_out_window.xml b/base/src/main/res/anim/bottom_out_window.xml
new file mode 100644
index 0000000..b62f8c5
--- /dev/null
+++ b/base/src/main/res/anim/bottom_out_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/fall_down_item.xml b/base/src/main/res/anim/fall_down_item.xml
new file mode 100644
index 0000000..a13e21e
--- /dev/null
+++ b/base/src/main/res/anim/fall_down_item.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/fall_down_layout.xml b/base/src/main/res/anim/fall_down_layout.xml
new file mode 100644
index 0000000..32cb14b
--- /dev/null
+++ b/base/src/main/res/anim/fall_down_layout.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/from_bottom_item.xml b/base/src/main/res/anim/from_bottom_item.xml
new file mode 100644
index 0000000..f6563f1
--- /dev/null
+++ b/base/src/main/res/anim/from_bottom_item.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/from_bottom_layout.xml b/base/src/main/res/anim/from_bottom_layout.xml
new file mode 100644
index 0000000..8b905e7
--- /dev/null
+++ b/base/src/main/res/anim/from_bottom_layout.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/from_right_item.xml b/base/src/main/res/anim/from_right_item.xml
new file mode 100644
index 0000000..9fdc6b4
--- /dev/null
+++ b/base/src/main/res/anim/from_right_item.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/from_right_layout.xml b/base/src/main/res/anim/from_right_layout.xml
new file mode 100644
index 0000000..02ef45c
--- /dev/null
+++ b/base/src/main/res/anim/from_right_layout.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/ios_in_window.xml b/base/src/main/res/anim/ios_in_window.xml
new file mode 100644
index 0000000..3aae57e
--- /dev/null
+++ b/base/src/main/res/anim/ios_in_window.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/ios_out_window.xml b/base/src/main/res/anim/ios_out_window.xml
new file mode 100644
index 0000000..4333adc
--- /dev/null
+++ b/base/src/main/res/anim/ios_out_window.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/left_in_window.xml b/base/src/main/res/anim/left_in_window.xml
new file mode 100644
index 0000000..42aa534
--- /dev/null
+++ b/base/src/main/res/anim/left_in_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/left_out_window.xml b/base/src/main/res/anim/left_out_window.xml
new file mode 100644
index 0000000..a646ba9
--- /dev/null
+++ b/base/src/main/res/anim/left_out_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/right_in_window.xml b/base/src/main/res/anim/right_in_window.xml
new file mode 100644
index 0000000..5b07f5a
--- /dev/null
+++ b/base/src/main/res/anim/right_in_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/right_out_window.xml b/base/src/main/res/anim/right_out_window.xml
new file mode 100644
index 0000000..ed38c36
--- /dev/null
+++ b/base/src/main/res/anim/right_out_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/scale_in_window.xml b/base/src/main/res/anim/scale_in_window.xml
new file mode 100644
index 0000000..ef519b3
--- /dev/null
+++ b/base/src/main/res/anim/scale_in_window.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/scale_out_window.xml b/base/src/main/res/anim/scale_out_window.xml
new file mode 100644
index 0000000..005c749
--- /dev/null
+++ b/base/src/main/res/anim/scale_out_window.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/top_in_window.xml b/base/src/main/res/anim/top_in_window.xml
new file mode 100644
index 0000000..23a7998
--- /dev/null
+++ b/base/src/main/res/anim/top_in_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/anim/top_out_window.xml b/base/src/main/res/anim/top_out_window.xml
new file mode 100644
index 0000000..1d5bd0b
--- /dev/null
+++ b/base/src/main/res/anim/top_out_window.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/res/values/colors.xml b/base/src/main/res/values/colors.xml
new file mode 100644
index 0000000..14140de
--- /dev/null
+++ b/base/src/main/res/values/colors.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ #00000000
+
+
+ #FFFFFFFF
+ #F2FFFFFF
+ #E6FFFFFF
+ #D9FFFFFF
+ #CCFFFFFF
+ #BFFFFFFF
+ #B3FFFFFF
+ #A6FFFFFF
+ #99FFFFFF
+ #8CFFFFFF
+ #80FFFFFF
+ #73FFFFFF
+ #66FFFFFF
+ #59FFFFFF
+ #4DFFFFFF
+ #40FFFFFF
+ #33FFFFFF
+ #26FFFFFF
+ #1AFFFFFF
+ #0DFFFFFF
+
+
+ #FF000000
+ #F2000000
+ #E6000000
+ #D9000000
+ #CC000000
+ #B000000F
+ #B3000000
+ #A6000000
+ #99000000
+ #8C000000
+ #80000000
+ #73000000
+ #66000000
+ #59000000
+ #4D000000
+ #40000000
+ #33000000
+ #26000000
+ #1A000000
+ #0D000000
+
+
+ #FF808080
+
+ #FFFF0000
+
+ #FFFFD700
+
+ #FFFFFF00
+
+ #FF008000
+
+ #FF0000FF
+
+ #FF800080
+
+ #FFFFC0CB
+
+ #FFFFA500
+
+
\ No newline at end of file
diff --git a/base/src/main/res/values/integers.xml b/base/src/main/res/values/integers.xml
new file mode 100644
index 0000000..3616732
--- /dev/null
+++ b/base/src/main/res/values/integers.xml
@@ -0,0 +1,5 @@
+
+
+ 300
+ 400
+
\ No newline at end of file
diff --git a/base/src/main/res/values/styles.xml b/base/src/main/res/values/styles.xml
new file mode 100644
index 0000000..dcb3781
--- /dev/null
+++ b/base/src/main/res/values/styles.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/test/java/com/example/base/ExampleUnitTest.java b/base/src/test/java/com/example/base/ExampleUnitTest.java
new file mode 100644
index 0000000..f6c4d6f
--- /dev/null
+++ b/base/src/test/java/com/example/base/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.base;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..ad6d153
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,38 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ maven {
+ url 'https://maven.google.com/'
+ name 'Google'
+ }
+ maven { url "https://oss.jfrog.org/libs-snapshot" }
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:4.0.1"
+ classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' // add plugin
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ maven {
+ url 'https://maven.google.com/'
+ name 'Google'
+ }
+ maven { url "https://oss.jfrog.org/libs-snapshot" }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/config.gradle b/config.gradle
new file mode 100644
index 0000000..bd9ad68
--- /dev/null
+++ b/config.gradle
@@ -0,0 +1,43 @@
+// 通用配置
+android {
+
+ compileSdkVersion 29
+ defaultConfig {
+ minSdkVersion 24
+ targetSdkVersion 29
+ versionName '1.0'
+ versionCode 10
+ }
+
+ // 支持 Java JDK 8
+ compileOptions {
+ targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ checkReleaseBuilds false
+ abortOnError false
+ }
+
+ // 设置存放 so 文件的目录
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['libs']
+ }
+ }
+}
+
+dependencies {
+ // 依赖 libs 目录下所有 jar 包
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ // 依赖 libs 目录下所有 aar 包
+ implementation fileTree(include: ['*.aar'], dir: 'libs')
+
+ // 谷歌兼容库:https://developer.android.google.cn/jetpack/androidx/releases/appcompat?hl=zh-cn
+ implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
+ implementation 'com.google.android.material:material:1.3.0-alpha01'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
+
+
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..c52ac9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3a5f9ed
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Nov 13 10:29:28 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..7ce3b55
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,4 @@
+include ':widget'
+include ':base'
+include ':app'
+rootProject.name = "Alcoholic"
\ No newline at end of file
diff --git a/up_load_txt.json b/up_load_txt.json
new file mode 100644
index 0000000..1cc02f3
--- /dev/null
+++ b/up_load_txt.json
@@ -0,0 +1,7 @@
+{
+ "minMustUpCode":1,
+ "vAPKDownUrl":"https://github.com/ymwm-lxl/Alcoholic/raw/master/app/release/Alcoholic_v1.0_release_1123.apk",
+ "vCode":2,
+ "vName":"2.0",
+ "vUpContent":"更新的一个版本"
+}
\ No newline at end of file
diff --git a/widget/.gitignore b/widget/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/widget/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/widget/build.gradle b/widget/build.gradle
new file mode 100644
index 0000000..376897c
--- /dev/null
+++ b/widget/build.gradle
@@ -0,0 +1,7 @@
+apply plugin: 'com.android.library'
+apply from: '../config.gradle'
+
+dependencies {
+ // 基础库(不包任何第三方框架)
+ implementation project(':base')
+}
\ No newline at end of file
diff --git a/widget/consumer-rules.pro b/widget/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/widget/proguard-rules.pro b/widget/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/widget/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/widget/src/androidTest/java/com/example/widget/ExampleInstrumentedTest.java b/widget/src/androidTest/java/com/example/widget/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..a471e66
--- /dev/null
+++ b/widget/src/androidTest/java/com/example/widget/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.example.widget;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.example.widget.test", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/AndroidManifest.xml b/widget/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6466e2b
--- /dev/null
+++ b/widget/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+ /
+
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/layout/CustomViewStub.java b/widget/src/main/java/com/example/widget/layout/CustomViewStub.java
new file mode 100644
index 0000000..8779ef3
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/layout/CustomViewStub.java
@@ -0,0 +1,117 @@
+package com.example.widget.layout;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.example.widget.R;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/07/06
+ * desc : 自定义 ViewStub(原生 ViewStub 的缺点:继承至 View,不支持 findViewById、动态添加和移除 View、监听显示隐藏)
+ */
+public final class CustomViewStub extends FrameLayout {
+
+ private OnViewStubListener mListener;
+
+ private final int mLayoutResource;
+
+ private View mInflateView;
+
+ public CustomViewStub(Context context) {
+ this(context, null);
+ }
+
+ public CustomViewStub(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CustomViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CustomViewStub(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStub);
+ mLayoutResource = array.getResourceId(R.styleable.CustomViewStub_android_layout, 0);
+ array.recycle();
+
+ // 隐藏自己
+ setVisibility(GONE);
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (mInflateView == null && visibility != GONE) {
+
+ mInflateView = LayoutInflater.from(getContext()).inflate(mLayoutResource, this, false);
+ LayoutParams layoutParams = (LayoutParams) mInflateView.getLayoutParams();
+ layoutParams.width = getLayoutParams().width;
+ layoutParams.height = getLayoutParams().height;
+ if (layoutParams.gravity == LayoutParams.UNSPECIFIED_GRAVITY) {
+ layoutParams.gravity = Gravity.CENTER;
+ }
+ mInflateView.setLayoutParams(layoutParams);
+ addView(mInflateView);
+
+ if (mListener != null) {
+ mListener.onInflate(this, mInflateView);
+ }
+ }
+
+ if (mListener != null) {
+ mListener.onVisibility(this, visibility);
+ }
+ }
+
+ /**
+ * 设置显示状态(避免 setVisibility 导致的无限递归)
+ */
+ public void setCustomVisibility(int visibility) {
+ super.setVisibility(visibility);
+ }
+
+ /**
+ * 获取填充的 View
+ */
+ public View getInflateView() {
+ return mInflateView;
+ }
+
+ /**
+ * 设置监听器
+ */
+ public void setOnViewStubListener(OnViewStubListener listener) {
+ mListener = listener;
+ }
+
+ public interface OnViewStubListener {
+
+ /**
+ * 布局填充回调(可在此中做 View 初始化)
+ *
+ * @param stub 当前 ViewStub 对象
+ * @param inflatedView 填充布局对象
+ */
+ void onInflate(CustomViewStub stub, View inflatedView);
+
+ /**
+ * 可见状态改变(可在此中做 View 更新)
+ *
+ * @param stub 当前 ViewStub 对象
+ * @param visibility 可见状态参数改变
+ */
+ void onVisibility(CustomViewStub stub, int visibility);
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/layout/NoScrollViewPager.java b/widget/src/main/java/com/example/widget/layout/NoScrollViewPager.java
new file mode 100644
index 0000000..72b9f8c
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/layout/NoScrollViewPager.java
@@ -0,0 +1,69 @@
+package com.example.widget.layout;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : 禁用水平滑动的ViewPager(一般用于 APP 主页的 ViewPager + Fragment)
+ */
+public final class NoScrollViewPager extends ViewPager {
+
+ public NoScrollViewPager(Context context) {
+ super(context);
+ }
+
+ public NoScrollViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // 不拦截这个事件
+ return false;
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // 不处理这个事件
+ return false;
+ }
+
+ @Override
+ public boolean executeKeyEvent(@NonNull KeyEvent event) {
+ // 不响应按键事件
+ return false;
+ }
+
+ @Override
+ public void setCurrentItem(int item) {
+ boolean smoothScroll;
+ int currentItem = getCurrentItem();
+ if (currentItem == 0) {
+ // 如果当前是第一页,只有第二页才会有动画
+ smoothScroll = item == currentItem + 1;
+ } else if (currentItem == getCount() - 1) {
+ // 如果当前是最后一页,只有最后第二页才会有动画
+ smoothScroll = item == currentItem - 1;
+ } else {
+ // 如果当前是中间页,只有相邻页才会有动画
+ smoothScroll = Math.abs(currentItem - item) == 1;
+ }
+ super.setCurrentItem(item, smoothScroll);
+ }
+
+ public int getCount() {
+ PagerAdapter adapter = getAdapter();
+ return adapter != null ? adapter.getCount() : 0;
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/layout/RatioFrameLayout.java b/widget/src/main/java/com/example/widget/layout/RatioFrameLayout.java
new file mode 100644
index 0000000..de123a4
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/layout/RatioFrameLayout.java
@@ -0,0 +1,72 @@
+package com.example.widget.layout;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.example.widget.R;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/08/23
+ * desc : 按照比例显示的 FrameLayout
+ */
+public final class RatioFrameLayout extends FrameLayout {
+
+ /** 宽高比 */
+ private final float mSizeRatio;
+
+ public RatioFrameLayout(Context context) {
+ this(context, null);
+ }
+
+ public RatioFrameLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RatioFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RatioFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RatioFrameLayout);
+ mSizeRatio = array.getFloat(R.styleable.RatioFrameLayout_sizeRatio, 0);
+ array.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mSizeRatio != 0) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
+ // 如果当前宽度和高度都是写死的
+ if (widthSpecSize / mSizeRatio <= heightSpecSize) {
+ // 如果宽度经过比例换算不超过原有的高度
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (widthSpecSize / mSizeRatio), MeasureSpec.EXACTLY);
+ } else if (heightSpecSize * mSizeRatio <= widthSpecSize) {
+ // 如果高度经过比例换算不超过原有的宽度
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (heightSpecSize * mSizeRatio), MeasureSpec.EXACTLY);
+ }
+ } else if (widthSpecMode == MeasureSpec.EXACTLY) {
+ // 如果当前宽度是写死的,但是高度不写死
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (widthSpecSize / mSizeRatio), MeasureSpec.EXACTLY);
+ } else if (heightSpecMode == MeasureSpec.EXACTLY) {
+ // 如果当前高度是写死的,但是宽度不写死
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (heightSpecSize * mSizeRatio), MeasureSpec.EXACTLY);
+ }
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/layout/SettingBar.java b/widget/src/main/java/com/example/widget/layout/SettingBar.java
new file mode 100644
index 0000000..a43eb11
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/layout/SettingBar.java
@@ -0,0 +1,350 @@
+package com.example.widget.layout;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.widget.R;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/01/23
+ * desc : 设置条自定义控件
+ */
+public final class SettingBar extends FrameLayout {
+
+ private final LinearLayout mMainLayout;
+ private final TextView mLeftView;
+ private final TextView mRightView;
+ private final View mLineView;
+
+ public SettingBar(Context context) {
+ this(context, null);
+ }
+
+ public SettingBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SettingBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SettingBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mMainLayout = new LinearLayout(getContext());
+ mLeftView = new TextView(getContext());
+ mRightView = new TextView(getContext());
+ mLineView = new View(getContext());
+
+ mRightView.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
+
+ mLeftView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mLeftView.getLineSpacingMultiplier());
+ mRightView.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()), mRightView.getLineSpacingMultiplier());
+
+ mLeftView.setPaddingRelative((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()));
+ mRightView.setPaddingRelative((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()),
+ (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()));
+
+ mLeftView.setCompoundDrawablePadding((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
+ mRightView.setCompoundDrawablePadding((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
+
+ LinearLayout.LayoutParams leftParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ leftParams.gravity = Gravity.CENTER_VERTICAL;
+ mMainLayout.addView(mLeftView, leftParams);
+
+ LinearLayout.LayoutParams rightParams = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT);
+ rightParams.gravity = Gravity.CENTER_VERTICAL;
+ rightParams.weight = 1;
+ mMainLayout.addView(mRightView, rightParams);
+
+ addView(mMainLayout, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_VERTICAL));
+ addView(mLineView, 1, new LayoutParams(LayoutParams.MATCH_PARENT, 1, Gravity.BOTTOM));
+
+ final TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.SettingBar);
+
+ // 文本设置
+ if (array.hasValue(R.styleable.SettingBar_bar_leftText)) {
+ setLeftText(array.getString(R.styleable.SettingBar_bar_leftText));
+ }
+
+ if (array.hasValue(R.styleable.SettingBar_bar_rightText)) {
+ setRightText(array.getString(R.styleable.SettingBar_bar_rightText));
+ }
+
+ // 提示设置
+ if (array.hasValue(R.styleable.SettingBar_bar_leftHint)) {
+ setLeftHint(array.getString(R.styleable.SettingBar_bar_leftHint));
+ }
+
+ if (array.hasValue(R.styleable.SettingBar_bar_rightHint)) {
+ setRightHint(array.getString(R.styleable.SettingBar_bar_rightHint));
+ }
+
+ // 图标设置
+ if (array.hasValue(R.styleable.SettingBar_bar_leftIcon)) {
+ setLeftIcon(array.getDrawable(R.styleable.SettingBar_bar_leftIcon));
+ }
+
+ if (array.hasValue(R.styleable.SettingBar_bar_rightIcon)) {
+ setRightIcon(array.getDrawable(R.styleable.SettingBar_bar_rightIcon));
+ }
+
+ // 文字颜色设置
+ setLeftColor(array.getColor(R.styleable.SettingBar_bar_leftColor, ContextCompat.getColor(getContext(), R.color.black80)));
+ setRightColor(array.getColor(R.styleable.SettingBar_bar_rightColor, ContextCompat.getColor(getContext(), R.color.black60)));
+
+ // 文字大小设置
+ setLeftSize(TypedValue.COMPLEX_UNIT_SP, array.getDimensionPixelSize(R.styleable.SettingBar_bar_leftSize, 15));
+ setRightSize(TypedValue.COMPLEX_UNIT_SP, array.getDimensionPixelSize(R.styleable.SettingBar_bar_rightSize, 14));
+
+ // 分割线设置
+ if (array.hasValue(R.styleable.SettingBar_bar_lineColor)) {
+ setLineDrawable(array.getDrawable(R.styleable.SettingBar_bar_lineColor));
+ } else {
+ setLineDrawable(new ColorDrawable(0xFFECECEC));
+ }
+
+ if (array.hasValue(R.styleable.SettingBar_bar_lineVisible)) {
+ setLineVisible(array.getBoolean(R.styleable.SettingBar_bar_lineVisible, true));
+ }
+
+ if (array.hasValue(R.styleable.SettingBar_bar_lineSize)) {
+ setLineSize(array.getDimensionPixelSize(R.styleable.SettingBar_bar_lineSize, 0));
+ }
+
+ if (array.hasValue(R.styleable.SettingBar_bar_lineMargin)) {
+ setLineMargin(array.getDimensionPixelSize(R.styleable.SettingBar_bar_lineMargin, 0));
+ }
+
+ if (getBackground() == null) {
+ StateListDrawable drawable = new StateListDrawable();
+ drawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.black5)));
+ drawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.black5)));
+ drawable.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.black5)));
+ drawable.addState(new int[]{}, new ColorDrawable(ContextCompat.getColor(getContext(), R.color.white)));
+ setBackground(drawable);
+
+ // 必须要设置可点击,否则点击屏幕任何角落都会触发按压事件
+ setFocusable(true);
+ setClickable(true);
+ }
+
+ array.recycle();
+ }
+
+ /**
+ * 设置左边的标题
+ */
+ public SettingBar setLeftText(@StringRes int id) {
+ return setLeftText(getResources().getString(id));
+ }
+
+ public SettingBar setLeftText(CharSequence text) {
+ mLeftView.setText(text);
+ return this;
+ }
+
+ public CharSequence getLeftText() {
+ return mLeftView.getText();
+ }
+
+ /**
+ * 设置左边的提示
+ */
+ public SettingBar setLeftHint(@StringRes int id) {
+ return setLeftHint(getResources().getString(id));
+ }
+
+ public SettingBar setLeftHint(CharSequence hint) {
+ mLeftView.setHint(hint);
+ return this;
+ }
+
+ /**
+ * 设置右边的标题
+ */
+ public SettingBar setRightText(@StringRes int id) {
+ setRightText(getResources().getString(id));
+ return this;
+ }
+
+ public SettingBar setRightText(CharSequence text) {
+ mRightView.setText(text);
+ return this;
+ }
+
+ public CharSequence getRightText() {
+ return mRightView.getText();
+ }
+
+ /**
+ * 设置右边的提示
+ */
+ public SettingBar setRightHint(@StringRes int id) {
+ return setRightHint(getResources().getString(id));
+ }
+
+ public SettingBar setRightHint(CharSequence hint) {
+ mRightView.setHint(hint);
+ return this;
+ }
+
+ /**
+ * 设置左边的图标
+ */
+ public SettingBar setLeftIcon(@DrawableRes int id) {
+ setLeftIcon(ContextCompat.getDrawable(getContext(), id));
+ return this;
+ }
+
+ public SettingBar setLeftIcon(Drawable drawable) {
+ mLeftView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ return this;
+ }
+
+ public Drawable getLeftIcon() {
+ return mLeftView.getCompoundDrawables()[0];
+ }
+
+ /**
+ * 设置右边的图标
+ */
+ public SettingBar setRightIcon(@DrawableRes int id) {
+ setRightIcon(ContextCompat.getDrawable(getContext(), id));
+ return this;
+ }
+
+ public SettingBar setRightIcon(Drawable drawable) {
+ mRightView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
+ return this;
+ }
+
+ public Drawable getRightIcon() {
+ return mRightView.getCompoundDrawables()[2];
+ }
+
+ /**
+ * 设置左标题颜色
+ */
+ public SettingBar setLeftColor(@ColorInt int color) {
+ mLeftView.setTextColor(color);
+ return this;
+ }
+
+ /**
+ * 设置右标题颜色
+ */
+ public SettingBar setRightColor(@ColorInt int color) {
+ mRightView.setTextColor(color);
+ return this;
+ }
+
+ /**
+ * 设置左标题的文本大小
+ */
+ public SettingBar setLeftSize(int unit, float size) {
+ mLeftView.setTextSize(unit, size);
+ return this;
+ }
+
+ /**
+ * 设置右标题的文本大小
+ */
+ public SettingBar setRightSize(int unit, float size) {
+ mRightView.setTextSize(unit, size);
+ return this;
+ }
+
+ /**
+ * 设置分割线是否显示
+ */
+ public SettingBar setLineVisible(boolean visible) {
+ mLineView.setVisibility(visible ? VISIBLE : GONE);
+ return this;
+ }
+
+ /**
+ * 设置分割线的颜色
+ */
+ public SettingBar setLineColor(@ColorInt int color) {
+ return setLineDrawable(new ColorDrawable(color));
+ }
+ public SettingBar setLineDrawable(Drawable drawable) {
+ mLineView.setBackground(drawable);
+ return this;
+ }
+
+ /**
+ * 设置分割线的大小
+ */
+ public SettingBar setLineSize(int size) {
+ ViewGroup.LayoutParams layoutParams = mLineView.getLayoutParams();
+ layoutParams.height = size;
+ mLineView.setLayoutParams(layoutParams);
+ return this;
+ }
+
+ /**
+ * 设置分割线边界
+ */
+ public SettingBar setLineMargin(int margin) {
+ LayoutParams params = (LayoutParams) mLineView.getLayoutParams();
+ params.leftMargin = margin;
+ params.rightMargin = margin;
+ mLineView.setLayoutParams(params);
+ return this;
+ }
+
+ /**
+ * 获取主布局
+ */
+ public LinearLayout getMainLayout() {
+ return mMainLayout;
+ }
+
+ /**
+ * 获取左标题
+ */
+ public TextView getLeftView() {
+ return mLeftView;
+ }
+
+ /**
+ * 获取右标题
+ */
+ public TextView getRightView() {
+ return mRightView;
+ }
+
+ /**
+ * 获取分割线
+ */
+ public View getLineView() {
+ return mLineView;
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/layout/SimpleLayout.java b/widget/src/main/java/com/example/widget/layout/SimpleLayout.java
new file mode 100644
index 0000000..3bf317a
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/layout/SimpleLayout.java
@@ -0,0 +1,104 @@
+package com.example.widget.layout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : 简单的 Layout(常用于自定义组合控件继承的基类,可以起到性能优化的作用)
+ */
+public class SimpleLayout extends ViewGroup {
+
+ public SimpleLayout(Context context) {
+ super(context);
+ }
+
+ public SimpleLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr, 0);
+ }
+
+ public SimpleLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int count = getChildCount();
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ // 测量子 View
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ // 被测量的子 View 不能是隐藏的
+ if (child.getVisibility() != GONE) {
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + params.leftMargin + params.rightMargin);
+ maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + params.topMargin + params.bottomMargin);
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+ }
+ }
+
+ maxWidth += (getPaddingLeft() + getPaddingRight());
+ maxHeight += (getPaddingTop() + getPaddingBottom());
+
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+
+ // 测量自身
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // 遍历子 View
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
+ int left = params.leftMargin + getPaddingLeft();
+ int top = params.topMargin + getPaddingTop();
+ int right = left + child.getMeasuredWidth() + getPaddingRight() + params.rightMargin;
+ int bottom = top + child.getMeasuredHeight() + getPaddingBottom() + params.bottomMargin;
+ // 将子 View 放置到左上角的位置
+ child.layout(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(LayoutParams params) {
+ return new MarginLayoutParams(params);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(LayoutParams params) {
+ return params instanceof MarginLayoutParams;
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/layout/WrapRecyclerView.java b/widget/src/main/java/com/example/widget/layout/WrapRecyclerView.java
new file mode 100644
index 0000000..32483fd
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/layout/WrapRecyclerView.java
@@ -0,0 +1,468 @@
+package com.example.widget.layout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/09/21
+ * desc : 支持添加底部和头部的 RecyclerView
+ */
+public final class WrapRecyclerView extends RecyclerView {
+
+ /** 原有的适配器 */
+ private RecyclerView.Adapter mRealAdapter;
+
+ /** 支持添加头部和底部的适配器 */
+ private final WrapRecyclerAdapter mWrapAdapter = new WrapRecyclerAdapter();
+
+ public WrapRecyclerView(Context context) {
+ super(context);
+ }
+
+ public WrapRecyclerView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WrapRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ mRealAdapter = adapter;
+ // 偷梁换柱
+ mWrapAdapter.setRealAdapter(mRealAdapter);
+ // 禁用条目动画
+ setItemAnimator(null);
+ super.setAdapter(mWrapAdapter);
+ }
+
+ @Override
+ public Adapter getAdapter() {
+ return mRealAdapter;
+ }
+
+ /**
+ * 添加头部View
+ */
+ public void addHeaderView(View view) {
+ mWrapAdapter.addHeaderView(view);
+ }
+
+ @SuppressWarnings("unchecked")
+ public V addHeaderView(@LayoutRes int id) {
+ View headerView = LayoutInflater.from(getContext()).inflate(id, this, false);
+ addHeaderView(headerView);
+ return (V) headerView;
+ }
+
+ /**
+ * 移除头部View
+ */
+ public void removeHeaderView(View view) {
+ mWrapAdapter.removeHeaderView(view);
+ }
+
+ /**
+ * 添加底部View
+ */
+ public void addFooterView(View view) {
+ mWrapAdapter.addFooterView(view);
+ }
+
+ @SuppressWarnings("unchecked")
+ public V addFooterView(@LayoutRes int id) {
+ View footerView = LayoutInflater.from(getContext()).inflate(id, this, false);
+ addFooterView(footerView);
+ return (V) footerView;
+ }
+
+ /**
+ * 移除底部View
+ */
+ public void removeFooterView(View view) {
+ mWrapAdapter.removeFooterView(view);
+ }
+
+ /**
+ * 获取头部View总数
+ */
+ public int getHeaderViewsCount() {
+ return mWrapAdapter.getHeaderViewsCount();
+ }
+
+ /**
+ * 获取底部View总数
+ */
+ public int getFooterViewsCount() {
+ return mWrapAdapter.getFooterViewsCount();
+ }
+
+ /**
+ * 获取头部View集合
+ */
+ public List getHeaderViews() {
+ return mWrapAdapter.getHeaderViews();
+ }
+
+ /**
+ * 获取底部View集合
+ */
+ public List getFooterViews() {
+ return mWrapAdapter.getFooterViews();
+ }
+
+ /**
+ * 刷新头部和底部布局所有的 View 的状态
+ */
+ public void refreshHeaderFooterViews() {
+ mWrapAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * 设置在 GridLayoutManager 模式下头部和尾部都是独占一行的效果
+ */
+ public void adjustSpanSize() {
+
+ final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (layoutManager instanceof GridLayoutManager) {
+ ((GridLayoutManager) layoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+
+ @Override
+ public int getSpanSize(int position) {
+ return (position < mWrapAdapter.getHeaderViewsCount()
+ || position >= mWrapAdapter.getHeaderViewsCount() + (mRealAdapter == null ? 0 : mRealAdapter.getItemCount()))
+ ? ((GridLayoutManager) layoutManager).getSpanCount() : 1;
+ }
+ });
+ }
+ }
+
+ /**
+ * 采用装饰设计模式,将原有的适配器包装起来
+ */
+ private static final class WrapRecyclerAdapter extends RecyclerView.Adapter {
+
+ /** 头部条目类型 */
+ private static final int HEADER_VIEW_TYPE = Integer.MIN_VALUE >> 1;
+ /** 底部条目类型 */
+ private static final int FOOTER_VIEW_TYPE = Integer.MAX_VALUE >> 1;
+
+ /** 原有的适配器 */
+ private RecyclerView.Adapter mRealAdapter;
+ /** 头部View集合 */
+ private final List mHeaderViews = new ArrayList<>();
+ /** 底部View集合 */
+ private final List mFooterViews = new ArrayList<>();
+ /** 当前调用的位置 */
+ private int mCurrentPosition;
+
+ /** RecyclerView对象 */
+ private RecyclerView mRecyclerView;
+
+ /** 数据观察者对象 */
+ private WrapAdapterDataObserver mObserver;
+
+ private void setRealAdapter(RecyclerView.Adapter adapter) {
+ if (mRealAdapter != adapter) {
+
+ if (mRealAdapter != null) {
+ if (mObserver != null) {
+ // 为原有的RecyclerAdapter移除数据监听对象
+ mRealAdapter.unregisterAdapterDataObserver(mObserver);
+ }
+ }
+
+ mRealAdapter = adapter;
+ if (mRealAdapter != null) {
+ if (mObserver == null) {
+ mObserver = new WrapAdapterDataObserver(this);
+ }
+ // 为原有的RecyclerAdapter添加数据监听对象
+ mRealAdapter.registerAdapterDataObserver(mObserver);
+ // 适配器不是第一次被绑定到RecyclerView上需要发送通知,因为第一次绑定会自动通知
+ if (mRecyclerView != null) {
+ notifyDataSetChanged();
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mRealAdapter != null) {
+ return getHeaderViewsCount() + mRealAdapter.getItemCount() + getFooterViewsCount();
+ } else {
+ return getHeaderViewsCount() + getFooterViewsCount();
+ }
+ }
+
+ @SuppressWarnings("all")
+ @Override
+ public int getItemViewType(int position) {
+ mCurrentPosition = position;
+ // 获取头部布局的总数
+ int headerCount = getHeaderViewsCount();
+ // 获取原有适配器的总数
+ int adapterCount = mRealAdapter != null ? mRealAdapter.getItemCount() : 0;
+ // 获取在原有适配器上的位置
+ int adjPosition = position - headerCount;
+ if (position < headerCount) {
+ return HEADER_VIEW_TYPE;
+ } else if (adjPosition < adapterCount) {
+ return mRealAdapter.getItemViewType(adjPosition);
+ } else {
+ return FOOTER_VIEW_TYPE;
+ }
+ }
+
+ public int getPosition() {
+ return mCurrentPosition;
+ }
+
+ @SuppressWarnings("all")
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case HEADER_VIEW_TYPE:
+ return newWrapViewHolder(mHeaderViews.get(getPosition()));
+ case FOOTER_VIEW_TYPE:
+ return newWrapViewHolder(mFooterViews.get(getPosition() - getHeaderViewsCount() - (mRealAdapter != null ? mRealAdapter.getItemCount() : 0)));
+ default:
+ int itemViewType = mRealAdapter.getItemViewType(getPosition() - getHeaderViewsCount());
+ if (itemViewType == HEADER_VIEW_TYPE || itemViewType == FOOTER_VIEW_TYPE) {
+ throw new IllegalStateException("Please do not use this type as itemType");
+ }
+ if (mRealAdapter != null) {
+ return mRealAdapter.onCreateViewHolder(parent, itemViewType);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ int viewType = getItemViewType(position);
+ switch (viewType) {
+ case HEADER_VIEW_TYPE:
+ case FOOTER_VIEW_TYPE:
+ break;
+ default:
+ if (mRealAdapter != null) {
+ mRealAdapter.onBindViewHolder(holder, getPosition() - getHeaderViewsCount());
+ }
+ break;
+ }
+ }
+
+ private WrapViewHolder newWrapViewHolder(View view) {
+ ViewParent parent = view.getParent();
+ if (parent instanceof ViewGroup) {
+ // IllegalStateException: ViewHolder views must not be attached when created.
+ // Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot)
+ ((ViewGroup) parent).removeView(view);
+ }
+ return new WrapViewHolder(view);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mRealAdapter != null && position > getHeaderViewsCount() - 1 && position < getHeaderViewsCount() + mRealAdapter.getItemCount()) {
+ return mRealAdapter.getItemId(position - getHeaderViewsCount());
+ } else {
+ return super.getItemId(position);
+ }
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ if (mRealAdapter != null) {
+ mRealAdapter.onAttachedToRecyclerView(recyclerView);
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
+ if (mRealAdapter != null) {
+ mRealAdapter.onDetachedFromRecyclerView(recyclerView);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onViewRecycled(@NonNull ViewHolder holder) {
+ if (holder instanceof WrapViewHolder) {
+ // 防止这个 ViewHolder 被 RecyclerView 拿去复用
+ holder.setIsRecyclable(false);
+ return;
+ }
+ if (mRealAdapter != null) {
+ mRealAdapter.onViewRecycled(holder);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean onFailedToRecycleView(@NonNull ViewHolder holder) {
+ if (mRealAdapter != null) {
+ return mRealAdapter.onFailedToRecycleView(holder);
+ }
+ return super.onFailedToRecycleView(holder);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
+ if (mRealAdapter != null) {
+ mRealAdapter.onViewAttachedToWindow(holder);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
+ if (mRealAdapter != null) {
+ mRealAdapter.onViewDetachedFromWindow(holder);
+ }
+ }
+
+ /**
+ * 添加头部View
+ */
+ private void addHeaderView(View view) {
+ // 不能添加同一个View对象,否则会导致RecyclerView复用异常
+ if (!mHeaderViews.contains(view) && !mFooterViews.contains(view)) {
+ mHeaderViews.add(view);
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * 移除头部View
+ */
+ private void removeHeaderView(View view) {
+ if (mHeaderViews.remove(view)) {
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * 添加底部View
+ */
+ private void addFooterView(View view) {
+ // 不能添加同一个View对象,否则会导致RecyclerView复用异常
+ if (!mFooterViews.contains(view) && !mHeaderViews.contains(view)) {
+ mFooterViews.add(view);
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * 移除底部View
+ */
+ private void removeFooterView(View view) {
+ if (mFooterViews.remove(view)) {
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * 获取头部View总数
+ */
+ private int getHeaderViewsCount() {
+ return mHeaderViews.size();
+ }
+
+ /**
+ * 获取底部View总数
+ */
+ private int getFooterViewsCount() {
+ return mFooterViews.size();
+ }
+
+ /**
+ * 获取头部View集合
+ */
+ private List getHeaderViews() {
+ return mHeaderViews;
+ }
+
+ /**
+ * 获取底部View集合
+ */
+ private List getFooterViews() {
+ return mFooterViews;
+ }
+ }
+
+ /**
+ * 头部和底部通用的ViewHolder对象
+ */
+ private static final class WrapViewHolder extends RecyclerView.ViewHolder {
+
+ private WrapViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+ /**
+ * 数据改变监听器
+ */
+ private static final class WrapAdapterDataObserver extends RecyclerView.AdapterDataObserver {
+
+ private final WrapRecyclerAdapter mWrapAdapter;
+
+ private WrapAdapterDataObserver(WrapRecyclerAdapter adapter) {
+ mWrapAdapter = adapter;
+ }
+
+ @Override
+ public void onChanged() {
+ mWrapAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ onItemRangeChanged(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ mWrapAdapter.notifyItemRangeChanged(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ mWrapAdapter.notifyItemRangeInserted(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ mWrapAdapter.notifyItemRangeRemoved(mWrapAdapter.getHeaderViewsCount() + positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ mWrapAdapter.notifyItemMoved(mWrapAdapter.getHeaderViewsCount() + fromPosition, toPosition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/ClearEditText.java b/widget/src/main/java/com/example/widget/view/ClearEditText.java
new file mode 100644
index 0000000..4480559
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/ClearEditText.java
@@ -0,0 +1,126 @@
+package com.example.widget.view;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.example.widget.R;
+
+import java.util.Objects;
+
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : 带清除按钮的 EditText
+ */
+public final class ClearEditText extends RegexEditText
+ implements View.OnTouchListener,
+ View.OnFocusChangeListener, TextWatcher {
+
+ private Drawable mClearDrawable;
+
+ private OnTouchListener mOnTouchListener;
+ private OnFocusChangeListener mOnFocusChangeListener;
+
+ public ClearEditText(Context context) {
+ this(context, null);
+ }
+
+ public ClearEditText(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.editTextStyle);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ public ClearEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ mClearDrawable = DrawableCompat.wrap(Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.input_delete_ic)));
+ mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight());
+ setDrawableVisible(false);
+ super.setOnTouchListener(this);
+ super.setOnFocusChangeListener(this);
+ super.addTextChangedListener(this);
+ }
+
+ private void setDrawableVisible(final boolean visible) {
+ if (mClearDrawable.isVisible() == visible) {
+ return;
+ }
+
+ mClearDrawable.setVisible(visible, false);
+ final Drawable[] drawables = getCompoundDrawables();
+ setCompoundDrawables(
+ drawables[0],
+ drawables[1],
+ visible ? mClearDrawable : null,
+ drawables[3]);
+ }
+
+ @Override
+ public void setOnFocusChangeListener(final OnFocusChangeListener onFocusChangeListener) {
+ mOnFocusChangeListener = onFocusChangeListener;
+ }
+
+ @Override
+ public void setOnTouchListener(final OnTouchListener onTouchListener) {
+ mOnTouchListener = onTouchListener;
+ }
+
+ /**
+ * {@link OnFocusChangeListener}
+ */
+
+ @Override
+ public void onFocusChange(final View view, final boolean hasFocus) {
+ if (hasFocus && getText() != null) {
+ setDrawableVisible(getText().length() > 0);
+ } else {
+ setDrawableVisible(false);
+ }
+ if (mOnFocusChangeListener != null) {
+ mOnFocusChangeListener.onFocusChange(view, hasFocus);
+ }
+ }
+
+ /**
+ * {@link OnTouchListener}
+ */
+
+ @Override
+ public boolean onTouch(final View view, final MotionEvent motionEvent) {
+ final int x = (int) motionEvent.getX();
+ if (mClearDrawable.isVisible() && x > getWidth() - getPaddingRight() - mClearDrawable.getIntrinsicWidth()) {
+ if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
+ setText("");
+ }
+ return true;
+ }
+ return mOnTouchListener != null && mOnTouchListener.onTouch(view, motionEvent);
+ }
+
+ /**
+ * {@link TextWatcher}
+ */
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ if (isFocused()) {
+ setDrawableVisible(s.length() > 0);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/CountdownView.java b/widget/src/main/java/com/example/widget/view/CountdownView.java
new file mode 100644
index 0000000..dc9aa29
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/CountdownView.java
@@ -0,0 +1,90 @@
+package com.example.widget.view;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2018/10/18
+ * desc : 验证码倒计时
+ */
+public final class CountdownView extends AppCompatTextView implements Runnable {
+
+ /** 倒计时秒数 */
+ private int mTotalSecond = 60;
+ /** 秒数单位文本 */
+ private static final String TIME_UNIT = "S";
+
+ /** 当前秒数 */
+ private int mCurrentSecond;
+ /** 记录原有的文本 */
+ private CharSequence mRecordText;
+
+ public CountdownView(Context context) {
+ super(context);
+ }
+
+ public CountdownView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CountdownView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * 设置倒计时总秒数
+ */
+ public void setTotalTime(int totalTime) {
+ this.mTotalSecond = totalTime;
+ }
+
+ /**
+ * 开始倒计时
+ */
+ public void start() {
+ mRecordText = getText();
+ setEnabled(false);
+ mCurrentSecond = mTotalSecond;
+ post(this);
+ }
+
+ /**
+ * 结束倒计时
+ */
+ public void stop() {
+ setText(mRecordText);
+ setEnabled(true);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ // 设置点击的属性
+ setClickable(true);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ // 移除延迟任务,避免内存泄露
+ removeCallbacks(this);
+ super.onDetachedFromWindow();
+ }
+
+ @SuppressLint("SetTextI18n")
+ @Override
+ public void run() {
+ if (mCurrentSecond == 0) {
+ stop();
+ } else {
+ mCurrentSecond--;
+ setText(mCurrentSecond + " " + TIME_UNIT);
+ postDelayed(this, 1000);
+ }
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/PasswordEditText.java b/widget/src/main/java/com/example/widget/view/PasswordEditText.java
new file mode 100644
index 0000000..dccde24
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/PasswordEditText.java
@@ -0,0 +1,166 @@
+package com.example.widget.view;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.text.method.HideReturnsTransformationMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.example.widget.R;
+
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/08/25
+ * desc : 密码隐藏显示 EditText
+ */
+public final class PasswordEditText extends RegexEditText
+ implements View.OnTouchListener,
+ View.OnFocusChangeListener, TextWatcher {
+
+ private Drawable mCurrentDrawable;
+ private final Drawable mVisibleDrawable;
+ private final Drawable mInvisibleDrawable;
+
+ private View.OnTouchListener mOnTouchListener;
+ private View.OnFocusChangeListener mOnFocusChangeListener;
+
+ public PasswordEditText(Context context) {
+ this(context, null);
+ }
+
+ public PasswordEditText(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.editTextStyle);
+ }
+
+ @SuppressWarnings("all")
+ @SuppressLint("ClickableViewAccessibility")
+ public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ mVisibleDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.password_off_ic));
+ mVisibleDrawable.setBounds(0, 0, mVisibleDrawable.getIntrinsicWidth(), mVisibleDrawable.getIntrinsicHeight());
+
+ mInvisibleDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.password_on_ic));
+ mInvisibleDrawable.setBounds(0, 0, mInvisibleDrawable.getIntrinsicWidth(), mInvisibleDrawable.getIntrinsicHeight());
+
+ mCurrentDrawable = mVisibleDrawable;
+
+ // 密码不可见
+ addInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ if (getInputRegex() == null) {
+ // 密码输入规则
+ setInputRegex(REGEX_NONNULL);
+ }
+
+ setDrawableVisible(false);
+ super.setOnTouchListener(this);
+ super.setOnFocusChangeListener(this);
+ super.addTextChangedListener(this);
+ }
+
+ private void setDrawableVisible(boolean visible) {
+ if (mCurrentDrawable.isVisible() == visible) {
+ return;
+ }
+
+ mCurrentDrawable.setVisible(visible, false);
+ Drawable[] drawables = getCompoundDrawablesRelative();
+ setCompoundDrawablesRelative(
+ drawables[0],
+ drawables[1],
+ visible ? mCurrentDrawable : null,
+ drawables[3]);
+ }
+
+ private void refreshDrawableStatus() {
+ Drawable[] drawables = getCompoundDrawablesRelative();
+ setCompoundDrawablesRelative(
+ drawables[0],
+ drawables[1],
+ mCurrentDrawable,
+ drawables[3]);
+ }
+
+ @Override
+ public void setOnFocusChangeListener(View.OnFocusChangeListener onFocusChangeListener) {
+ mOnFocusChangeListener = onFocusChangeListener;
+ }
+
+ @Override
+ public void setOnTouchListener(View.OnTouchListener onTouchListener) {
+ mOnTouchListener = onTouchListener;
+ }
+
+ /**
+ * {@link View.OnFocusChangeListener}
+ */
+
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (hasFocus && getText() != null) {
+ setDrawableVisible(getText().length() > 0);
+ } else {
+ setDrawableVisible(false);
+ }
+ if (mOnFocusChangeListener != null) {
+ mOnFocusChangeListener.onFocusChange(view, hasFocus);
+ }
+ }
+
+ /**
+ * {@link View.OnTouchListener}
+ */
+
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ int x = (int) motionEvent.getX();
+ if (mCurrentDrawable.isVisible() && x > getWidth() - getPaddingRight() - mCurrentDrawable.getIntrinsicWidth()) {
+ if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
+ if (mCurrentDrawable == mVisibleDrawable) {
+ mCurrentDrawable = mInvisibleDrawable;
+ // 密码可见
+ setTransformationMethod(HideReturnsTransformationMethod.getInstance());
+ refreshDrawableStatus();
+ } else if (mCurrentDrawable == mInvisibleDrawable) {
+ mCurrentDrawable = mVisibleDrawable;
+ // 密码不可见
+ setTransformationMethod(PasswordTransformationMethod.getInstance());
+ refreshDrawableStatus();
+ }
+ Editable editable = getText();
+ if (editable != null) {
+ setSelection(editable.toString().length());
+ }
+ }
+ return true;
+ }
+ return mOnTouchListener != null && mOnTouchListener.onTouch(view, motionEvent);
+ }
+
+ /**
+ * {@link TextWatcher}
+ */
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (isFocused()) {
+ setDrawableVisible(s.length() > 0);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/RegexEditText.java b/widget/src/main/java/com/example/widget/view/RegexEditText.java
new file mode 100644
index 0000000..c94ad60
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/RegexEditText.java
@@ -0,0 +1,191 @@
+package com.example.widget.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import com.example.widget.R;
+
+import java.util.regex.Pattern;
+
+import androidx.appcompat.widget.AppCompatEditText;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/06/29
+ * desc : 正则输入限制编辑框
+ */
+public class RegexEditText extends AppCompatEditText implements InputFilter {
+
+ /** 手机号(只能以 1 开头) */
+ public static final String REGEX_MOBILE = "[1]\\d{0,10}";
+ /** 中文(普通的中文字符) */
+ public static final String REGEX_CHINESE = "[\\u4e00-\\u9fa5]*";
+ /** 英文(大写和小写的英文) */
+ public static final String REGEX_ENGLISH = "[a-zA-Z]*";
+ /** 计数(非 0 开头的数字) */
+ public static final String REGEX_COUNT = "[1-9]\\d*";
+ /** 用户名(中文、英文、数字) */
+ public static final String REGEX_NAME = "[[\\u4e00-\\u9fa5]|[a-zA-Z]|\\d]*";
+ /** 非空格的字符(不能输入空格) */
+ public static final String REGEX_NONNULL = "\\S+";
+
+ /** 正则表达式规则 */
+ private Pattern mPattern;
+
+ public RegexEditText(Context context) {
+ this(context, null);
+ }
+
+ public RegexEditText(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.editTextStyle);
+ }
+
+ public RegexEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RegexEditText);
+
+ if (array.hasValue(R.styleable.RegexEditText_inputRegex)) {
+ setInputRegex(array.getString(R.styleable.RegexEditText_inputRegex));
+ } else {
+ if (array.hasValue(R.styleable.RegexEditText_regexType)) {
+ int regexType = array.getInt(R.styleable.RegexEditText_regexType, 0);
+ switch (regexType) {
+ case 0x01:
+ setInputRegex(REGEX_MOBILE);
+ break;
+ case 0x02:
+ setInputRegex(REGEX_CHINESE);
+ break;
+ case 0x03:
+ setInputRegex(REGEX_ENGLISH);
+ break;
+ case 0x04:
+ setInputRegex(REGEX_COUNT);
+ break;
+ case 0x05:
+ setInputRegex(REGEX_NAME);
+ break;
+ case 0x06:
+ setInputRegex(REGEX_NONNULL);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ array.recycle();
+ }
+
+ /**
+ * 是否有这个输入标记
+ */
+ public boolean hasInputType(int type) {
+ return (getInputType() & type) != 0;
+ }
+
+ /**
+ * 添加一个输入标记
+ */
+ public void addInputType(int type) {
+ setInputType(getInputType() | type);
+ }
+
+ /**
+ * 移除一个输入标记
+ */
+ public void removeInputType(int type) {
+ setInputType(getInputType() & ~type);
+ }
+
+ /**
+ * 设置输入正则
+ */
+ public void setInputRegex(String regex) {
+ if (TextUtils.isEmpty(regex)) {
+ return;
+ }
+
+ mPattern = Pattern.compile(regex);
+ addFilters(this);
+ }
+
+ /**
+ * 获取输入正则
+ */
+ public String getInputRegex() {
+ if (mPattern == null) {
+ return null;
+ }
+ return mPattern.pattern();
+ }
+
+ /**
+ * 添加筛选规则
+ */
+ public void addFilters(InputFilter filter) {
+ if (filter == null) {
+ return;
+ }
+
+ final InputFilter[] newFilters;
+ final InputFilter[] oldFilters = getFilters();
+ if (oldFilters != null && oldFilters.length > 0) {
+ newFilters = new InputFilter[oldFilters.length + 1];
+ // 复制旧数组的元素到新数组中
+ System.arraycopy(oldFilters, 0, newFilters, 0, oldFilters.length);
+ newFilters[oldFilters.length] = filter;
+ } else {
+ newFilters = new InputFilter[1];
+ newFilters[0] = filter;
+ }
+ super.setFilters(newFilters);
+ }
+
+ /**
+ * {@link InputFilter}
+ *
+ * @param source 新输入的字符串
+ * @param start 新输入的字符串起始下标,一般为0
+ * @param end 新输入的字符串终点下标,一般为source长度-1
+ * @param dest 输入之前文本框内容
+ * @param destStart 原内容起始坐标,一般为0
+ * @param destEnd 原内容终点坐标,一般为dest长度-1
+ * @return 返回字符串将会加入到内容中
+ */
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int destStart, int destEnd) {
+ if (mPattern != null) {
+ // 拼接出最终的字符串
+ String begin = dest.toString().substring(0, destStart);
+ String over = dest.toString().substring(destStart + (destEnd - destStart), destStart + (dest.toString().length() - begin.length()));
+ String result = begin + source + over;
+
+ // 判断是插入还是删除
+ if (destStart > destEnd - 1) {
+ // 如果是插入字符
+ if (!mPattern.matcher(result).matches()) {
+ // 如果不匹配就不让这个字符输入
+ return "";
+ }
+ } else {
+ // 如果是删除字符
+ if (!mPattern.matcher(result).matches()) {
+ // 如果不匹配则不让删除(删空操作除外)
+ if (!"".equals(result)) {
+ return dest.toString().substring(destStart, destEnd);
+ }
+ }
+ }
+ }
+
+ // 不做任何修改
+ return source;
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/ScaleImageView.java b/widget/src/main/java/com/example/widget/view/ScaleImageView.java
new file mode 100644
index 0000000..ac19a30
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/ScaleImageView.java
@@ -0,0 +1,53 @@
+package com.example.widget.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+
+import com.example.widget.R;
+
+import androidx.appcompat.widget.AppCompatImageView;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/08/02
+ * desc : 长按缩放松手恢复的 ImageView
+ */
+public final class ScaleImageView extends AppCompatImageView {
+
+ private float mScaleSize = 1.2f;
+
+ public ScaleImageView(Context context) {
+ this(context, null);
+ }
+
+ public ScaleImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ScaleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ScaleImageView);
+ setScaleSize(array.getFloat(R.styleable.ScaleImageView_scaleRatio, mScaleSize));
+ array.recycle();
+ }
+
+ @Override
+ protected void dispatchSetPressed(boolean pressed) {
+ // 判断当前手指是否按下了
+ if (pressed) {
+ setScaleX(mScaleSize);
+ setScaleY(mScaleSize);
+ } else {
+ setScaleX(1);
+ setScaleY(1);
+ }
+ }
+
+ public void setScaleSize(float size) {
+ mScaleSize = size;
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/SmartTextView.java b/widget/src/main/java/com/example/widget/view/SmartTextView.java
new file mode 100644
index 0000000..01fbb81
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/SmartTextView.java
@@ -0,0 +1,58 @@
+package com.example.widget.view;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+
+import androidx.appcompat.widget.AppCompatTextView;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/08/18
+ * desc : 智能显示的 TextView
+ */
+public final class SmartTextView extends AppCompatTextView implements TextWatcher {
+
+ public SmartTextView(Context context) {
+ this(context, null);
+ }
+
+ public SmartTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.textViewStyle);
+ }
+
+ public SmartTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ addTextChangedListener(this);
+ // 触发一次监听
+ afterTextChanged(null);
+ }
+
+ /**
+ * {@link 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) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // 判断当前有没有设置文本达到自动隐藏和显示的效果
+ if (TextUtils.isEmpty(getText().toString())) {
+ if (getVisibility() != GONE) {
+ setVisibility(GONE);
+ }
+ } else {
+ if (getVisibility() != VISIBLE) {
+ setVisibility(VISIBLE);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/java/com/example/widget/view/SwitchButton.java b/widget/src/main/java/com/example/widget/view/SwitchButton.java
new file mode 100644
index 0000000..63019cc
--- /dev/null
+++ b/widget/src/main/java/com/example/widget/view/SwitchButton.java
@@ -0,0 +1,532 @@
+package com.example.widget.view;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+
+import com.example.widget.R;
+
+import androidx.annotation.Nullable;
+
+/**
+ * author : Android 轮子哥
+ * github : https://github.com/getActivity/AndroidProject
+ * time : 2019/02/20
+ * desc : 高仿 ios 开关按钮
+ */
+public final class SwitchButton extends View {
+
+ private static final int STATE_SWITCH_OFF = 1;
+ private static final int STATE_SWITCH_OFF2 = 2;
+ private static final int STATE_SWITCH_ON = 3;
+ private static final int STATE_SWITCH_ON2 = 4;
+
+ private final AccelerateInterpolator mInterpolator = new AccelerateInterpolator(2);
+ private final Paint mPaint = new Paint();
+ private final Path mBackgroundPath = new Path();
+ private final Path mBarPath = new Path();
+ private final RectF mBound = new RectF();
+
+ private float mAnim1, mAnim2;
+ private RadialGradient mShadowGradient;
+
+ /** 按钮宽高形状比率(0,1] 不推荐大幅度调整 */
+ protected final float mAspectRatio = 0.68f;
+ /** (0,1] */
+ protected final float mAnimationSpeed = 0.1f;
+
+ /** 上一个选中状态 */
+ private int mLastCheckedState;
+ /** 当前的选中状态 */
+ private int mCheckedState;
+
+ private boolean isCanVisibleDrawing = false;
+
+ /** 是否显示按钮阴影 */
+ protected boolean isShadow;
+ /** 是否选中 */
+ protected boolean mChecked;
+
+ /** 开启状态背景色 */
+ protected int mAccentColor = 0xFF4BD763;
+ /** 开启状态按钮描边色 */
+ protected int mPrimaryDarkColor = 0xFF3AC652;
+ /** 关闭状态描边色 */
+ protected int mOffColor = 0xFFE3E3E3;
+ /** 关闭状态按钮描边色 */
+ protected int mOffDarkColor = 0xFFBFBFBF;
+ /** 按钮阴影色 */
+ protected int mShadowColor = 0xFF333333;
+ /** 监听器 */
+ private OnCheckedChangeListener mListener;
+
+ private float mRight;
+ private float mCenterX, mCenterY;
+ private float mScale;
+
+ private float mOffset;
+ private float mRadius, mStrokeWidth;
+ private float mWidth;
+ private float mLeft;
+ private float bRight;
+ private float mOnLeftX, mOn2LeftX, mOff2LeftX, mOffLeftX;
+
+ private float mShadowReservedHeight;
+
+ public SwitchButton(Context context) {
+ this(context, null);
+ }
+
+ public SwitchButton(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SwitchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setLayerType(LAYER_TYPE_SOFTWARE, null);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton);
+ mChecked = array.getBoolean(R.styleable.SwitchButton_android_checked, mChecked);
+ setEnabled(array.getBoolean(R.styleable.SwitchButton_android_enabled, isEnabled()));
+ mLastCheckedState = mCheckedState = mChecked ? STATE_SWITCH_ON : STATE_SWITCH_OFF;
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ switch (MeasureSpec.getMode(widthMeasureSpec)) {
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.UNSPECIFIED:
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 56, getResources().getDisplayMetrics())
+ + getPaddingLeft() + getPaddingRight()), MeasureSpec.EXACTLY);
+ break;
+ case MeasureSpec.EXACTLY:
+ default:
+ break;
+ }
+ switch (MeasureSpec.getMode(heightMeasureSpec)) {
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.UNSPECIFIED:
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (MeasureSpec.getSize(widthMeasureSpec) * mAspectRatio)
+ + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY);
+ break;
+ case MeasureSpec.EXACTLY:
+ default:
+ break;
+ }
+ setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ isCanVisibleDrawing = width > getPaddingLeft() + getPaddingRight() && height > getPaddingTop() + getPaddingBottom();
+
+ if (isCanVisibleDrawing) {
+ int actuallyDrawingAreaWidth = width - getPaddingLeft() - getPaddingRight();
+ int actuallyDrawingAreaHeight = height - getPaddingTop() - getPaddingBottom();
+
+ int actuallyDrawingAreaLeft;
+ int actuallyDrawingAreaRight;
+ int actuallyDrawingAreaTop;
+ int actuallyDrawingAreaBottom;
+ if (actuallyDrawingAreaWidth * mAspectRatio < actuallyDrawingAreaHeight) {
+ actuallyDrawingAreaLeft = getPaddingLeft();
+ actuallyDrawingAreaRight = width - getPaddingRight();
+ int heightExtraSize = (int) (actuallyDrawingAreaHeight - actuallyDrawingAreaWidth * mAspectRatio);
+ actuallyDrawingAreaTop = getPaddingTop() + heightExtraSize / 2;
+ actuallyDrawingAreaBottom = getHeight() - getPaddingBottom() - heightExtraSize / 2;
+ } else {
+ int widthExtraSize = (int) (actuallyDrawingAreaWidth - actuallyDrawingAreaHeight / mAspectRatio);
+ actuallyDrawingAreaLeft = getPaddingLeft() + widthExtraSize / 2;
+ actuallyDrawingAreaRight = getWidth() - getPaddingRight() - widthExtraSize / 2;
+ actuallyDrawingAreaTop = getPaddingTop();
+ actuallyDrawingAreaBottom = getHeight() - getPaddingBottom();
+ }
+
+ mShadowReservedHeight = (int) ((actuallyDrawingAreaBottom - actuallyDrawingAreaTop) * 0.07f);
+ float left = actuallyDrawingAreaLeft;
+ float top = actuallyDrawingAreaTop + mShadowReservedHeight;
+ mRight = actuallyDrawingAreaRight;
+ float bottom = actuallyDrawingAreaBottom - mShadowReservedHeight;
+
+ float sHeight = bottom - top;
+ mCenterX = (mRight + left) / 2;
+ mCenterY = (bottom + top) / 2;
+
+ mLeft = left;
+ mWidth = bottom - top;
+ bRight = left + mWidth;
+ // OfB
+ final float halfHeightOfS = mWidth / 2;
+ mRadius = halfHeightOfS * 0.95f;
+ // offset of switching
+ mOffset = mRadius * 0.2f;
+ mStrokeWidth = (halfHeightOfS - mRadius) * 2;
+ mOnLeftX = mRight - mWidth;
+ mOn2LeftX = mOnLeftX - mOffset;
+ mOffLeftX = left;
+ mOff2LeftX = mOffLeftX + mOffset;
+ mScale = 1 - mStrokeWidth / sHeight;
+
+ mBackgroundPath.reset();
+ RectF bound = new RectF();
+ bound.top = top;
+ bound.bottom = bottom;
+ bound.left = left;
+ bound.right = left + sHeight;
+ mBackgroundPath.arcTo(bound, 90, 180);
+ bound.left = mRight - sHeight;
+ bound.right = mRight;
+ mBackgroundPath.arcTo(bound, 270, 180);
+ mBackgroundPath.close();
+
+ mBound.left = mLeft;
+ mBound.right = bRight;
+ // bTop = sTop
+ mBound.top = top + mStrokeWidth / 2;
+ // bBottom = sBottom
+ mBound.bottom = bottom - mStrokeWidth / 2;
+ float bCenterX = (bRight + mLeft) / 2;
+ float bCenterY = (bottom + top) / 2;
+
+ int red = mShadowColor >> 16 & 0xFF;
+ int green = mShadowColor >> 8 & 0xFF;
+ int blue = mShadowColor & 0xFF;
+ mShadowGradient = new RadialGradient(bCenterX, bCenterY, mRadius, Color.argb(200, red, green, blue),
+ Color.argb(25, red, green, blue), Shader.TileMode.CLAMP);
+ }
+ }
+
+ private void calcBPath(float percent) {
+ mBarPath.reset();
+ mBound.left = mLeft + mStrokeWidth / 2;
+ mBound.right = bRight - mStrokeWidth / 2;
+ mBarPath.arcTo(mBound, 90, 180);
+ mBound.left = mLeft + percent * mOffset + mStrokeWidth / 2;
+ mBound.right = bRight + percent * mOffset - mStrokeWidth / 2;
+ mBarPath.arcTo(mBound, 270, 180);
+ mBarPath.close();
+ }
+
+ private float calcBTranslate(float percent) {
+ float result = 0;
+ switch (mCheckedState - mLastCheckedState) {
+ case 1:
+ if (mCheckedState == STATE_SWITCH_OFF2) {
+ // off -> off2
+ result = mOffLeftX;
+ } else if (mCheckedState == STATE_SWITCH_ON) {
+ // on2 -> on
+ result = mOnLeftX - (mOnLeftX - mOn2LeftX) * percent;
+ }
+ break;
+ case 2:
+ if (mCheckedState == STATE_SWITCH_ON) {
+ // off2 -> on
+ result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent;
+ } else if (mCheckedState == STATE_SWITCH_ON2) {
+ // off -> on2
+ result = mOn2LeftX - (mOn2LeftX - mOffLeftX) * percent;
+ }
+ break;
+ case 3:
+ // off -> on
+ result = mOnLeftX - (mOnLeftX - mOffLeftX) * percent;
+ break;
+ case -1:
+ if (mCheckedState == STATE_SWITCH_ON2) {
+ // on -> on2
+ result = mOn2LeftX + (mOnLeftX - mOn2LeftX) * percent;
+ } else if (mCheckedState == STATE_SWITCH_OFF) {
+ // off2 -> off
+ result = mOffLeftX;
+ }
+ break;
+ case -2:
+ if (mCheckedState == STATE_SWITCH_OFF) {
+ // on2 -> off
+ result = mOffLeftX + (mOn2LeftX - mOffLeftX) * percent;
+ } else if (mCheckedState == STATE_SWITCH_OFF2) {
+ // on -> off2
+ result = mOff2LeftX + (mOnLeftX - mOff2LeftX) * percent;
+ }
+ break;
+ case -3:
+ // on -> off
+ result = mOffLeftX + (mOnLeftX - mOffLeftX) * percent;
+ break;
+ default: // init
+ case 0:
+ if (mCheckedState == STATE_SWITCH_OFF) {
+ // off -> off
+ result = mOffLeftX;
+ } else if (mCheckedState == STATE_SWITCH_ON) {
+ // on -> on
+ result = mOnLeftX;
+ }
+ break;
+ }
+ return result - mOffLeftX;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (!isCanVisibleDrawing) {
+ return;
+ }
+
+ mPaint.setAntiAlias(true);
+
+ final boolean isOn = (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_ON2);
+ // Draw background
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(isOn ? mAccentColor : mOffColor);
+ canvas.drawPath(mBackgroundPath, mPaint);
+
+ mAnim1 = mAnim1 - mAnimationSpeed > 0 ? mAnim1 - mAnimationSpeed : 0;
+ mAnim2 = mAnim2 - mAnimationSpeed > 0 ? mAnim2 - mAnimationSpeed : 0;
+
+ final float dsAnim = mInterpolator.getInterpolation(mAnim1);
+ final float dbAnim = mInterpolator.getInterpolation(mAnim2);
+ // Draw background animation
+ final float scale = mScale * (isOn ? dsAnim : 1 - dsAnim);
+ final float scaleOffset = (mRight - mCenterX - mRadius) * (isOn ? 1 - dsAnim : dsAnim);
+ canvas.save();
+ canvas.scale(scale, scale, mCenterX + scaleOffset, mCenterY);
+ mPaint.setColor(0xFFFFFFFF);
+ canvas.drawPath(mBackgroundPath, mPaint);
+ canvas.restore();
+ // To prepare center bar path
+ canvas.save();
+ canvas.translate(calcBTranslate(dbAnim), mShadowReservedHeight);
+ final boolean isState2 = (mCheckedState == STATE_SWITCH_ON2 || mCheckedState == STATE_SWITCH_OFF2);
+ calcBPath(isState2 ? 1 - dbAnim : dbAnim);
+ // Use center bar path to draw shadow
+ if (isShadow) {
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setShader(mShadowGradient);
+ canvas.drawPath(mBarPath, mPaint);
+ mPaint.setShader(null);
+ }
+ canvas.translate(0, -mShadowReservedHeight);
+ // draw bar
+ canvas.scale(0.98f, 0.98f, mWidth / 2, mWidth / 2);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(0xFFFFFFFF);
+ canvas.drawPath(mBarPath, mPaint);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mStrokeWidth * 0.5f);
+ mPaint.setColor(isOn ? mPrimaryDarkColor : mOffDarkColor);
+ canvas.drawPath(mBarPath, mPaint);
+ canvas.restore();
+
+ mPaint.reset();
+ if (mAnim1 > 0 || mAnim2 > 0) {
+ invalidate();
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (isEnabled()
+ && (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_OFF)
+ && (mAnim1 * mAnim2 == 0)) {
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_UP:
+ mLastCheckedState = mCheckedState;
+ mAnim2 = 1;
+
+ switch (mCheckedState) {
+ case STATE_SWITCH_OFF:
+ setChecked(true, false);
+ if (mListener != null) {
+ mListener.onCheckedChanged(this, true);
+ }
+ break;
+ case STATE_SWITCH_ON:
+ setChecked(false, false);
+ if (mListener != null) {
+ mListener.onCheckedChanged(this, false);
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ case MotionEvent.ACTION_DOWN:
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState state = new SavedState(superState);
+ state.checked = mChecked;
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ mChecked = savedState.checked;
+ mCheckedState = mChecked ? STATE_SWITCH_ON : STATE_SWITCH_OFF;
+ invalidate();
+ }
+
+ public void setColor(int newColorPrimary, int newColorPrimaryDark) {
+ setColor(newColorPrimary, newColorPrimaryDark, mOffColor, mOffDarkColor);
+ }
+
+ public void setColor(int newColorPrimary, int newColorPrimaryDark, int newColorOff, int newColorOffDark) {
+ setColor(newColorPrimary, newColorPrimaryDark, newColorOff, newColorOffDark, mShadowColor);
+ }
+
+ public void setColor(int newColorPrimary, int newColorPrimaryDark, int newColorOff, int newColorOffDark, int newColorShadow) {
+ mAccentColor = newColorPrimary;
+ mPrimaryDarkColor = newColorPrimaryDark;
+ mOffColor = newColorOff;
+ mOffDarkColor = newColorOffDark;
+ mShadowColor = newColorShadow;
+ invalidate();
+ }
+
+ /**
+ * 设置按钮阴影开关
+ */
+ public void setShadow(boolean shadow) {
+ isShadow = shadow;
+ invalidate();
+ }
+
+ /**
+ * 当前状态是否选中
+ */
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
+ * 设置选择状态(默认会回调监听器)
+ */
+ public void setChecked(boolean checked) {
+ // 回调监听器
+ setChecked(checked, true);
+ }
+
+ /**
+ * 设置选择状态
+ */
+ public void setChecked(boolean checked, boolean callback) {
+ int newState = checked ? STATE_SWITCH_ON : STATE_SWITCH_OFF;
+ if (newState == mCheckedState) {
+ return;
+ }
+ if ((newState == STATE_SWITCH_ON && (mCheckedState == STATE_SWITCH_OFF || mCheckedState == STATE_SWITCH_OFF2))
+ || (newState == STATE_SWITCH_OFF && (mCheckedState == STATE_SWITCH_ON || mCheckedState == STATE_SWITCH_ON2))) {
+ mAnim1 = 1;
+ }
+ mAnim2 = 1;
+
+ if (!mChecked && newState == STATE_SWITCH_ON) {
+ mChecked = true;
+ } else if (mChecked && newState == STATE_SWITCH_OFF) {
+ mChecked = false;
+ }
+ mLastCheckedState = mCheckedState;
+ mCheckedState = newState;
+ postInvalidate();
+
+ if (callback && mListener != null) {
+ mListener.onCheckedChanged(this, checked);
+ }
+ }
+
+ /**
+ * 设置选中状态改变监听
+ */
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ mListener = listener;
+ }
+
+ public interface OnCheckedChangeListener {
+ /**
+ * 回调监听
+ *
+ * @param button 切换按钮
+ * @param isChecked 是否选中
+ */
+ void onCheckedChanged(SwitchButton button, boolean isChecked);
+ }
+
+ /**
+ * 保存开关状态
+ */
+ private static final class SavedState extends BaseSavedState {
+
+ private boolean checked;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ checked = 1 == in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(checked ? 1 : 0);
+ }
+
+ /**
+ * fixed by Night99 https://github.com/g19980115
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/res/drawable/icon.png b/widget/src/main/res/drawable/icon.png
new file mode 100644
index 0000000..8de003e
Binary files /dev/null and b/widget/src/main/res/drawable/icon.png differ
diff --git a/widget/src/main/res/drawable/input_delete_ic.xml b/widget/src/main/res/drawable/input_delete_ic.xml
new file mode 100644
index 0000000..7f143c4
--- /dev/null
+++ b/widget/src/main/res/drawable/input_delete_ic.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/widget/src/main/res/drawable/password_off_ic.xml b/widget/src/main/res/drawable/password_off_ic.xml
new file mode 100644
index 0000000..cae4597
--- /dev/null
+++ b/widget/src/main/res/drawable/password_off_ic.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/widget/src/main/res/drawable/password_on_ic.xml b/widget/src/main/res/drawable/password_on_ic.xml
new file mode 100644
index 0000000..dbff3e4
--- /dev/null
+++ b/widget/src/main/res/drawable/password_on_ic.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/widget/src/main/res/values/attrs.xml b/widget/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..8c19df4
--- /dev/null
+++ b/widget/src/main/res/values/attrs.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/widget/src/test/java/com/example/widget/ExampleUnitTest.java b/widget/src/test/java/com/example/widget/ExampleUnitTest.java
new file mode 100644
index 0000000..53d2d07
--- /dev/null
+++ b/widget/src/test/java/com/example/widget/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.widget;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file