From c9c1f80d72a0f5bea64dbf3e44311b60b21c2473 Mon Sep 17 00:00:00 2001 From: huangwei <983142558@qq.com> Date: Thu, 1 Aug 2024 11:53:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=91=E7=9A=84=E5=92=8C=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 3 +- app/src/main/AndroidManifest.xml | 24 +- app/src/main/java/com/project/survey/App.java | 1 - .../project/survey/activity/LoginActivity.kt | 90 + .../activity/base/BaseBindingActivity.kt | 1 - .../survey/fragment/home/MeFragment.kt | 7 + .../com/project/survey/util/ColorUtils.java | 237 + .../com/project/survey/util/DensityUtils.java | 343 ++ .../com/project/survey/util/DeviceUtils.java | 271 + .../project/survey/util/DrawableUtils.java | 640 +++ .../project/survey/util/KeyboardUtils.java | 525 ++ .../com/project/survey/util/ResUtils.java | 447 ++ .../project/survey/util/StatusBarUtils.java | 734 +++ .../com/project/survey/util/ThemeUtils.java | 414 ++ .../java/com/project/survey/widget/XUI.java | 114 + .../AsteriskPasswordTransformationMethod.java | 69 + .../survey/widget/edittext/GravityEnum.java | 74 + .../survey/widget/edittext/PassEdittext2.java | 318 ++ .../widget/edittext/PasswordEditText.java | 323 ++ .../materialedittext/MaterialEditText.java | 1788 +++++++ .../validation/METLengthChecker.java | 13 + .../validation/METValidator.java | 43 + .../validation/NotAllowEmptyValidator.java | 38 + .../validation/RegexpValidator.java | 31 + .../com/project/survey/widget/util/Utils.java | 470 ++ app/src/main/res/drawable/about.xml | 10 + app/src/main/res/drawable/arrow_right.xml | 9 + app/src/main/res/drawable/bg_btn_login.xml | 9 + app/src/main/res/drawable/bg_pwd_sc.xml | 5 + app/src/main/res/drawable/bg_top_blue.xml | 2 +- app/src/main/res/drawable/change_pwd.xml | 10 + app/src/main/res/drawable/login_error.xml | 10 + .../res/drawable/pet_icon_visibility_24dp.xml | 9 + .../drawable/pet_icon_visibility_off_24dp.xml | 9 + app/src/main/res/drawable/pwd_hide.xml | 10 + app/src/main/res/drawable/pwd_show.xml | 10 + app/src/main/res/drawable/switch_project.xml | 10 + .../res/drawable/xui_ic_default_clear_btn.xml | 26 + app/src/main/res/layout/activity_login.xml | 166 + app/src/main/res/layout/fragment_me.xml | 180 +- app/src/main/res/layout/line_hor.xml | 5 + app/src/main/res/layout/toolbar.xml | 10 + app/src/main/res/values-sw300dp/dimens.xml | 1 + app/src/main/res/values-sw320dp/dimens.xml | 1 + app/src/main/res/values-sw340dp/dimens.xml | 1 + app/src/main/res/values-sw360dp/dimens.xml | 1 + app/src/main/res/values-sw380dp/dimens.xml | 1 + app/src/main/res/values-sw400dp/dimens.xml | 1 + app/src/main/res/values-sw410dp/dimens.xml | 1 + app/src/main/res/values-sw420dp/dimens.xml | 4515 +++++++++-------- app/src/main/res/values-sw440dp/dimens.xml | 1 + app/src/main/res/values-sw460dp/dimens.xml | 1 + app/src/main/res/values-sw480dp/dimens.xml | 1 + app/src/main/res/values-sw500dp/dimens.xml | 1 + app/src/main/res/values-sw520dp/dimens.xml | 1 + app/src/main/res/values/attr_widget.xml | 131 + app/src/main/res/values/color.xml | 12 +- app/src/main/res/values/dimens.xml | 4515 +++++++++-------- app/src/main/res/values/strings.xml | 16 +- app/src/main/res/values/styles.xml | 21 +- app/src/main/res/values/xui_dimens.xml | 478 ++ 61 files changed, 12660 insertions(+), 4548 deletions(-) create mode 100644 app/src/main/java/com/project/survey/activity/LoginActivity.kt create mode 100644 app/src/main/java/com/project/survey/util/ColorUtils.java create mode 100644 app/src/main/java/com/project/survey/util/DensityUtils.java create mode 100644 app/src/main/java/com/project/survey/util/DeviceUtils.java create mode 100644 app/src/main/java/com/project/survey/util/DrawableUtils.java create mode 100644 app/src/main/java/com/project/survey/util/KeyboardUtils.java create mode 100644 app/src/main/java/com/project/survey/util/ResUtils.java create mode 100644 app/src/main/java/com/project/survey/util/StatusBarUtils.java create mode 100644 app/src/main/java/com/project/survey/util/ThemeUtils.java create mode 100644 app/src/main/java/com/project/survey/widget/XUI.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/AsteriskPasswordTransformationMethod.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/GravityEnum.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/PassEdittext2.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/PasswordEditText.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/materialedittext/MaterialEditText.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METLengthChecker.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METValidator.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/NotAllowEmptyValidator.java create mode 100644 app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/RegexpValidator.java create mode 100644 app/src/main/java/com/project/survey/widget/util/Utils.java create mode 100644 app/src/main/res/drawable/about.xml create mode 100644 app/src/main/res/drawable/arrow_right.xml create mode 100644 app/src/main/res/drawable/bg_btn_login.xml create mode 100644 app/src/main/res/drawable/bg_pwd_sc.xml create mode 100644 app/src/main/res/drawable/change_pwd.xml create mode 100644 app/src/main/res/drawable/login_error.xml create mode 100644 app/src/main/res/drawable/pet_icon_visibility_24dp.xml create mode 100644 app/src/main/res/drawable/pet_icon_visibility_off_24dp.xml create mode 100644 app/src/main/res/drawable/pwd_hide.xml create mode 100644 app/src/main/res/drawable/pwd_show.xml create mode 100644 app/src/main/res/drawable/switch_project.xml create mode 100644 app/src/main/res/drawable/xui_ic_default_clear_btn.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/line_hor.xml create mode 100644 app/src/main/res/layout/toolbar.xml create mode 100644 app/src/main/res/values/attr_widget.xml create mode 100644 app/src/main/res/values/xui_dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 066bcbb..2f3415d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,6 +170,7 @@ dependencies { implementation "androidx.core:core-ktx:1.13.1" implementation "com.github.zagum:Android-SwitchIcon:1.4.0"//必须的 - + implementation 'io.github.inflationx:calligraphy3:3.1.1' +// implementation 'io.github.inflationx:viewpump:2.0.3' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index acf8249..532aa32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,15 +45,6 @@ android:name="org.apache.http.legacy" android:required="false" /> - - - - - + + + + + + + + diff --git a/app/src/main/java/com/project/survey/App.java b/app/src/main/java/com/project/survey/App.java index c8d2342..41ba619 100644 --- a/app/src/main/java/com/project/survey/App.java +++ b/app/src/main/java/com/project/survey/App.java @@ -23,7 +23,6 @@ public class App extends Application { super.onCreate(); app = this; - initThemeDark(); RxJavaPlugins.setErrorHandler(new Consumer() { @Override diff --git a/app/src/main/java/com/project/survey/activity/LoginActivity.kt b/app/src/main/java/com/project/survey/activity/LoginActivity.kt new file mode 100644 index 0000000..0d3b55e --- /dev/null +++ b/app/src/main/java/com/project/survey/activity/LoginActivity.kt @@ -0,0 +1,90 @@ +package com.project.survey.activity + +import android.text.method.HideReturnsTransformationMethod +import android.text.method.PasswordTransformationMethod +import android.view.View +import com.project.survey.R +import com.project.survey.activity.base.BaseBindingActivity +import com.project.survey.databinding.ActivityLoginBinding +import com.project.survey.util.Tools + +class LoginActivity : BaseBindingActivity() { + + private var isOutLogin = false//默认集团登录 + + override fun getBinding(): ActivityLoginBinding { + return ActivityLoginBinding.inflate(layoutInflater) + } + + override fun initView() { + mBinding.pwdShowHide.setOnClickListener { + mBinding.ivPwdShowHide.isSelected = !mBinding.ivPwdShowHide.isSelected + if (mBinding.ivPwdShowHide.isSelected) { + mBinding.etPwd.transformationMethod = HideReturnsTransformationMethod.getInstance() + } else { + mBinding.etPwd.transformationMethod = PasswordTransformationMethod.getInstance() + } + mBinding.etPwd.setSelection(mBinding.etPwd.text?.length ?: 0) + } + + mBinding.tvShowOutLogin.setOnClickListener { + //切换到外部登录 + isOutLogin = true + refreshInnerOutUi() + } + + mBinding.tvShowInnerLogin.setOnClickListener { + //切换到内部集团登录 + isOutLogin = false + refreshInnerOutUi() + } + + mBinding.btnLogin.setOnClickListener { + if (isOutLogin) { + loginByOut() + } else { + loginByInner() + } + } + + } + + /** + * 内部登录 + */ + private fun loginByInner() { + + + } + + /** + * 外部登录 + */ + private fun loginByOut() { + + + } + + private fun refreshInnerOutUi() { + if (isOutLogin) { + //当前外部登录 + mBinding.tvAccountDesc.text = Tools.getString(R.string.external_account_login) + + mBinding.llShowOutLogin.visibility = View.GONE + mBinding.llShowInnerLogin.visibility = View.VISIBLE + } else { + mBinding.tvAccountDesc.text = Tools.getString(R.string.group_account_login) + + mBinding.llShowOutLogin.visibility = View.VISIBLE + mBinding.llShowInnerLogin.visibility = View.GONE + } + + mBinding.etAccount.text = null + mBinding.etPwd.text = null + } + + override fun initData() { + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/project/survey/activity/base/BaseBindingActivity.kt b/app/src/main/java/com/project/survey/activity/base/BaseBindingActivity.kt index f8820d2..790104b 100644 --- a/app/src/main/java/com/project/survey/activity/base/BaseBindingActivity.kt +++ b/app/src/main/java/com/project/survey/activity/base/BaseBindingActivity.kt @@ -28,7 +28,6 @@ abstract class BaseBindingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initTheme() setContentView(getContentView()) //设置背景色 mBinding.root.setBackgroundColor(Tools.getColor(R.color.bg_content)) diff --git a/app/src/main/java/com/project/survey/fragment/home/MeFragment.kt b/app/src/main/java/com/project/survey/fragment/home/MeFragment.kt index 44f31a1..c794248 100644 --- a/app/src/main/java/com/project/survey/fragment/home/MeFragment.kt +++ b/app/src/main/java/com/project/survey/fragment/home/MeFragment.kt @@ -1,7 +1,9 @@ package com.project.survey.fragment.home +import android.content.Intent import android.view.LayoutInflater import android.view.ViewGroup +import com.project.survey.activity.LoginActivity import com.project.survey.databinding.FragmentMeBinding import com.project.survey.fragment.base.BaseFragmentBinding @@ -14,6 +16,11 @@ class MeFragment : BaseFragmentBinding() { } override fun initView() { + + mBinding.llLogin.setOnClickListener { + startActivity(Intent(context, LoginActivity::class.java)) + } + } override fun initData() { diff --git a/app/src/main/java/com/project/survey/util/ColorUtils.java b/app/src/main/java/com/project/survey/util/ColorUtils.java new file mode 100644 index 0000000..43c1911 --- /dev/null +++ b/app/src/main/java/com/project/survey/util/ColorUtils.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.project.survey.util; + +import android.graphics.Color; + +import androidx.annotation.ColorInt; + +import java.util.Random; + +/** + * 颜色辅助工具 + * + * @author xuexiang + * @since 2018/12/27 下午3:00 + */ +public final class ColorUtils { + + private ColorUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + public static int setColorAlpha(@ColorInt int color, float alpha) { + return setColorAlpha(color, alpha, true); + } + + /** + * 设置颜色的alpha值 + * + * @param color 需要被设置的颜色值 + * @param alpha 取值为[0,1],0表示全透明,1表示不透明 + * @param override 覆盖原本的 alpha + * @return 返回改变了 alpha 值的颜色值 + */ + public static int setColorAlpha(@ColorInt int color, float alpha, boolean override) { + int origin = override ? 0xff : (color >> 24) & 0xff; + return color & 0x00ffffff | (int) (alpha * origin) << 24; + } + + /** + * 根据比例,在两个color值之间计算出一个color值 + * 注意该方法是ARGB通道分开计算比例的 + * + * @param fromColor 开始的color值 + * @param toColor 最终的color值 + * @param fraction 比例,取值为[0,1],为0时返回 fromColor, 为1时返回 toColor + * @return 计算出的color值 + */ + public static int computeColor(@ColorInt int fromColor, @ColorInt int toColor, float fraction) { + fraction = Math.max(Math.min(fraction, 1), 0); + + int minColorA = Color.alpha(fromColor); + int maxColorA = Color.alpha(toColor); + int resultA = (int) ((maxColorA - minColorA) * fraction) + minColorA; + + int minColorR = Color.red(fromColor); + int maxColorR = Color.red(toColor); + int resultR = (int) ((maxColorR - minColorR) * fraction) + minColorR; + + int minColorG = Color.green(fromColor); + int maxColorG = Color.green(toColor); + int resultG = (int) ((maxColorG - minColorG) * fraction) + minColorG; + + int minColorB = Color.blue(fromColor); + int maxColorB = Color.blue(toColor); + int resultB = (int) ((maxColorB - minColorB) * fraction) + minColorB; + + return Color.argb(resultA, resultR, resultG, resultB); + } + + /** + * 将 color 颜色值转换为十六进制字符串 + * + * @param color 颜色值 + * @return 转换后的字符串 + */ + public static String colorToString(@ColorInt int color) { + return String.format("#%08X", color); + } + + /** + * 加深颜色 + * + * @param color 需要加深的颜色 + */ + public static int darker(int color) { + return darker(color, 0.8F); + } + + /** + * 加深颜色 + * + * @param color 需要加深的颜色 + * @param factor The factor to darken the color. + * @return darker version of specified color. + */ + public static int darker(int color, float factor) { + return Color.argb(Color.alpha(color), Math.max((int) (Color.red(color) * factor), 0), + Math.max((int) (Color.green(color) * factor), 0), + Math.max((int) (Color.blue(color) * factor), 0)); + } + + /** + * 变浅颜色 + * + * @param color 需要变浅的颜色 + */ + public static int lighter(int color) { + return lighter(color, 0.8F); + } + + /** + * 变浅颜色 + * + * @param color 需要变浅的颜色 + * @param factor The factor to lighten the color. 0 will make the color unchanged. 1 will make the + * color white. + * @return lighter version of the specified color. + */ + public static int lighter(int color, float factor) { + int red = (int) ((Color.red(color) * (1 - factor) / 255 + factor) * 255); + int green = (int) ((Color.green(color) * (1 - factor) / 255 + factor) * 255); + int blue = (int) ((Color.blue(color) * (1 - factor) / 255 + factor) * 255); + return Color.argb(Color.alpha(color), red, green, blue); + } + + /** + * 是否是深色的颜色 + * + * @param color + * @return + */ + public static boolean isColorDark(@ColorInt int color) { + return isColorDark(color, 0.5F); + } + + + /** + * 是否是深色的颜色 + * + * @param color 颜色 + * @param factor 比例 + * @return + */ + public static boolean isColorDark(@ColorInt int color, double factor) { + double darkness = + 1 + - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) + / 255; + return darkness >= factor; + } + + /** + * @return 获取随机色 + */ + public static int getRandomColor() { + return new RandomColor(255, 0, 255).getColor(); + } + + /** + * 随机颜色 + */ + public static class RandomColor { + int alpha; + int lower; + int upper; + + RandomColor(int alpha, int lower, int upper) { + if (upper <= lower) { + throw new IllegalArgumentException("must be lower < upper"); + } + setAlpha(alpha); + setLower(lower); + setUpper(upper); + } + + public int getColor() { + //随机数是前闭 后开 + int red = getLower() + new Random().nextInt(getUpper() - getLower() + 1); + int green = getLower() + new Random().nextInt(getUpper() - getLower() + 1); + int blue = getLower() + new Random().nextInt(getUpper() - getLower() + 1); + return Color.argb(getAlpha(), red, green, blue); + } + + public int getAlpha() { + return alpha; + } + + public void setAlpha(int alpha) { + if (alpha > 255) { + alpha = 255; + } + if (alpha < 0) { + alpha = 0; + } + this.alpha = alpha; + } + + int getLower() { + return lower; + } + + void setLower(int lower) { + if (lower < 0) { + lower = 0; + } + this.lower = lower; + } + + int getUpper() { + return upper; + } + + void setUpper(int upper) { + if (upper > 255) { + upper = 255; + } + this.upper = upper; + } + } + + +} diff --git a/app/src/main/java/com/project/survey/util/DensityUtils.java b/app/src/main/java/com/project/survey/util/DensityUtils.java new file mode 100644 index 0000000..4a0a582 --- /dev/null +++ b/app/src/main/java/com/project/survey/util/DensityUtils.java @@ -0,0 +1,343 @@ +package com.project.survey.util; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import com.project.survey.App; + + +/** + * 屏幕密度工具类 + * + * @author xuexiang + * @since 2018/12/18 上午12:15 + */ +public final class DensityUtils { + + private DensityUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * DisplayMetrics + * + * @return 屏幕密度 + */ + @Deprecated + public static DisplayMetrics getDisplayMetrics() { + return ResUtils.getResources().getDisplayMetrics(); + } + + /** + * DisplayMetrics + * + * @return 屏幕密度 + */ + public static DisplayMetrics getDisplayMetrics(@NonNull Context context) { + return context.getResources().getDisplayMetrics(); + } + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + * + * @param dpValue 尺寸dip + * @return 像素值 + */ + @Deprecated + public static int dp2px(float dpValue) { + final float scale = ResUtils.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + * + * @param context 上下文 + * @param dpValue 尺寸dip + * @return 像素值 + */ + public static int dp2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + * + * @param pxValue 尺寸像素 + * @return DIP值 + */ + @Deprecated + public static int px2dp(float pxValue) { + final float scale = ResUtils.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + * + * @param context 上下文 + * @param pxValue 尺寸像素 + * @return DIP值 + */ + public static int px2dp(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 sp + * + * @param pxValue 尺寸像素 + * @return SP值 + */ + @Deprecated + public static int px2sp(float pxValue) { + float fontScale = ResUtils.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 sp + * + * @param pxValue 尺寸像素 + * @return SP值 + */ + public static int px2sp(Context context, float pxValue) { + float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 根据手机的分辨率从 sp 的单位 转成为 px + * + * @param spValue SP值 + * @return 像素值 + */ + @Deprecated + public static int sp2px(float spValue) { + float fontScale = ResUtils.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * 根据手机的分辨率从 sp 的单位 转成为 px + * + * @param spValue SP值 + * @return 像素值 + */ + public static int sp2px(Context context, float spValue) { + float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * 获取屏幕分辨率 + * + * @return 屏幕分辨率幕高度 + */ + @Deprecated + public static int getScreenDpi() { + return getDisplayMetrics().densityDpi; + } + + /** + * 获取屏幕分辨率 + * + * @param context 上下文 + * @return 屏幕分辨率幕高度 + */ + public static int getScreenDpi(@NonNull Context context) { + return getDisplayMetrics(context).densityDpi; + } + + /** + * 获取真实屏幕密度 + * + * @param context 上下文【注意,Application和Activity的屏幕密度是不一样的】 + * @return + */ + public static int getRealDpi(Context context) { + DisplayMetrics metric = context.getResources().getDisplayMetrics(); + float xdpi = metric.xdpi; + float ydpi = metric.ydpi; + + return (int) (((xdpi + ydpi) / 2.0F) + 0.5F); + } + + /** + * 获取应用窗口的尺寸 + * + * @param activity 应用窗口 + * @param isReal 是否是真实的尺寸 + * @return 应用窗口的尺寸 + */ + public static Point getAppSize(Activity activity, boolean isReal) { + return getDisplaySize(activity, isReal); + } + + /** + * 获取屏幕的尺寸 + * + * @param isReal 是否是真实的尺寸 + * @return 屏幕的尺寸 + */ + public static Point getScreenSize(boolean isReal) { + return getDisplaySize(App.getApp(), isReal); + } + + /** + * 获取上下文所在的尺寸 + * + * @param context 上下文 + * @param isReal 是否是真实的尺寸 + * @return 上下文所在的尺寸 + */ + public static Point getDisplaySize(Context context, boolean isReal) { + WindowManager windowManager; + if (context instanceof Activity) { + windowManager = ((Activity) context).getWindowManager(); + } else { + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + if (windowManager == null) { + return null; + } + Display display = windowManager.getDefaultDisplay(); + Point point = new Point(); + if (isReal) { + display.getRealSize(point); + } else { + display.getSize(point); + } + return point; + } + + /** + * 获取上下文所在的宽度 + * + * @param context 上下文 + * @param isReal 是否是真实的尺寸 + * @return 上下文所在的宽度 + */ + public static int getDisplayWidth(Context context, boolean isReal) { + Point point = getDisplaySize(context, isReal); + return point != null ? point.x : 0; + } + + /** + * 获取上下文所在的高度 + * + * @param context 上下文 + * @param isReal 是否是真实的尺寸 + * @return 上下文所在的高度 + */ + public static int getDisplayHeight(Context context, boolean isReal) { + Point point = getDisplaySize(context, isReal); + return point != null ? point.y : 0; + } + + /** + * 获取应用窗口的度量信息 + * + * @param activity 应用窗口 + * @param isReal 是否是真实的度量信息 + * @return 应用窗口的度量信息 + */ + public static DisplayMetrics getAppMetrics(Activity activity, boolean isReal) { + return getDisplayMetrics(activity, isReal); + } + + /** + * 获取屏幕的度量信息 + * + * @param isReal 是否是真实的度量信息 + * @return 屏幕的度量信息 + */ + public static DisplayMetrics getScreenMetrics(boolean isReal) { + return getDisplayMetrics(App.getApp(), isReal); + } + + /** + * 获取上下文所在的度量信息 + * + * @param context 上下文 + * @param isReal 是否是真实的度量信息 + * @return 上下文所在的度量信息 + */ + public static DisplayMetrics getDisplayMetrics(Context context, boolean isReal) { + WindowManager windowManager; + if (context instanceof Activity) { + windowManager = ((Activity) context).getWindowManager(); + } else { + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + if (windowManager == null) { + return null; + } + Display display = windowManager.getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + if (isReal) { + display.getRealMetrics(metrics); + } else { + display.getMetrics(metrics); + } + return metrics; + } + + + /** + * 底部导航条是否开启 + * + * @param context 上下文 + * @return 底部导航条是否显示 + */ + public static boolean isNavigationBarExist(Context context) { + return getNavigationBarHeight(context) > 0; + } + + /** + * 获取系统底部导航栏的高度 + * + * @param context 上下文 + * @return 系统状态栏的高度 + */ + public static int getNavigationBarHeight(Context context) { + WindowManager windowManager; + if (context instanceof Activity) { + windowManager = ((Activity) context).getWindowManager(); + } else { + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + if (windowManager == null) { + return 0; + } + Display defaultDisplay = windowManager.getDefaultDisplay(); + DisplayMetrics realDisplayMetrics = new DisplayMetrics(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + defaultDisplay.getRealMetrics(realDisplayMetrics); + } + int realHeight = realDisplayMetrics.heightPixels; + int realWidth = realDisplayMetrics.widthPixels; + + DisplayMetrics displayMetrics = new DisplayMetrics(); + defaultDisplay.getMetrics(displayMetrics); + + int displayHeight = displayMetrics.heightPixels; + int displayWidth = displayMetrics.widthPixels; + + if (realHeight - displayHeight > 0) { + return realHeight - displayHeight; + } + return Math.max(realWidth - displayWidth, 0); + } + + +} diff --git a/app/src/main/java/com/project/survey/util/DeviceUtils.java b/app/src/main/java/com/project/survey/util/DeviceUtils.java new file mode 100644 index 0000000..51f0dd9 --- /dev/null +++ b/app/src/main/java/com/project/survey/util/DeviceUtils.java @@ -0,0 +1,271 @@ +package com.project.survey.util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Binder; +import android.os.Build; +import android.os.Environment; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import com.project.survey.widget.util.Utils; + +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.Method; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 设备类型工具类 + * + * @author XUE + * @since 2019/3/22 10:56 + */ +@SuppressLint("PrivateApi") +public class DeviceUtils { + private final static String KEY_MIUI_VERSION_NAME = "ro.miui.ui.version.name"; + private static final String KEY_FLYME_VERSION_NAME = "ro.build.display.id"; + private final static String FLYME = "flyme"; + private final static String ZTEC2016 = "zte c2016"; + private final static String ZUKZ1 = "zuk z1"; + private final static String ESSENTIAL = "essential"; + private final static String[] MEIZUBOARD = {"m9", "M9", "mx", "MX"}; + private static String sMiuiVersionName; + private static String sFlymeVersionName; + private static boolean sIsTabletChecked = false; + private static boolean sIsTabletValue = false; + private static final String BRAND = Build.BRAND.toLowerCase(); + + private DeviceUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + static { + Properties properties = new Properties(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // android 8.0,读取 /system/uild.prop 会报 permission denied + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(new File(Environment.getRootDirectory(), "build.prop")); + properties.load(fileInputStream); + } catch (Exception e) { + e.printStackTrace(); + } finally { + Utils.closeIOQuietly(fileInputStream); + } + } + + Class clzSystemProperties = null; + try { + clzSystemProperties = Class.forName("android.os.SystemProperties"); + Method getMethod = clzSystemProperties.getDeclaredMethod("get", String.class); + // miui + sMiuiVersionName = getLowerCaseName(properties, getMethod, KEY_MIUI_VERSION_NAME); + //flyme + sFlymeVersionName = getLowerCaseName(properties, getMethod, KEY_FLYME_VERSION_NAME); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static boolean _isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= + Configuration.SCREENLAYOUT_SIZE_LARGE; + } + + /** + * 判断是否为平板设备 + */ + public static boolean isTablet(Context context) { + if (sIsTabletChecked) { + return sIsTabletValue; + } + sIsTabletValue = _isTablet(context); + sIsTabletChecked = true; + return sIsTabletValue; + } + + /** + * 判断是否是flyme系统 + */ + public static boolean isFlyme() { + return !TextUtils.isEmpty(sFlymeVersionName) && sFlymeVersionName.contains(FLYME); + } + + /** + * 判断是否是MIUI系统 + */ + public static boolean isMIUI() { + return !TextUtils.isEmpty(sMiuiVersionName); + } + + public static boolean isMIUIV5() { + return "v5".equals(sMiuiVersionName); + } + + public static boolean isMIUIV6() { + return "v6".equals(sMiuiVersionName); + } + + public static boolean isMIUIV7() { + return "v7".equals(sMiuiVersionName); + } + + public static boolean isMIUIV8() { + return "v8".equals(sMiuiVersionName); + } + + public static boolean isMIUIV9() { + return "v9".equals(sMiuiVersionName); + } + + public static boolean isFlymeVersionHigher5_2_4() { + //查不到默认高于5.2.4 + boolean isHigher = true; + if (sFlymeVersionName != null && !"".equals(sFlymeVersionName)) { + Pattern pattern = Pattern.compile("(\\d+\\.){2}\\d"); + Matcher matcher = pattern.matcher(sFlymeVersionName); + if (matcher.find()) { + String versionString = matcher.group(); + if (versionString != null && !"".equals(versionString)) { + String[] version = versionString.split("\\."); + if (version.length == 3) { + if (Integer.parseInt(version[0]) < 5) { + isHigher = false; + } else if (Integer.parseInt(version[0]) > 5) { + isHigher = true; + } else { + if (Integer.parseInt(version[1]) < 2) { + isHigher = false; + } else if (Integer.parseInt(version[1]) > 2) { + isHigher = true; + } else { + if (Integer.parseInt(version[2]) < 4) { + isHigher = false; + } else if (Integer.parseInt(version[2]) >= 5) { + isHigher = true; + } + } + } + } + + } + } + } + return isMeizu() && isHigher; + } + + public static boolean isMeizu() { + return isPhone(MEIZUBOARD) || isFlyme(); + } + + /** + * 判断是否为小米 + * https://dev.mi.com/doc/?p=254 + */ + public static boolean isXiaomi() { + return "xiaomi".equals(Build.MANUFACTURER.toLowerCase()); + } + + public static boolean isVivo() { + return BRAND.contains("vivo") || BRAND.contains("bbk"); + } + + public static boolean isOppo() { + return BRAND.contains("oppo"); + } + + public static boolean isHuawei() { + return BRAND.contains("huawei") || BRAND.contains("honor"); + } + + public static boolean isEssentialPhone() { + return BRAND.contains("essential"); + } + + + /** + * 判断是否为 ZUK Z1 和 ZTK C2016。 + * 两台设备的系统虽然为 android 6.0,但不支持状态栏icon颜色改变,因此经常需要对它们进行额外判断。 + */ + public static boolean isZUKZ1() { + final String board = Build.MODEL; + return board != null && board.toLowerCase().contains(ZUKZ1); + } + + public static boolean isZTKC2016() { + final String board = Build.MODEL; + return board != null && board.toLowerCase().contains(ZTEC2016); + } + + private static boolean isPhone(String[] boards) { + final String board = Build.BOARD; + if (board == null) { + return false; + } + for (String board1 : boards) { + if (board.equals(board1)) { + return true; + } + } + return false; + } + + /** + * 判断悬浮窗权限(目前主要用户魅族与小米的检测)。 + */ + public static boolean isFloatWindowOpAllowed(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return checkOp(context, 24); // 24 是AppOpsManager.OP_SYSTEM_ALERT_WINDOW 的值,该值无法直接访问 + } else { + try { + return (context.getApplicationInfo().flags & 1 << 27) == 1 << 27; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + } + + @TargetApi(19) + private static boolean checkOp(Context context, int op) { + final int version = Build.VERSION.SDK_INT; + if (version >= Build.VERSION_CODES.KITKAT) { + AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + try { + Method method = manager.getClass().getDeclaredMethod("checkOp", int.class, int.class, String.class); + int property = (Integer) method.invoke(manager, op, + Binder.getCallingUid(), context.getPackageName()); + return AppOpsManager.MODE_ALLOWED == property; + } catch (Exception e) { + e.printStackTrace(); + } + } + return false; + } + + @Nullable + private static String getLowerCaseName(Properties p, Method get, String key) { + String name = p.getProperty(key); + if (name == null) { + try { + name = (String) get.invoke(null, key); + } catch (Exception ignored) { + } + } + if (name != null) { + name = name.toLowerCase(); + } + return name; + } + + +} diff --git a/app/src/main/java/com/project/survey/util/DrawableUtils.java b/app/src/main/java/com/project/survey/util/DrawableUtils.java new file mode 100644 index 0000000..e8512bd --- /dev/null +++ b/app/src/main/java/com/project/survey/util/DrawableUtils.java @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2019 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.project.survey.util; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LightingColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.StateListDrawable; +import android.view.View; +import android.webkit.WebView; +import android.widget.ImageView; +import android.widget.ScrollView; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.FloatRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.widget.NestedScrollView; + +import java.security.InvalidParameterException; + +/** + * Drawable工具类 + * + * @author xuexiang + * @since 2019/1/3 下午3:47 + */ +public final class DrawableUtils { + + private DrawableUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + private static final String TAG = DrawableUtils.class.getSimpleName(); + + /** + * 节省每次创建时产生的开销,但要注意多线程操作synchronized + */ + private static final Canvas CANVAS = new Canvas(); + + /** + * 从一个view创建Bitmap。 + * 注意点:绘制之前要清掉 View 的焦点,因为焦点可能会改变一个 View 的 UI 状态。 + * 来源:https://github.com/tyrantgit/ExplosionField + * + * @param view 传入一个 View,会获取这个 View 的内容创建 Bitmap。 + * @param scale 缩放比例,对创建的 Bitmap 进行缩放,数值支持从 0 到 1。 + */ + public static Bitmap createBitmapFromView(View view, float scale) { + if (view instanceof ImageView) { + Drawable drawable = ((ImageView) view).getDrawable(); + if (drawable != null && drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + } + view.clearFocus(); + int viewHeight = 0; + if (view instanceof ScrollView) { + for (int i = 0; i < ((ScrollView) view).getChildCount(); i++) { + viewHeight += ((ScrollView) view).getChildAt(i).getHeight(); + } + } else if (view instanceof NestedScrollView) { + for (int i = 0; i < ((NestedScrollView) view).getChildCount(); i++) { + viewHeight += ((NestedScrollView) view).getChildAt(i).getHeight(); + } + } else { + viewHeight = view.getHeight(); + } + + Bitmap bitmap = createBitmapSafely((int) (view.getWidth() * scale), + (int) (viewHeight * scale), Bitmap.Config.ARGB_8888, 1); + if (bitmap != null) { + synchronized (CANVAS) { + Canvas canvas = CANVAS; + canvas.setBitmap(bitmap); + canvas.save(); + // 防止 View 上面有些区域空白导致最终 Bitmap 上有些区域变黑 + canvas.drawColor(Color.WHITE); + canvas.scale(scale, scale); + view.draw(canvas); + canvas.restore(); + canvas.setBitmap(null); + } + } + return bitmap; + } + + + public static Bitmap createBitmapFromWebView(WebView view) { + return createBitmapFromWebView(view, 1f); + } + + public static Bitmap createBitmapFromWebView(WebView view, float scale) { + view.clearFocus(); + int viewHeight = (int) (view.getContentHeight() * view.getScale()); + Bitmap bitmap = createBitmapSafely((int) (view.getWidth() * scale), (int) (viewHeight * scale), Bitmap.Config.ARGB_8888, 1); + + int unitHeight = view.getHeight(); + int bottom = viewHeight; + + if (bitmap != null) { + synchronized (CANVAS) { + Canvas canvas = CANVAS; + canvas.setBitmap(bitmap); + // 防止 View 上面有些区域空白导致最终 Bitmap 上有些区域变黑 + canvas.drawColor(Color.WHITE); + canvas.scale(scale, scale); + while (bottom > 0) { + if (bottom < unitHeight) { + bottom = 0; + } else { + bottom -= unitHeight; + } + canvas.save(); + canvas.clipRect(0, bottom, canvas.getWidth(), bottom + unitHeight); + view.scrollTo(0, bottom); + view.draw(canvas); + canvas.restore(); + } + canvas.setBitmap(null); + } + } + return bitmap; + } + + + public static Bitmap createBitmapFromView(View view) { + return createBitmapFromView(view, 1f); + } + + /** + * 从一个view创建Bitmap。把view的区域截掉leftCrop/topCrop/rightCrop/bottomCrop + */ + public static Bitmap createBitmapFromView(View view, int leftCrop, int topCrop, int rightCrop, int bottomCrop) { + Bitmap originBitmap = DrawableUtils.createBitmapFromView(view); + if (originBitmap == null) { + return null; + } + Bitmap cutBitmap = createBitmapSafely(view.getWidth() - rightCrop - leftCrop, view.getHeight() - topCrop - bottomCrop, Bitmap.Config.ARGB_8888, 1); + if (cutBitmap == null) { + return null; + } + Canvas canvas = new Canvas(cutBitmap); + Rect src = new Rect(leftCrop, topCrop, view.getWidth() - rightCrop, view.getHeight() - bottomCrop); + Rect dest = new Rect(0, 0, view.getWidth() - rightCrop - leftCrop, view.getHeight() - topCrop - bottomCrop); + // 防止 View 上面有些区域空白导致最终 Bitmap 上有些区域变黑 + canvas.drawColor(Color.WHITE); + canvas.drawBitmap(originBitmap, src, dest, null); + originBitmap.recycle(); + return cutBitmap; + } + + /** + * 安全的创建bitmap。 + * 如果新建 Bitmap 时产生了 OOM,可以主动进行一次 GC - System.gc(),然后再次尝试创建。 + * + * @param width Bitmap 宽度。 + * @param height Bitmap 高度。 + * @param config 传入一个 Bitmap.Config。 + * @param retryCount 创建 Bitmap 时产生 OOM 后,主动重试的次数。 + * @return 返回创建的 Bitmap。 + */ + public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) { + //width and height must be > 0 + if (width <= 0 || height <= 0) { + return null; + } + try { + return Bitmap.createBitmap(width, height, config); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + if (retryCount > 0) { + System.gc(); + return createBitmapSafely(width, height, config, retryCount - 1); + } + return null; + } + } + + + /** + * 安全的创建bitmap。 + * 如果新建 Bitmap 时产生了 OOM,可以主动进行一次 GC - System.gc(),然后再次尝试创建。 + * + * @param source 原图片 + * @param x 源中第一个像素的x坐标 + * @param y 源中第一个像素的y坐标 + * @param width 一行像素点的数量 + * @param height 行数 + * @param retryCount 创建 Bitmap 时产生 OOM 后,主动重试的次数。 + * @return 返回创建的 Bitmap。 + */ + public static Bitmap createBitmapSafely(@NonNull Bitmap source, int x, int y, int width, int height, int retryCount) { + if (x < 0 || y < 0 || width <= 0 || height <= 0) { + return null; + } + try { + return Bitmap.createBitmap(source, x, y, width, height); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return null; + } catch (OutOfMemoryError e) { + e.printStackTrace(); + if (retryCount > 0) { + System.gc(); + return createBitmapSafely(source, x, y, width, height, retryCount - 1); + } + return null; + } + } + + /** + * 创建一张指定大小的纯色图片,支持圆角 + * + * @param resources Resources对象,用于创建BitmapDrawable + * @param width 图片的宽度 + * @param height 图片的高度 + * @param cornerRadius 图片的圆角,不需要则传0 + * @param filledColor 图片的填充色 + * @return 指定大小的纯色图片 + */ + public static BitmapDrawable createDrawableWithSize(Resources resources, int width, int height, int cornerRadius, @ColorInt int filledColor) { + Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + if (filledColor == 0) { + filledColor = Color.TRANSPARENT; + } + if (cornerRadius > 0) { + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.FILL); + paint.setColor(filledColor); + canvas.drawRoundRect(new RectF(0, 0, width, height), cornerRadius, cornerRadius, paint); + } else { + canvas.drawColor(filledColor); + } + return new BitmapDrawable(resources, output); + } + + /** + * 设置Drawable的颜色 + * 这里不对Drawable进行mutate(),会影响到所有用到这个Drawable的地方,如果要避免,请先自行mutate() + */ + public static ColorFilter setDrawableTintColor(Drawable drawable, @ColorInt int tintColor) { + LightingColorFilter colorFilter = new LightingColorFilter(Color.argb(255, 0, 0, 0), tintColor); + if (drawable != null) { + drawable.setColorFilter(colorFilter); + } + return colorFilter; + } + + /** + * 创建一张渐变图片,支持韵脚。 + * + * @param startColor 渐变开始色 + * @param endColor 渐变结束色 + * @param radius 圆角大小 + * @param centerX 渐变中心点 X 轴坐标 + * @param centerY 渐变中心点 Y 轴坐标 + * @return 返回所创建的渐变图片。 + */ + public static GradientDrawable createCircleGradientDrawable(@ColorInt int startColor, + @ColorInt int endColor, int radius, + @FloatRange(from = 0f, to = 1f) float centerX, + @FloatRange(from = 0f, to = 1f) float centerY) { + GradientDrawable gradientDrawable = new GradientDrawable(); + gradientDrawable.setColors(new int[]{ + startColor, + endColor + }); + gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT); + gradientDrawable.setGradientRadius(radius); + gradientDrawable.setGradientCenter(centerX, centerY); + return gradientDrawable; + } + + + /** + * 动态创建带上分隔线或下分隔线的Drawable。 + * + * @param separatorColor 分割线颜色。 + * @param bgColor Drawable 的背景色。 + * @param top true 则分割线为上分割线,false 则为下分割线。 + * @return 返回所创建的 Drawable。 + */ + public static LayerDrawable createItemSeparatorBg(@ColorInt int separatorColor, @ColorInt int bgColor, int separatorHeight, boolean top) { + + ShapeDrawable separator = new ShapeDrawable(); + separator.getPaint().setStyle(Paint.Style.FILL); + separator.getPaint().setColor(separatorColor); + + ShapeDrawable bg = new ShapeDrawable(); + bg.getPaint().setStyle(Paint.Style.FILL); + bg.getPaint().setColor(bgColor); + + Drawable[] layers = {separator, bg}; + LayerDrawable layerDrawable = new LayerDrawable(layers); + + layerDrawable.setLayerInset(1, 0, top ? separatorHeight : 0, 0, top ? 0 : separatorHeight); + return layerDrawable; + } + + + /** + * 创建一张指定大小的圆形图片,并附带文字 + * + * @param resources Resources对象,用于创建BitmapDrawable + * @param size 图片的宽度 + * @param filledColor 图片的填充色 + * @param text 文字 + * @param textSize 文字大小(px) + * @param textColor 文字颜色 + * @return 指定大小的纯色图片 + */ + public static BitmapDrawable createCircleDrawableWithText(Resources resources, int size, @ColorInt int filledColor, String text, float textSize, @ColorInt int textColor) { + if (size <= 0) { + throw new InvalidParameterException("bitmap size must be > 0!"); + } + if (textSize <= 0) { + throw new InvalidParameterException("text size must be > 0!"); + } + Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + canvas.drawColor(Color.TRANSPARENT); + // 画圆 + int radius = size / 2; + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.FILL); + paint.setColor(filledColor); + canvas.drawCircle(radius, radius, radius, paint); + // 画文字 + paint.setColor(textColor); + paint.setTextSize(textSize); + paint.setTextAlign(Paint.Align.CENTER); + float baseline = radius + getBaselineDistance(paint); + canvas.drawText(text, radius, baseline, paint); + return new BitmapDrawable(resources, output); + } + + /** + * 获取画笔的基线距离 + * + * @param paint 画笔 + * @return 基线距离 + */ + public static float getBaselineDistance(@NonNull Paint paint) { + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + return (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom; + } + + /////////////// StateListDrawable ///////////////////// + + /** + * 实体 得到随机色 状态选择器 + * + * @param cornerRadius 圆角半径 + * @return 状态选择器 + */ + public static StateListDrawable getDrawable(int cornerRadius) { + return getDrawable(cornerRadius, ColorUtils.getRandomColor()); + } + + /** + * 实体 按下的颜色加深 + * + * @param cornerRadius 圆角半径 + * @param normalColor 正常的颜色 + * @return 状态选择器 + */ + + public static StateListDrawable getDrawable(int cornerRadius, int normalColor) { + return getDrawable(cornerRadius, ColorUtils.darker(normalColor, 0.8F), normalColor); + } + + /** + * 实体 状态选择器 + * + * @param cornerRadius 圆角半径 + * @param pressedColor 按下颜色 + * @param normalColor 正常的颜色 + * @return 状态选择器 + */ + public static StateListDrawable getDrawable(int cornerRadius, int pressedColor, int normalColor) { + return getStateListDrawable(getSolidRectDrawable(cornerRadius, pressedColor), getSolidRectDrawable(cornerRadius, normalColor)); + } + + /** + * 背景选择器 + * + * @param pressedDrawable 按下状态的Drawable + * @param normalDrawable 正常状态的Drawable + * @return 状态选择器 + */ + public static StateListDrawable getStateListDrawable(Drawable pressedDrawable, Drawable normalDrawable) { + StateListDrawable stateListDrawable = new StateListDrawable(); + stateListDrawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, pressedDrawable); + stateListDrawable.addState(new int[]{android.R.attr.state_enabled}, normalDrawable); + //设置不能用的状态 + //默认其他状态背景 + GradientDrawable gray = getSolidRectDrawable(10, Color.GRAY); + stateListDrawable.addState(new int[]{}, gray); + return stateListDrawable; + } + + /** + * 得到实心的drawable, 一般作为选中,点中的效果 + * + * @param cornerRadius 圆角半径 + * @param solidColor 实心颜色 + * @return 得到实心效果 + */ + public static GradientDrawable getSolidRectDrawable(int cornerRadius, int solidColor) { + GradientDrawable gradientDrawable = new GradientDrawable(); + // 设置矩形的圆角半径 + gradientDrawable.setCornerRadius(cornerRadius); + // 设置绘画图片色值 + gradientDrawable.setColor(solidColor); + // 绘画的是矩形 + gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT); + return gradientDrawable; + } + + /** + * 给drawable上色 + * + * @param drawable 图像 + * @param tint 颜色 + * @return drawable + */ + public static Drawable setTint(final Drawable drawable, @ColorInt int tint) { + if (drawable != null) { + DrawableCompat.setTint(drawable, tint); + } + return drawable; + } + + /** + * 给drawable上色 + * + * @param drawable 图像 + * @param tint 颜色 + * @return drawable + */ + public static Drawable setTintList(final Drawable drawable, @Nullable ColorStateList tint) { + if (drawable != null) { + DrawableCompat.setTintList(drawable, tint); + } + return drawable; + } + + /////////////// VectorDrawable ///////////////////// + + @Nullable + public static Drawable getVectorDrawable(Context context, @DrawableRes int resVector) { + try { + return AppCompatResources.getDrawable(context, resVector); + } catch (Exception e) { + return null; + } + } + + public static Bitmap vectorDrawableToBitmap(Context context, @DrawableRes int resVector) { + Drawable drawable = getVectorDrawable(context, resVector); + if (drawable != null) { + return drawable2Bitmap(drawable); + } + return null; + } + + /////////////// VectorDrawable ///////////////////// + + /** + * 获取支持RTL布局的drawable【如果是RTL布局就旋转180度】 + * + * @param src 原drawable + * @return + */ + public static Drawable getSupportRTLDrawable(@NonNull Context context, Drawable src) { + return getSupportRTLDrawable(context, src, false); + } + + /** + * 获取支持RTL布局的drawable【如果是RTL布局就旋转180度】 + * + * @param context + * @param src 原drawable + * @return + */ + public static Drawable getSupportRTLDrawable(@NonNull Context context, Drawable src, boolean recycle) { + if (ResUtils.isRtl(context)) { + return rotate(src, 180, 0, 0, recycle); + } + return src; + } + + /** + * Return the rotated drawable. + * + * @param src The source of drawable. + * @param degrees The number of degrees. + * @param px The x coordinate of the pivot point. + * @param py The y coordinate of the pivot point. + * @param recycle True to recycle the source of drawable, false otherwise. + * @return the rotated drawable + */ + public static Drawable rotate(final Drawable src, + final int degrees, + final float px, + final float py, + final boolean recycle) { + return bitmap2Drawable(rotate(drawable2Bitmap(src), degrees, px, py, recycle)); + } + + /** + * Return the rotated bitmap. + * + * @param src The source of bitmap. + * @param degrees The number of degrees. + * @param px The x coordinate of the pivot point. + * @param py The y coordinate of the pivot point. + * @param recycle True to recycle the source of bitmap, false otherwise. + * @return the rotated bitmap + */ + public static Bitmap rotate(final Bitmap src, + final int degrees, + final float px, + final float py, + final boolean recycle) { + if (isEmptyBitmap(src)) { + return null; + } + if (src.isRecycled()) { + return null; + } + if (degrees == 0) { + return src; + } + Matrix matrix = new Matrix(); + matrix.setRotate(degrees, px, py); + Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); + if (recycle && !src.isRecycled()) { + src.recycle(); + } + return ret; + } + + private static boolean isEmptyBitmap(final Bitmap src) { + return src == null || src.getWidth() == 0 || src.getHeight() == 0; + } + + /** + * 获取图片 + * + * @param context 上下文 + * @param resId 图片资源 + * @return 图片 + */ + public static Bitmap getBitmapByDrawableId(Context context, @DrawableRes int resId) { + return drawable2Bitmap(ResUtils.getDrawable(context, resId)); + } + + /** + * Drawable to bitmap. + * + * @param drawable The drawable. + * @return bitmap + */ + public static Bitmap drawable2Bitmap(final Drawable drawable) { + if (drawable == null) { + return null; + } + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null) { + return bitmapDrawable.getBitmap(); + } + } + Bitmap bitmap; + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + bitmap = Bitmap.createBitmap(1, 1, + drawable.getOpacity() != PixelFormat.OPAQUE + ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565); + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + drawable.getOpacity() != PixelFormat.OPAQUE + ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565); + } + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** + * Bitmap to drawable. + * + * @param bitmap The bitmap. + * @return drawable + */ + public static Drawable bitmap2Drawable(final Bitmap bitmap) { + return bitmap == null ? null : new BitmapDrawable(ResUtils.getResources(), bitmap); + } +} diff --git a/app/src/main/java/com/project/survey/util/KeyboardUtils.java b/app/src/main/java/com/project/survey/util/KeyboardUtils.java new file mode 100644 index 0000000..c7de15c --- /dev/null +++ b/app/src/main/java/com/project/survey/util/KeyboardUtils.java @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.project.survey.util; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Rect; +import android.os.Build; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import androidx.annotation.NonNull; + +import com.project.survey.App; + +import java.lang.reflect.Field; +import java.util.HashMap; + +/** + * 软键盘工具 + * + * @author xuexiang + * @since 2019/1/14 下午10:04 + */ +public class KeyboardUtils implements ViewTreeObserver.OnGlobalLayoutListener { + + private SoftKeyboardToggleListener mCallback; + private ViewGroup mRootView; + private Boolean prevValue = null; + private static HashMap sListenerMap = new HashMap<>(); + + public interface SoftKeyboardToggleListener { + /** + * 键盘显示状态监听回调 + * + * @param isVisible 键盘是否显示 + */ + void onToggleSoftKeyboard(boolean isVisible); + } + + @Override + public void onGlobalLayout() { + boolean isVisible = isSoftInputShow(mRootView); + if (mCallback != null && (prevValue == null || isVisible != prevValue)) { + prevValue = isVisible; + mCallback.onToggleSoftKeyboard(isVisible); + } + } + + /** + * Add a new keyboard listener + * + * @param act calling activity + * @param listener callback + */ + public static void addKeyboardToggleListener(Activity act, SoftKeyboardToggleListener listener) { + removeKeyboardToggleListener(listener); + sListenerMap.put(listener, new KeyboardUtils(act, listener)); + } + + /** + * Add a new keyboard listener + * + * @param act calling activity + * @param listener callback + */ + public static void addKeyboardToggleListener(ViewGroup act, SoftKeyboardToggleListener listener) { + removeKeyboardToggleListener(listener); + sListenerMap.put(listener, new KeyboardUtils(act, listener)); + } + + /** + * Remove a registered listener + * + * @param listener {@link SoftKeyboardToggleListener} + */ + public static void removeKeyboardToggleListener(SoftKeyboardToggleListener listener) { + if (sListenerMap.containsKey(listener)) { + KeyboardUtils k = sListenerMap.get(listener); + if (k != null) { + k.removeListener(); + } + sListenerMap.remove(listener); + } + } + + /** + * Remove all registered keyboard listeners + */ + public static void removeAllKeyboardToggleListeners() { + for (SoftKeyboardToggleListener l : sListenerMap.keySet()) { + KeyboardUtils k = sListenerMap.get(l); + if (k != null) { + k.removeListener(); + } + } + sListenerMap.clear(); + } + + /** + * Manually toggle soft keyboard visibility + * + * @param context calling context + */ + public static void toggleKeyboardVisibility(Context context) { + InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + + /** + * Force closes the soft keyboard + * + * @param activeView the view with the keyboard focus + */ + public static void forceCloseKeyboard(View activeView) { + InputMethodManager inputMethodManager = (InputMethodManager) activeView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(activeView.getWindowToken(), 0); + } + + private void removeListener() { + mCallback = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + mRootView.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + } + + private KeyboardUtils(Activity activity, SoftKeyboardToggleListener listener) { + mCallback = listener; + mRootView = (ViewGroup) activity.getWindow().getDecorView(); + mRootView.getViewTreeObserver().addOnGlobalLayoutListener(this); + } + + private KeyboardUtils(ViewGroup viewGroup, SoftKeyboardToggleListener listener) { + mCallback = listener; + mRootView = viewGroup; + mRootView.getViewTreeObserver().addOnGlobalLayoutListener(this); + } + + /** + * 软键盘以覆盖当前界面的形式出现 + * + * @param activity + */ + public static void setSoftInputAdjustNothing(@NonNull Activity activity) { + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING + | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + + /** + * 软键盘以顶起当前界面的形式出现, 注意这种方式会使得当前布局的高度发生变化,触发当前布局onSizeChanged方法回调,这里前后高度差就是软键盘的高度了 + * + * @param activity + */ + public static void setSoftInputAdjustResize(@NonNull Activity activity) { + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + + /** + * 软键盘以上推当前界面的形式出现, 注意这种方式不会改变布局的高度 + * + * @param activity + */ + public static void setSoftInputAdjustPan(@NonNull Activity activity) { + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + + + /** + * 输入键盘是否在显示 + * + * @param activity 应用窗口 + */ + public static boolean isSoftInputShow(Activity activity) { + return activity != null && isSoftInputShow(activity.getWindow()); + } + + /** + * 输入键盘是否在显示 + * + * @param window 应用窗口 + */ + public static boolean isSoftInputShow(Window window) { + if (window != null && window.getDecorView() instanceof ViewGroup) { + return isSoftInputShow((ViewGroup) window.getDecorView()); + } + return false; + } + + /** + * 输入键盘是否在显示 + * + * @param rootView 根布局 + */ + public static boolean isSoftInputShow(ViewGroup rootView) { + if (rootView == null) { + return false; + } + int viewHeight = rootView.getHeight(); + //获取View可见区域的bottom + Rect rect = new Rect(); + rootView.getWindowVisibleDisplayFrame(rect); + int space = viewHeight - rect.bottom - DensityUtils.getNavigationBarHeight(rootView.getContext()); + return space > 0; + } + + /** + * 禁用物理返回键 + *

+ * 使用方法: + *

需重写 onKeyDown

+ * + * @param keyCode + * @return 是否拦截事件 + *

+ * @Override public boolean onKeyDown(int keyCode, KeyEvent event) { + * return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event); + * } + *

+ */ + public static boolean onDisableBackKeyDown(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_HOME: + return false; + default: + break; + } + return true; + } + + + /** + * 点击屏幕空白区域隐藏软键盘 + *

根据 EditText 所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘

+ *

需重写 dispatchTouchEvent

+ * + * @param ev 点击事件 + * @param activity 窗口 + */ + public static void dispatchTouchEvent(MotionEvent ev, @NonNull Activity activity) { + dispatchTouchEvent(ev, activity.getWindow()); + } + + /** + * 点击屏幕空白区域隐藏软键盘 + *

根据 EditText 所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘

+ *

需重写 dispatchTouchEvent

+ * + * @param ev 点击事件 + * @param dialog 窗口 + */ + public static void dispatchTouchEvent(MotionEvent ev, @NonNull Dialog dialog) { + dispatchTouchEvent(ev, dialog.getWindow()); + } + + /** + * 点击屏幕空白区域隐藏软键盘 + *

根据 EditText 所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘

+ *

需重写 dispatchTouchEvent

+ * + * @param ev 点击事件 + * @param window 窗口 + */ + public static void dispatchTouchEvent(MotionEvent ev, Window window) { + if (ev == null || window == null) { + return; + } + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (isShouldHideKeyboard(window, ev)) { + hideSoftInputClearFocus(window.getCurrentFocus()); + } + } + } + + /** + * 根据 EditText 所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘 + * + * @param view 窗口 + * @param event 用户点击事件 + * @return 是否隐藏键盘 + */ + public static boolean isShouldHideKeyboard(View view, MotionEvent event) { + if ((view instanceof EditText) && event != null) { + return !isTouchView(view, event); + } + return false; + } + + /** + * 根据用户点击的坐标获取用户在窗口上触摸到的View,判断这个View是否是EditText来判断是否隐藏键盘 + * + * @param window 窗口 + * @param event 用户点击事件 + * @return 是否隐藏键盘 + */ + public static boolean isShouldHideKeyboard(Window window, MotionEvent event) { + if (window == null || event == null) { + return false; + } + if (!isSoftInputShow(window)) { + return false; + } + if (!(window.getCurrentFocus() instanceof EditText)) { + return false; + } + View decorView = window.getDecorView(); + if (decorView instanceof ViewGroup) { + return findTouchEditText((ViewGroup) decorView, event) == null; + } + return false; + } + + private static View findTouchEditText(ViewGroup viewGroup, MotionEvent event) { + if (viewGroup == null) { + return null; + } + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View child = viewGroup.getChildAt(i); + if (child == null || !child.isShown()) { + continue; + } + if (!isTouchView(child, event)) { + continue; + } + if (child instanceof EditText) { + return child; + } else if (child instanceof ViewGroup) { + return findTouchEditText((ViewGroup) child, event); + } + } + return null; + } + + /** + * 判断view是否在触摸区域内 + * + * @param view view + * @param event 点击事件 + * @return view是否在触摸区域内 + */ + private static boolean isTouchView(View view, MotionEvent event) { + if (view == null || event == null) { + return false; + } + int[] location = new int[2]; + view.getLocationOnScreen(location); + int left = location[0]; + int top = location[1]; + int right = left + view.getMeasuredWidth(); + int bottom = top + view.getMeasuredHeight(); + return event.getY() >= top && event.getY() <= bottom && event.getX() >= left + && event.getX() <= right; + } + + /** + * 动态隐藏软键盘 + * + * @param view 视图 + */ + public static void hideSoftInput(final View view) { + if (view == null) { + return; + } + InputMethodManager imm = + (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * 动态隐藏弹窗弹出的软键盘【注意:一定要在dialog.dismiss之前调用】 + * + * @param dialog 对话框 + */ + public static void hideSoftInput(@NonNull DialogInterface dialog) { + if (dialog instanceof Dialog) { + hideSoftInput((Dialog) dialog); + } + } + + /** + * 动态隐藏弹窗弹出的软键盘【注意:一定要在dialog.dismiss之前调用】 + * + * @param dialog 对话框 + */ + public static void hideSoftInput(@NonNull Dialog dialog) { + View view = dialog.getCurrentFocus(); + if (view == null && dialog.getWindow() != null) { + view = dialog.getWindow().getDecorView(); + } + hideSoftInput(view); + } + + /** + * 动态隐藏软键盘并且清除当前view的焦点【记住,要在xml的父布局加上android:focusable="true" 和 android:focusableInTouchMode="true"】 + * + * @param view 视图 + */ + public static void hideSoftInputClearFocus(final View view) { + if (view == null) { + return; + } + InputMethodManager imm = + (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + if (view instanceof EditText) { + view.clearFocus(); + } + } + + + /** + * 切换软键盘显示与否状态 + */ + public static void toggleSoftInput() { + InputMethodManager imm = + (InputMethodManager) App.getApp().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + + + /** + * 强制显示软键盘 + * + * @param activity 活动窗口 + */ + public static void showSoftInputForce(Activity activity) { + if (!isSoftInputShow(activity)) { + toggleSoftInput(); + } + } + + + /** + * 显示软键盘 + * + * @param view 可输入控件,并且在焦点上方可显示 + */ + public static void showSoftInput(final EditText view) { + if (view == null) { + return; + } + InputMethodManager imm = + (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + + + /** + * 修复软键盘内存泄漏 + * + * @param context context + */ + public static void fixSoftInputLeaks(final Context context) { + if (context == null) { + return; + } + InputMethodManager imm = + (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + String[] strArr = new String[]{"mCurRootView", "mServedView", "mNextServedView"}; + for (String s : strArr) { + try { + Field declaredField = imm.getClass().getDeclaredField(s); + if (!declaredField.isAccessible()) { + declaredField.setAccessible(true); + } + Object obj = declaredField.get(imm); + if (!(obj instanceof View)) { + continue; + } + View view = (View) obj; + if (view.getContext() == context) { + declaredField.set(imm, null); + } else { + return; + } + } catch (Throwable th) { + th.printStackTrace(); + } + } + } + +} diff --git a/app/src/main/java/com/project/survey/util/ResUtils.java b/app/src/main/java/com/project/survey/util/ResUtils.java new file mode 100644 index 0000000..35deb8a --- /dev/null +++ b/app/src/main/java/com/project/survey/util/ResUtils.java @@ -0,0 +1,447 @@ +package com.project.survey.util; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import androidx.annotation.AnimRes; +import androidx.annotation.ArrayRes; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.annotation.StyleableRes; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; + +import com.project.survey.App; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 获取res中的资源 + * + * @author xuexiang + * @since 2018/12/18 上午12:14 + */ +public final class ResUtils { + + private ResUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 获取resources对象 + * + * @return resources对象 + */ + @Deprecated + public static Resources getResources() { + return App.getApp().getResources(); + } + + /** + * 获取resources对象 + * + * @param context 上下文 + * @return resources对象 + */ + public static Resources getResources(@NonNull Context context) { + return context.getResources(); + } + + /** + * 获取字符串 + * + * @param resId 资源id + * @return 字符串 + */ + @Deprecated + public static String getString(@StringRes int resId) { + return getResources().getString(resId); + } + + /** + * 获取字符串 + * + * @param context 上下文 + * @param resId 资源id + * @return 字符串 + */ + public static String getString(@NonNull Context context, @StringRes int resId) { + return context.getResources().getString(resId); + } + + /** + * 获取资源图片 + * + * @param resId 图片资源id + * @return 资源图片 + */ + @Deprecated + public static Drawable getDrawable(@DrawableRes int resId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return App.getApp().getDrawable(resId); + } + return getResources().getDrawable(resId); + } + + /** + * 获取资源图片【和主题有关】 + * + * @param resId 图片资源id + * @return 资源图片 + */ + public static Drawable getDrawable(Context context, @DrawableRes int resId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return context.getDrawable(resId); + } + return AppCompatResources.getDrawable(context, resId); + } + + /** + * 获取svg资源图片 + * + * @param context 上下文 + * @param resId 图片资源id + * @return svg资源图片 + */ + public static Drawable getVectorDrawable(Context context, @DrawableRes int resId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return context.getDrawable(resId); + } + return AppCompatResources.getDrawable(context, resId); + } + + /** + * 获取Drawable属性(兼容VectorDrawable) + * + * @param context 上下文 + * @param typedArray 样式属性数组 + * @param styleableResId 样式资源ID + * @return Drawable + */ + public static Drawable getDrawableAttrRes(Context context, TypedArray typedArray, @StyleableRes int styleableResId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return typedArray.getDrawable(styleableResId); + } else { + int resourceId = typedArray.getResourceId(styleableResId, -1); + if (resourceId != -1) { + return AppCompatResources.getDrawable(context, resourceId); + } + } + return null; + } + + /** + * 获取ColorStateList属性(兼容?attr属性) + * + * @param context 上下文 + * @param typedArray 样式属性数组 + * @param styleableResId 样式资源ID + * @return ColorStateList + */ + public static ColorStateList getColorStateListAttrRes(Context context, TypedArray typedArray, @StyleableRes int styleableResId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return typedArray.getColorStateList(styleableResId); + } else { + int resourceId = typedArray.getResourceId(styleableResId, -1); + if (resourceId != -1) { + return AppCompatResources.getColorStateList(context, resourceId); + } + } + return null; + } + + /** + * 获取dimes值,返回的是精确的值 + * + * @param resId 资源id + * @return dimes值,返回的是精确的值 + */ + @Deprecated + public static float getDimens(@DimenRes int resId) { + return getResources().getDimension(resId); + } + + /** + * 获取dimes值,返回的是精确的值 + * + * @param context 上下文 + * @param resId 资源id + * @return dimes值,返回的是精确的值 + */ + public static float getDimens(@NonNull Context context, @DimenRes int resId) { + return context.getResources().getDimension(resId); + } + + /** + * 获取Color值 + * + * @param resId 资源id + * @return Color值 + */ + @Deprecated + public static int getColor(@ColorRes int resId) { + return getResources().getColor(resId); + } + + /** + * 获取Color值 + * + * @param context 上下文 + * @param resId 资源id + * @return Color值 + */ + public static int getColor(@NonNull Context context, @ColorRes int resId) { + return ContextCompat.getColor(context, resId); + } + + /** + * 获取ColorStateList值 + * + * @param resId 资源id + * @return ColorStateList值 + */ + @Deprecated + public static ColorStateList getColors(@ColorRes int resId) { + return getResources().getColorStateList(resId); + } + + /** + * 获取ColorStateList值 + * + * @param context 上下文 + * @param resId 资源id + * @return ColorStateList值 + */ + public static ColorStateList getColors(@NonNull Context context, @ColorRes int resId) { + return ContextCompat.getColorStateList(context, resId); + } + + /** + * 获取dimes值,返回的是【去余取整】的值 + * + * @param resId 资源id + * @return dimes值【去余取整】 + */ + @Deprecated + public static int getDimensionPixelOffset(@DimenRes int resId) { + return getResources().getDimensionPixelOffset(resId); + } + + /** + * 获取dimes值,返回的是【去余取整】的值 + * + * @param context 上下文 + * @param resId 资源id + * @return dimes值【去余取整】 + */ + public static int getDimensionPixelOffset(@NonNull Context context, @DimenRes int resId) { + return context.getResources().getDimensionPixelOffset(resId); + } + + /** + * 获取dimes值,返回的是【4舍5入取整】的值 + * + * @param resId 资源id + * @return dimes值【4舍5入取整】 + */ + @Deprecated + public static int getDimensionPixelSize(@DimenRes int resId) { + return getResources().getDimensionPixelSize(resId); + } + + /** + * 获取dimes值,返回的是【4舍5入取整】的值 + * + * @param context 上下文 + * @param resId 资源id + * @return dimes值【4舍5入取整】 + */ + public static int getDimensionPixelSize(@NonNull Context context, @DimenRes int resId) { + return context.getResources().getDimensionPixelSize(resId); + } + + /** + * 获取字符串的数组 + * + * @param resId 资源id + * @return 字符串的数组 + */ + @Deprecated + public static String[] getStringArray(@ArrayRes int resId) { + return getResources().getStringArray(resId); + } + + /** + * 获取字符串的数组 + * + * @param context 上下文 + * @param resId 资源id + * @return 字符串的数组 + */ + public static String[] getStringArray(Context context, @ArrayRes int resId) { + return context.getResources().getStringArray(resId); + } + + /** + * 获取字符串的集合 + * + * @param context 上下文 + * @param resId 资源id + * @return 字符串的集合 + */ + @NonNull + public static List getStringList(@NonNull Context context, int resId) { + return getStringList(context, resId, 0); + } + + /** + * 获取字符串的集合 + * + * @param context 上下文 + * @param resId 资源id + * @param emptyId 空资源id + * @return 字符串的集合 + */ + @NonNull + public static List getStringList(@NonNull Context context, int resId, int emptyId) { + List data = new ArrayList<>(); + if (resId == emptyId) { + return data; + } + String[] array = context.getResources().getStringArray(resId); + if (array.length > 0) { + data.addAll(Arrays.asList(array)); + } + return data; + } + + /** + * 获取Drawable的数组 + * + * @param context context + * @param resId 资源id + * @return Drawable的数组 + */ + public static Drawable[] getDrawableArray(Context context, @ArrayRes int resId) { + TypedArray ta = getResources().obtainTypedArray(resId); + Drawable[] icons = new Drawable[ta.length()]; + for (int i = 0; i < ta.length(); i++) { + int id = ta.getResourceId(i, 0); + if (id != 0) { + icons[i] = ContextCompat.getDrawable(context, id); + } + } + ta.recycle(); + return icons; + } + + /** + * 获取数字的数组 + * + * @param resId 数组资源id + * @return 数字的数组 + */ + @Deprecated + public static int[] getIntArray(@ArrayRes int resId) { + return getResources().getIntArray(resId); + } + + /** + * 获取数字的数组 + * + * @param context 上下文 + * @param resId 数组资源id + * @return 数字的数组 + */ + public static int[] getIntArray(@NonNull Context context, @ArrayRes int resId) { + return context.getResources().getIntArray(resId); + } + + /** + * 获取动画 + * + * @param resId 动画资源id + * @return 动画 + */ + @Deprecated + public static Animation getAnim(@AnimRes int resId) { + return AnimationUtils.loadAnimation(App.getApp(), resId); + } + + /** + * 获取动画 + * + * @param context 上下文 + * @param resId 动画资源id + * @return 动画 + */ + public static Animation getAnim(@NonNull Context context, @AnimRes int resId) { + return AnimationUtils.loadAnimation(context, resId); + } + + + /** + * Check if layout direction is RTL + * + * @return {@code true} if the layout direction is right-to-left + */ + @Deprecated + public static boolean isRtl() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && + getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + + /** + * Check if layout direction is RTL + * + * @param context context + * @return {@code true} if the layout direction is right-to-left + */ + public static boolean isRtl(@NonNull Context context) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && + context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + /** + * Darkens a color by a given factor. + * + * @param color the color to darken + * @param factor The factor to darken the color. + * @return darker version of specified color. + */ + public static int darker(int color, float factor) { + return Color.argb(Color.alpha(color), Math.max((int) (Color.red(color) * factor), 0), + Math.max((int) (Color.green(color) * factor), 0), + Math.max((int) (Color.blue(color) * factor), 0)); + } + + /** + * Lightens a color by a given factor. + * + * @param color The color to lighten + * @param factor The factor to lighten the color. 0 will make the color unchanged. 1 will make the + * color white. + * @return lighter version of the specified color. + */ + public static int lighter(int color, float factor) { + int red = (int) ((Color.red(color) * (1 - factor) / 255 + factor) * 255); + int green = (int) ((Color.green(color) * (1 - factor) / 255 + factor) * 255); + int blue = (int) ((Color.blue(color) * (1 - factor) / 255 + factor) * 255); + return Color.argb(Color.alpha(color), red, green, blue); + } + +} diff --git a/app/src/main/java/com/project/survey/util/StatusBarUtils.java b/app/src/main/java/com/project/survey/util/StatusBarUtils.java new file mode 100644 index 0000000..807a173 --- /dev/null +++ b/app/src/main/java/com/project/survey/util/StatusBarUtils.java @@ -0,0 +1,734 @@ +package com.project.survey.util; + +import static android.os.Build.VERSION_CODES.HONEYCOMB; +import static android.os.Build.VERSION_CODES.KITKAT; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.core.view.ViewCompat; + +import com.project.survey.widget.util.Utils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * 状态栏工具 + * + * @author XUE + * @since 2019/3/22 10:50 + */ +public class StatusBarUtils { + + private final static int STATUSBAR_TYPE_DEFAULT = 0; + private final static int STATUSBAR_TYPE_MIUI = 1; + private final static int STATUSBAR_TYPE_FLYME = 2; + private final static int STATUSBAR_TYPE_ANDROID6 = 3; // Android 6.0 + private final static int STATUS_BAR_DEFAULT_HEIGHT_DP = 25; // 大部分状态栏都是25dp + // 在某些机子上存在不同的density值,所以增加两个虚拟值 + public static float sVirtualDensity = -1; + public static float sVirtualDensityDpi = -1; + private static int sStatusbarHeight = -1; + private static @StatusBarType + int mStatusBarType = STATUSBAR_TYPE_DEFAULT; + private static Integer sTransparentValue; + + public static void translucent(Activity activity) { + translucent(activity.getWindow()); + } + + public static void translucent(Window window) { + translucent(window, 0x40000000); + } + + private static boolean supportTranslucent() { + return Build.VERSION.SDK_INT >= KITKAT + // Essential Phone 在 Android 8 之前沉浸式做得不全,系统不从状态栏顶部开始布局却会下发 WindowInsets + && !(DeviceUtils.isEssentialPhone() && Build.VERSION.SDK_INT < 26); + } + + private StatusBarUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 设置沉浸式状态栏样式 + * + * @param activity + * @param isDark 是否是深色的状态栏 + */ + public static void initStatusBarStyle(Activity activity, boolean isDark) { + initStatusBarStyle(activity, isDark, Color.TRANSPARENT); + } + + /** + * 设置沉浸式状态栏样式 + * + * @param activity + * @param isDark 是否是深色的状态栏 + * @param colorOn5x 颜色 + */ + public static void initStatusBarStyle(Activity activity, boolean isDark, @ColorInt int colorOn5x) { + //设置沉浸式状态栏的颜色 + translucent(activity, colorOn5x); + //修改状态栏的字体颜色 + if (isDark) { + setStatusBarDarkMode(activity); + } else { + setStatusBarLightMode(activity); + } + } + + /** + * 沉浸式状态栏。 + * 支持 4.4 以上版本的 MIUI 和 Flyme,以及 5.0 以上版本的其他 Android。 + * + * @param activity 需要被设置沉浸式状态栏的 Activity。 + */ + public static void translucent(Activity activity, @ColorInt int colorOn5x) { + Window window = activity.getWindow(); + translucent(window, colorOn5x); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void translucent(Window window, @ColorInt int colorOn5x) { + if (!supportTranslucent()) { + // 版本小于4.4,绝对不考虑沉浸式 + return; + } + + if (isNotchOfficialSupport()) { + handleDisplayCutoutMode(window); + } + + // 小米和魅族4.4 以上版本支持沉浸式 + // 小米 Android 6.0 ,开发版 7.7.13 及以后版本设置黑色字体又需要 clear FLAG_TRANSLUCENT_STATUS, 因此还原为官方模式 + if (DeviceUtils.isMeizu() || (DeviceUtils.isMIUI() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M)) { + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && supportTransclentStatusBar6()) { + // android 6以后可以改状态栏字体颜色,因此可以自行设置为透明 + // ZUK Z1是个另类,自家应用可以实现字体颜色变色,但没开放接口 + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(Color.TRANSPARENT); + } else { + // android 5不能修改状态栏字体颜色,因此直接用FLAG_TRANSLUCENT_STATUS,nexus表现为半透明 + // 魅族和小米的表现如何? + // update: 部分手机运用FLAG_TRANSLUCENT_STATUS时背景不是半透明而是没有背景了。。。。。 +// window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + // 采取setStatusBarColor的方式,部分机型不支持,那就纯黑了,保证状态栏图标可见 + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(colorOn5x); + } +// } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// // android4.4的默认是从上到下黑到透明,我们的背景是白色,很难看,因此只做魅族和小米的 +// } else if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1){ +// // 如果app 为白色,需要更改状态栏颜色,因此不能让19一下支持透明状态栏 +// Window window = activity.getWindow(); +// Integer transparentValue = getStatusBarAPITransparentValue(activity); +// if(transparentValue != null) { +// window.getDecorView().setSystemUiVisibility(transparentValue); +// } + } + } + + public static boolean isNotchOfficialSupport() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + } + + @TargetApi(Build.VERSION_CODES.P) + private static void handleDisplayCutoutMode(final Window window) { + View decorView = window.getDecorView(); + if (decorView != null) { + if (ViewCompat.isAttachedToWindow(decorView)) { + realHandleDisplayCutoutMode(window, decorView); + } else { + decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + v.removeOnAttachStateChangeListener(this); + realHandleDisplayCutoutMode(window, v); + } + + @Override + public void onViewDetachedFromWindow(View v) { + + } + }); + } + } + } + + @TargetApi(Build.VERSION_CODES.P) + private static void realHandleDisplayCutoutMode(Window window, View decorView) { + if (decorView.getRootWindowInsets() != null && + decorView.getRootWindowInsets().getDisplayCutout() != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + window.setAttributes(params); + } + } + + /** + * 设置状态栏黑色字体图标, + * 支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android + * + * @param activity 需要被处理的 Activity + */ + public static boolean setStatusBarLightMode(Activity activity) { + if (activity == null) { + return false; + } + // 无语系列:ZTK C2016只能时间和电池图标变色。。。。 + if (DeviceUtils.isZTKC2016()) { + return false; + } + + if (mStatusBarType != STATUSBAR_TYPE_DEFAULT) { + return setStatusBarLightMode(activity, mStatusBarType); + } + if (Build.VERSION.SDK_INT >= KITKAT) { + if (isMIUICustomStatusBarLightModeImpl() && MIUISetStatusBarLightMode(activity.getWindow(), true)) { + mStatusBarType = STATUSBAR_TYPE_MIUI; + return true; + } else if (FlymeSetStatusBarLightMode(activity.getWindow(), true)) { + mStatusBarType = STATUSBAR_TYPE_FLYME; + return true; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Android6SetStatusBarLightMode(activity.getWindow(), true); + mStatusBarType = STATUSBAR_TYPE_ANDROID6; + return true; + } + } + return false; + } + + /** + * 已知系统类型时,设置状态栏黑色字体图标。 + * 支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android + * + * @param activity 需要被处理的 Activity + * @param type StatusBar 类型,对应不同的系统 + */ + private static boolean setStatusBarLightMode(Activity activity, @StatusBarType int type) { + if (type == STATUSBAR_TYPE_MIUI) { + return MIUISetStatusBarLightMode(activity.getWindow(), true); + } else if (type == STATUSBAR_TYPE_FLYME) { + return FlymeSetStatusBarLightMode(activity.getWindow(), true); + } else if (type == STATUSBAR_TYPE_ANDROID6) { + return Android6SetStatusBarLightMode(activity.getWindow(), true); + } + return false; + } + + + /** + * 设置状态栏白色字体图标 + * 支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android + */ + public static boolean setStatusBarDarkMode(Activity activity) { + if (activity == null) { + return false; + } + if (mStatusBarType == STATUSBAR_TYPE_DEFAULT) { + // 默认状态,不需要处理 + return true; + } + + if (mStatusBarType == STATUSBAR_TYPE_MIUI) { + return MIUISetStatusBarLightMode(activity.getWindow(), false); + } else if (mStatusBarType == STATUSBAR_TYPE_FLYME) { + return FlymeSetStatusBarLightMode(activity.getWindow(), false); + } else if (mStatusBarType == STATUSBAR_TYPE_ANDROID6) { + return Android6SetStatusBarLightMode(activity.getWindow(), false); + } + return true; + } + + @TargetApi(Build.VERSION_CODES.M) + private static int changeStatusBarModeRetainFlag(Window window, int out) { + out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_FULLSCREEN); + out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + out = retainSystemUiFlag(window, out, View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + return out; + } + + public static int retainSystemUiFlag(Window window, int out, int type) { + int now = window.getDecorView().getSystemUiVisibility(); + if ((now & type) == type) { + out |= type; + } + return out; + } + + + /** + * 设置状态栏字体图标为深色,Android 6 + * + * @param window 需要设置的窗口 + * @param light 是否把状态栏字体及图标颜色设置为深色 + * @return boolean 成功执行返回true + */ + @TargetApi(Build.VERSION_CODES.M) + private static boolean Android6SetStatusBarLightMode(Window window, boolean light) { + View decorView = window.getDecorView(); + int systemUi = light ? View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR : View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + systemUi = changeStatusBarModeRetainFlag(window, systemUi); + decorView.setSystemUiVisibility(systemUi); + if (DeviceUtils.isMIUIV9()) { + // MIUI 9 低于 6.0 版本依旧只能回退到以前的方案 + // https://github.com/Tencent/QMUI_Android/issues/160 + MIUISetStatusBarLightMode(window, light); + } + return true; + } + + /** + * 设置状态栏字体图标为深色,需要 MIUIV6 以上 + * + * @param window 需要设置的窗口 + * @param light 是否把状态栏字体及图标颜色设置为深色 + * @return boolean 成功执行返回 true + */ + @SuppressWarnings("unchecked") + public static boolean MIUISetStatusBarLightMode(Window window, boolean light) { + boolean result = false; + if (window != null) { + Class clazz = window.getClass(); + try { + int darkModeFlag; + Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); + Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); + darkModeFlag = field.getInt(layoutParams); + Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); + if (light) { + extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体 + } else { + extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体 + } + result = true; + } catch (Exception ignored) { + + } + } + return result; + } + + /** + * 更改状态栏图标、文字颜色的方案是否是MIUI自家的, MIUI9 && Android 6 之后用回Android原生实现 + * 见小米开发文档说明:https://dev.mi.com/console/doc/detail?pId=1159 + */ + private static boolean isMIUICustomStatusBarLightModeImpl() { + if (DeviceUtils.isMIUIV9() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + return DeviceUtils.isMIUIV5() || DeviceUtils.isMIUIV6() || + DeviceUtils.isMIUIV7() || DeviceUtils.isMIUIV8(); + } + + /** + * 设置状态栏图标为深色和魅族特定的文字风格 + * 可以用来判断是否为 Flyme 用户 + * + * @param window 需要设置的窗口 + * @param light 是否把状态栏字体及图标颜色设置为深色 + * @return boolean 成功执行返回true + */ + public static boolean FlymeSetStatusBarLightMode(Window window, boolean light) { + boolean result = false; + if (window != null) { + // flyme 在 6.2.0.0A 支持了 Android 官方的实现方案,旧的方案失效 + Android6SetStatusBarLightMode(window, light); + + try { + WindowManager.LayoutParams lp = window.getAttributes(); + Field darkFlag = WindowManager.LayoutParams.class + .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON"); + Field meizuFlags = WindowManager.LayoutParams.class + .getDeclaredField("meizuFlags"); + darkFlag.setAccessible(true); + meizuFlags.setAccessible(true); + int bit = darkFlag.getInt(null); + int value = meizuFlags.getInt(lp); + if (light) { + value |= bit; + } else { + value &= ~bit; + } + meizuFlags.setInt(lp, value); + window.setAttributes(lp); + result = true; + } catch (Exception ignored) { + + } + } + return result; + } + + /** + * 获取是否全屏 + * + * @return 是否全屏 + */ + public static boolean isFullScreen(Activity activity) { + boolean ret = false; + try { + WindowManager.LayoutParams attrs = activity.getWindow().getAttributes(); + ret = (attrs.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0; + } catch (Exception e) { + e.printStackTrace(); + } + return ret; + } + + /** + * API19之前透明状态栏:获取设置透明状态栏的system ui visibility的值,这是部分有提供接口的rom使用的 + * http://stackoverflow.com/questions/21865621/transparent-status-bar-before-4-4-kitkat + */ + public static Integer getStatusBarAPITransparentValue(Context context) { + if (sTransparentValue != null) { + return sTransparentValue; + } + String[] systemSharedLibraryNames = context.getPackageManager() + .getSystemSharedLibraryNames(); + String fieldName = null; + for (String lib : systemSharedLibraryNames) { + if ("touchwiz".equals(lib)) { + fieldName = "SYSTEM_UI_FLAG_TRANSPARENT_BACKGROUND"; + } else if (lib.startsWith("com.sonyericsson.navigationbar")) { + fieldName = "SYSTEM_UI_FLAG_TRANSPARENT"; + } + } + + if (fieldName != null) { + try { + Field field = View.class.getField(fieldName); + if (field != null) { + Class type = field.getType(); + if (type == int.class) { + sTransparentValue = field.getInt(null); + } + } + } catch (Exception ignored) { + } + } + return sTransparentValue; + } + + /** + * 检测 Android 6.0 是否可以启用 window.setStatusBarColor(Color.TRANSPARENT)。 + */ + public static boolean supportTransclentStatusBar6() { + return !(DeviceUtils.isZUKZ1() || DeviceUtils.isZTKC2016()); + } + + /** + * 获取状态栏的高度。 + * + * @param context 上下文 + * @return 状态栏高度 + */ + public static int getStatusBarHeight(Context context) { + if (sStatusbarHeight == -1) { + sStatusbarHeight = Utils.getStatusBarHeight(context); + } + return sStatusbarHeight; + } + + public static void setVirtualDensity(float density) { + sVirtualDensity = density; + } + + public static void setVirtualDensityDpi(float densityDpi) { + sVirtualDensityDpi = densityDpi; + } + + @IntDef({STATUSBAR_TYPE_DEFAULT, STATUSBAR_TYPE_MIUI, STATUSBAR_TYPE_FLYME, STATUSBAR_TYPE_ANDROID6}) + @Retention(RetentionPolicy.SOURCE) + private @interface StatusBarType { + } + + + /** + * 全屏 + * + * @param activity 窗口 + */ + public static void fullScreen(Activity activity) { + if (activity == null) { + return; + } + fullScreen(activity.getWindow()); + } + + /** + * 全屏 + * + * @param window 窗口 + */ + public static void fullScreen(Window window) { + if (window == null) { + return; + } + if (Build.VERSION.SDK_INT > HONEYCOMB && Build.VERSION.SDK_INT < KITKAT) { // lower api + window.getDecorView().setSystemUiVisibility(View.GONE); + } else if (Build.VERSION.SDK_INT >= KITKAT) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + } + } + + /** + * 取消全屏 + * + * @param activity 窗口 + * @param statusBarColor 状态栏的颜色 + * @param navigationBarColor 导航栏的颜色 + */ + public static void cancelFullScreen(Activity activity, @ColorInt int statusBarColor, @ColorInt int navigationBarColor) { + if (activity == null) { + return; + } + cancelFullScreen(activity.getWindow(), statusBarColor, navigationBarColor); + } + + /** + * 取消全屏 + * + * @param window 窗口 + * @param statusBarColor 状态栏的颜色 + * @param navigationBarColor 导航栏的颜色 + */ + public static void cancelFullScreen(Window window, @ColorInt int statusBarColor, @ColorInt int navigationBarColor) { + if (window == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + if (statusBarColor != -1) { + window.setStatusBarColor(statusBarColor); + } + if (navigationBarColor != -1) { + window.setNavigationBarColor(navigationBarColor); + } + } + } + + /** + * 取消全屏 + * + * @param activity 窗口 + */ + public static void cancelFullScreen(Activity activity) { + if (activity == null) { + return; + } + cancelFullScreen(activity.getWindow()); + } + + /** + * 取消全屏 + * + * @param window 窗口 + */ + public static void cancelFullScreen(Window window) { + cancelFullScreen(window, -1, -1); + } + + /** + * 设置底部导航条的颜色 + * + * @param activity 窗口 + * @param color 颜色 + */ + public static void setNavigationBarColor(Activity activity, int color) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + //5.0以上可以直接设置 navigation颜色 + activity.getWindow().setNavigationBarColor(color); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + View navigationBar = new View(activity); + FrameLayout.LayoutParams params; + params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, getNavigationBarHeight(activity)); + params.gravity = Gravity.BOTTOM; + navigationBar.setLayoutParams(params); + navigationBar.setBackgroundColor(color); + decorView.addView(navigationBar); + } else { + //4.4以下无法设置NavigationBar颜色 + } + + } + + /** + * 获取底部导航条的高度 + * + * @param context 上下文 + * @return 底部导航条的高度 + */ + public static int getNavigationBarHeight(Context context) { + int height = 0; + int id = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (id > 0) { + height = context.getResources().getDimensionPixelSize(id); + } + return height; + } + + /** + * 底部导航条是否显示 + * + * @param activity 窗口 + * @return 底部导航条是否显示 + */ + public static boolean isNavigationBarExist(Activity activity) { + return DensityUtils.isNavigationBarExist(activity); + } + + /** + * 全屏下显示弹窗 + * + * @param dialog 弹窗 + */ + public static void showDialogInFullScreen(final Dialog dialog) { + if (dialog == null) { + return; + } + showWindowInFullScreen(dialog.getWindow(), new IWindowShower() { + @Override + public void show(Window window) { + dialog.show(); + } + }); + } + + /** + * 全屏下显示窗口【包括dialog等】 + * + * @param window 窗口 + * @param iWindowShower 窗口显示接口 + */ + public static void showWindowInFullScreen(Window window, IWindowShower iWindowShower) { + if (window == null || iWindowShower == null) { + return; + } + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + iWindowShower.show(window); + StatusBarUtils.fullScreen(window); + window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + } + + /** + * 显示窗口【同步窗口系统view的可见度, 解决全屏下显示窗口导致界面退出全屏的问题】 + * + * @param activity 活动窗口 + * @param dialog 需要显示的窗口 + */ + public static void showDialog(Activity activity, final Dialog dialog) { + if (dialog == null) { + return; + } + showWindow(activity, dialog.getWindow(), new IWindowShower() { + @Override + public void show(Window window) { + dialog.show(); + } + }); + } + + /** + * 显示窗口【同步窗口系统view的可见度, 解决全屏下显示窗口导致界面退出全屏的问题】 + * + * @param activity 活动窗口 + * @param window 需要显示的窗口 + * @param iWindowShower 窗口显示接口 + * @return 是否执行成功 + */ + public static boolean showWindow(Activity activity, Window window, IWindowShower iWindowShower) { + if (activity == null || window == null || iWindowShower == null) { + return false; + } + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + iWindowShower.show(window); + StatusBarUtils.syncSystemUiVisibility(activity, window); + window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + return true; + } + + /** + * 同步窗口的系统view的可见度【解决全屏下显示窗口导致界面退出全屏的问题】 + * + * @param original 活动窗口 + * @param target 目标窗口 + * @return 是否执行成功 + */ + public static boolean syncSystemUiVisibility(Activity original, Window target) { + if (original == null) { + return false; + } + return syncSystemUiVisibility(original.getWindow(), target); + } + + /** + * 同步两个窗口的系统view的可见度【解决全屏下显示窗口导致界面退出全屏的问题】 + * + * @param original 原始窗口 + * @param target 目标窗口 + * @return 是否执行成功 + */ + public static boolean syncSystemUiVisibility(Window original, Window target) { + if (original == null || target == null) { + return false; + } + target.getDecorView().setSystemUiVisibility(original.getDecorView().getSystemUiVisibility()); + return true; + } + + /** + * 窗口显示接口 + */ + public interface IWindowShower { + /** + * 显示窗口 + * + * @param window 窗口 + */ + void show(Window window); + } + +} diff --git a/app/src/main/java/com/project/survey/util/ThemeUtils.java b/app/src/main/java/com/project/survey/util/ThemeUtils.java new file mode 100644 index 0000000..add43fe --- /dev/null +++ b/app/src/main/java/com/project/survey/util/ThemeUtils.java @@ -0,0 +1,414 @@ +package com.project.survey.util; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.ArrayRes; +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; + + +import com.project.survey.R; +import com.project.survey.widget.edittext.GravityEnum; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * 主题工具 + * + * @author xuexiang + * @since 2018/11/14 下午1:46 + */ +public final class ThemeUtils { + + private ThemeUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + @ColorInt + public static int getDisabledColor(Context context) { + final int primaryColor = resolveColor(context, android.R.attr.textColorPrimary); + final int disabledColor = isColorDark(primaryColor) ? Color.BLACK : Color.WHITE; + return adjustAlpha(disabledColor, 0.3f); + } + + @ColorInt + public static int adjustAlpha( + @ColorInt int color, @SuppressWarnings("SameParameterValue") float factor) { + int alpha = Math.round(Color.alpha(color) * factor); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + return Color.argb(alpha, red, green, blue); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr) { + return resolveColor(context, attr, 0); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getColor(0, fallback); + } finally { + a.recycle(); + } + } + + public static int getColorFromAttrRes(int attrRes, int defaultValue, Context context) { + TypedArray a = context.obtainStyledAttributes(new int[]{attrRes}); + try { + return a.getColor(0, defaultValue); + } finally { + a.recycle(); + } + } + + public static float resolveFloat(Context context, int attrRes) { + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(attrRes, typedValue, true); + return typedValue.getFloat(); + } + + public static int resolveInt(Context context, int attrRes) { + return resolveInt(context, attrRes, 0); + } + + public static int resolveInt(Context context, int attrRes, int defaultValue) { + TypedArray a = context.obtainStyledAttributes(new int[]{attrRes}); + try { + return a.getInt(0, defaultValue); + } finally { + a.recycle(); + } + } + + public static float resolveFloat(Context context, int attrRes, float defaultValue) { + TypedArray a = context.obtainStyledAttributes(new int[]{attrRes}); + try { + return a.getFloat(0, defaultValue); + } finally { + a.recycle(); + } + } + + // Try to resolve the colorAttr attribute. + public static ColorStateList resolveActionTextColorStateList( + Context context, @AttrRes int colorAttr, ColorStateList fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{colorAttr}); + try { + final TypedValue value = a.peekValue(0); + if (value == null) { + return fallback; + } + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + final ColorStateList stateList = a.getColorStateList(0); + if (stateList != null) { + return stateList; + } else { + return fallback; + } + } + } finally { + a.recycle(); + } + } + + // Get the specified color resource, creating a ColorStateList if the resource + // points to a color value. + public static ColorStateList getActionTextColorStateList(Context context, @ColorRes int colorId) { + final TypedValue value = new TypedValue(); + context.getResources().getValue(colorId, value, true); + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + //noinspection deprecation + return context.getResources().getColorStateList(colorId); + } else { + return context.getColorStateList(colorId); + } + } + } + + /** + * Returns a color associated with a particular resource ID + * + *

Starting in {@link Build.VERSION_CODES#M}, the returned color will be styled for + * the specified Context's theme. + * + * @param colorId The desired resource identifier, as generated by the aapt tool. This integer + * encodes the package, type, and resource entry. The value 0 is an invalid identifier. + * @return A single color value in the form 0xAARRGGBB. + */ + @ColorInt + public static int getColor(Context context, @ColorRes int colorId) { + return ContextCompat.getColor(context, colorId); + } + + public static String resolveString(Context context, @AttrRes int attr) { + TypedValue v = new TypedValue(); + context.getTheme().resolveAttribute(attr, v, true); + return (String) v.string; + } + + public static String resolveString(Context context, @AttrRes int attr, String defaultValue) { + TypedValue v = new TypedValue(); + context.getTheme().resolveAttribute(attr, v, true); + String value = (String) v.string; + return TextUtils.isEmpty(value) ? defaultValue : value; + } + + public static String resolveString(Resources.Theme theme, @AttrRes int attr) { + TypedValue v = new TypedValue(); + theme.resolveAttribute(attr, v, true); + return (String) v.string; + } + + + public static Drawable resolveDrawable(Context context, @AttrRes int attr) { + return resolveDrawable(context, attr, null); + } + + public static Drawable resolveDrawable( + Context context, + @AttrRes int attr, + @SuppressWarnings("SameParameterValue") Drawable fallback) { + TypedArray array = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + Drawable drawable = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + drawable = array.getDrawable(0); + } else { + int id = array.getResourceId(0, -1); + if (id != -1) { + drawable = AppCompatResources.getDrawable(context, id); + } + } + if (drawable == null && fallback != null) { + drawable = fallback; + } + return drawable; + } finally { + array.recycle(); + } + } + + public static int resolveDimension(Context context, @AttrRes int attr) { + return resolveDimension(context, attr, -1); + } + + public static int resolveDimension(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getDimensionPixelSize(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr, boolean fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getBoolean(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr) { + return resolveBoolean(context, attr, false); + } + + public static boolean isColorDark(@ColorInt int color) { + double darkness = + 1 + - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) + / 255; + return darkness >= 0.5; + } + + public static void setBackgroundCompat(View view, Drawable d) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + //noinspection deprecation + view.setBackgroundDrawable(d); + } else { + view.setBackground(d); + } + } + + + public static ColorStateList getActionTextStateList(Context context, int newPrimaryColor) { + final int fallBackButtonColor = + ThemeUtils.resolveColor(context, android.R.attr.textColorPrimary); + if (newPrimaryColor == 0) { + newPrimaryColor = fallBackButtonColor; + } + int[][] states = + new int[][]{ + new int[]{-android.R.attr.state_enabled}, // disabled + new int[]{} // enabled + }; + int[] colors = new int[]{ThemeUtils.adjustAlpha(newPrimaryColor, 0.4f), newPrimaryColor}; + return new ColorStateList(states, colors); + } + + public static int[] getColorArray(@NonNull Context context, @ArrayRes int array) { + if (array == 0) { + return null; + } + TypedArray ta = context.getResources().obtainTypedArray(array); + int[] colors = new int[ta.length()]; + for (int i = 0; i < ta.length(); i++) { + colors[i] = ta.getColor(i, 0); + } + ta.recycle(); + return colors; + } + + public static boolean isIn(@NonNull T find, @Nullable T[] ary) { + if (ary == null || ary.length == 0) { + return false; + } + for (T item : ary) { + if (item.equals(find)) { + return true; + } + } + return false; + } + + /** + * 获取主题色 + * + * @param context 上下文 + * @return 主题色 + */ + @ColorInt + public static int getMainThemeColor(Context context) { + return resolveColor(context, R.attr.colorPrimary, getColor(context, R.color.colorPrimary)); + } + + //========================深色模式==============================// + /** + * 系统默认模式 + */ + public static final int DEFAULT_MODE = 0; + /** + * 浅色模式 + */ + public static final int LIGHT_MODE = 1; + /** + * 深色模式 + */ + public static final int DARK_MODE = 2; + + @IntDef({DEFAULT_MODE, LIGHT_MODE, DARK_MODE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Theme { + } + + /** + * 当前是否是处于深色模式 + * + * @return 是否是深色模式 + */ + @Deprecated + public static boolean isNightMode() { + int mode = ResUtils.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + return mode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * 当前是否是处于深色模式 + * + * @param context 上下文 + * @return 是否是深色模式 + */ + public static boolean isNightMode(@NonNull Context context) { + int mode = ResUtils.getResources(context).getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + return mode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * 设置应用的主题(深色模式) + * + * @param theme 主题类型 + */ + @SuppressLint("WrongConstant") + public static void applyTheme(@Theme int theme) { + switch (theme) { + case LIGHT_MODE: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + break; + case DARK_MODE: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + break; + case DEFAULT_MODE: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); + } + break; + default: + break; + } + } + + //=================// + + private static int gravityEnumToAttrInt(GravityEnum value) { + switch (value) { + case CENTER: + return 1; + case END: + return 2; + default: + return 0; + } + } + + public static GravityEnum resolveGravityEnum( + Context context, @AttrRes int attr, GravityEnum defaultGravity) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + switch (a.getInt(0, gravityEnumToAttrInt(defaultGravity))) { + case 1: + return GravityEnum.CENTER; + case 2: + return GravityEnum.END; + default: + return GravityEnum.START; + } + } finally { + a.recycle(); + } + } + +} diff --git a/app/src/main/java/com/project/survey/widget/XUI.java b/app/src/main/java/com/project/survey/widget/XUI.java new file mode 100644 index 0000000..a60dab8 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/XUI.java @@ -0,0 +1,114 @@ +package com.project.survey.widget; + + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; + +import com.project.survey.R; + +import io.github.inflationx.calligraphy3.CalligraphyConfig; +import io.github.inflationx.calligraphy3.CalligraphyInterceptor; +import io.github.inflationx.calligraphy3.TypefaceUtils; +import io.github.inflationx.viewpump.ViewPump; + +/** + * UI全局设置 + * + * @author xuexiang + * @since 2018/11/14 上午11:40 + */ +public class XUI { + + private static Application sContext; + + private static boolean sIsTabletChecked; + + private static int sScreenType; + + private static String sDefaultFontAssetPath; + + //=======================初始化设置===========================// + + /** + * 初始化 + * + * @param context 上下文 + */ + public static void init(Application context) { + sContext = context; + } + + /** + * 设置默认字体 + */ + public static void initFontStyle(String defaultFontAssetPath) { + if (!TextUtils.isEmpty(defaultFontAssetPath)) { + sDefaultFontAssetPath = defaultFontAssetPath; + ViewPump.init(ViewPump.builder() + .addInterceptor(new CalligraphyInterceptor( + new CalligraphyConfig.Builder() + .setDefaultFontPath(defaultFontAssetPath) + .setFontAttrId(R.attr.fontPath) + .build())) + .build()); + } + } + + public static Context getContext() { + testInitialize(); + return sContext; + } + + private static void testInitialize() { + if (sContext == null) { + throw new ExceptionInInitializerError("请先在全局Application中调用 XUI.init() 初始化!"); + } + } + + //=======================日志调试===========================// + + + //=======================字体===========================// + /** + * @return 获取默认字体 + */ + @Nullable + public static Typeface getDefaultTypeface() { + if (!TextUtils.isEmpty(sDefaultFontAssetPath)) { + return TypefaceUtils.load(getContext().getAssets(), sDefaultFontAssetPath); + } + return null; + } + + /** + * @return 默认字体的存储位置 + */ + public static String getDefaultFontAssetPath() { + return sDefaultFontAssetPath; + } + + /** + * @param fontPath 字体路径 + * @return 获取默认字体 + */ + @Nullable + public static Typeface getDefaultTypeface(String fontPath) { + if (TextUtils.isEmpty(fontPath)) { + fontPath = sDefaultFontAssetPath; + } + if (!TextUtils.isEmpty(fontPath)) { + return TypefaceUtils.load(getContext().getAssets(), fontPath); + } + return null; + } + + +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/AsteriskPasswordTransformationMethod.java b/app/src/main/java/com/project/survey/widget/edittext/AsteriskPasswordTransformationMethod.java new file mode 100644 index 0000000..99c5daa --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/AsteriskPasswordTransformationMethod.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.project.survey.widget.edittext; + +import android.text.method.PasswordTransformationMethod; +import android.view.View; + +/** + * ‘****’号密码输入样式 + * + * @author xuexiang + * @since 2019-07-05 9:34 + */ +public class AsteriskPasswordTransformationMethod extends PasswordTransformationMethod { + + private static AsteriskPasswordTransformationMethod sInstance; + + public static PasswordTransformationMethod getInstance() { + if (sInstance != null) { + return sInstance; + } + sInstance = new AsteriskPasswordTransformationMethod(); + return sInstance; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + return new AsteriskPasswordCharSequence(source); + } + + private static class AsteriskPasswordCharSequence implements CharSequence { + private CharSequence mSource; + + AsteriskPasswordCharSequence(CharSequence source) { + mSource = source; // Store char sequence + } + + @Override + public char charAt(int index) { + return '*'; // This is the important part + } + + @Override + public int length() { + return mSource.length(); // Return default + } + + @Override + public CharSequence subSequence(int start, int end) { + return mSource.subSequence(start, end); // Return default + } + } + +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/GravityEnum.java b/app/src/main/java/com/project/survey/widget/edittext/GravityEnum.java new file mode 100644 index 0000000..0bcb2c2 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/GravityEnum.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.project.survey.widget.edittext; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Gravity; +import android.view.View; + +/** + * 对齐方式 + * + * @author xuexiang + * @since 2018/11/14 下午4:44 + */ +public enum GravityEnum { + /** + * 头部对齐 + */ + START, + /** + * 居中对齐 + */ + CENTER, + /** + * 尾部对齐 + */ + END; + + private static final boolean HAS_RTL = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + + @SuppressLint("RtlHardcoded") + public int getGravityInt() { + switch (this) { + case START: + return HAS_RTL ? Gravity.START : Gravity.LEFT; + case CENTER: + return Gravity.CENTER_HORIZONTAL; + case END: + return HAS_RTL ? Gravity.END : Gravity.RIGHT; + default: + throw new IllegalStateException("Invalid gravity constant"); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getTextAlignment() { + switch (this) { + case CENTER: + return View.TEXT_ALIGNMENT_CENTER; + case END: + return View.TEXT_ALIGNMENT_VIEW_END; + default: + return View.TEXT_ALIGNMENT_VIEW_START; + } + } +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/PassEdittext2.java b/app/src/main/java/com/project/survey/widget/edittext/PassEdittext2.java new file mode 100644 index 0000000..24a2ec5 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/PassEdittext2.java @@ -0,0 +1,318 @@ +package com.project.survey.widget.edittext; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatEditText; + +import com.project.survey.R; +import com.project.survey.util.DensityUtils; +import com.project.survey.util.ResUtils; + +public class PassEdittext2 extends androidx.appcompat.widget.AppCompatEditText { + /** + * 增大点击区域 + */ + private int mExtraClickArea; + + private final static int ALPHA_ICON_ENABLED = (int) (255 * 0.54f); + private final static int ALPHA_ICON_DISABLED = (int) (255 * 0.38f); + + private Drawable mShowPwDrawable; + private Drawable mHidePwDrawable; + private boolean mPasswordVisible; + private boolean mShowingIcon; + private boolean mSetErrorCalled; + private boolean mHoverShowsPw; + private boolean mHandlingHoverEvent; + private PasswordTransformationMethod mTransformationMethod; + + public PassEdittext2(Context context) { + this(context, null); + } + + public PassEdittext2(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.PasswordEditTextStyle); + } + + public PassEdittext2(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initAttrs(context, attrs, defStyleAttr); + } + + public void initAttrs(Context context, AttributeSet attrs, int defStyleAttr) { + mExtraClickArea = DensityUtils.dp2px(context, 20); + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PasswordEditText, defStyleAttr, 0); + boolean useNonMonospaceFont; + boolean enableIconAlpha; + try { + mShowPwDrawable = ResUtils.getDrawableAttrRes(getContext(), typedArray, R.styleable.PasswordEditText_pet_iconShow); + if (mShowPwDrawable == null) { + mShowPwDrawable = ResUtils.getVectorDrawable(getContext(), R.drawable.pet_icon_visibility_24dp); + } + mHidePwDrawable = ResUtils.getDrawableAttrRes(getContext(), typedArray, R.styleable.PasswordEditText_pet_iconHide); + if (mHidePwDrawable == null) { + mHidePwDrawable = ResUtils.getVectorDrawable(getContext(), R.drawable.pet_icon_visibility_off_24dp); + } + mHoverShowsPw = typedArray.getBoolean(R.styleable.PasswordEditText_pet_hoverShowsPw, false); + useNonMonospaceFont = typedArray.getBoolean(R.styleable.PasswordEditText_pet_nonMonospaceFont, false); + enableIconAlpha = typedArray.getBoolean(R.styleable.PasswordEditText_pet_enableIconAlpha, true); + boolean isAsteriskStyle = typedArray.getBoolean(R.styleable.PasswordEditText_pet_isAsteriskStyle, false); + if (isAsteriskStyle) { + mTransformationMethod = AsteriskPasswordTransformationMethod.getInstance(); + } else { + mTransformationMethod = PasswordTransformationMethod.getInstance(); + } + } finally { + typedArray.recycle(); + } + + if (enableIconAlpha) { + mHidePwDrawable.setAlpha(ALPHA_ICON_ENABLED); + mShowPwDrawable.setAlpha(ALPHA_ICON_DISABLED); + } + +// if (useNonMonospaceFont) { +// setTypeface(Typeface.DEFAULT); +// } + + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence seq, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() > 0) { + if (mSetErrorCalled) { + setCompoundDrawablesRelative(null, null, null, null); + mSetErrorCalled = false; + showPasswordVisibilityIndicator(true); + } + if (!mShowingIcon) { + showPasswordVisibilityIndicator(true); + } + } else { + // hides the indicator if no text inside text field + mPasswordVisible = false; + handlePasswordInputVisibility(); + showPasswordVisibilityIndicator(false); + } + + } + }); + + handlePasswordInputVisibility(); + } + + public PassEdittext2 setExtraClickAreaSize(int extraClickArea) { + mExtraClickArea = extraClickArea; + return this; + } + + /** + * 设置密码输入框的样式 + * + * @param transformationMethod + * @return + */ + public PassEdittext2 setPasswordTransformationMethod(PasswordTransformationMethod transformationMethod) { + mTransformationMethod = transformationMethod; + return this; + } + + /** + * 设置密码输入框的样式 + * + * @param isAsteriskStyle + * @return + */ + public PassEdittext2 setIsAsteriskStyle(boolean isAsteriskStyle) { + if (isAsteriskStyle) { + mTransformationMethod = AsteriskPasswordTransformationMethod.getInstance(); + } else { + mTransformationMethod = PasswordTransformationMethod.getInstance(); + } + return this; + } + + private boolean isRtl() { + return getLayoutDirection() == LAYOUT_DIRECTION_RTL; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new PassEdittext2.SavedState(superState, mShowingIcon, mPasswordVisible); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + PasswordEditText.SavedState savedState = (PasswordEditText.SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + mShowingIcon = savedState.isShowingIcon(); + mPasswordVisible = savedState.isPasswordVisible(); + handlePasswordInputVisibility(); + showPasswordVisibilityIndicator(mShowingIcon); + } + + @Override + public void setError(CharSequence error) { + super.setError(error); + mSetErrorCalled = true; + + } + + @Override + public void setError(CharSequence error, Drawable icon) { + super.setError(error, icon); + mSetErrorCalled = true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mShowingIcon) { + return super.onTouchEvent(event); + } else { + boolean touchable = isTouchable(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mHoverShowsPw) { + if (touchable) { + togglePasswordIconVisibility(); + // prevent keyboard from coming up + event.setAction(MotionEvent.ACTION_CANCEL); + mHandlingHoverEvent = true; + } + } + break; + case MotionEvent.ACTION_UP: + if (mHandlingHoverEvent || touchable) { + togglePasswordIconVisibility(); + // prevent keyboard from coming up + event.setAction(MotionEvent.ACTION_CANCEL); + mHandlingHoverEvent = false; + } + break; + default: + break; + } + return super.onTouchEvent(event); + } + } + + private boolean isTouchable(MotionEvent event) { + boolean touchable; + if (isRtl()) { + touchable = event.getX() > getPaddingLeft() - mExtraClickArea && event.getX() < getPaddingLeft() + mShowPwDrawable.getIntrinsicWidth() + mExtraClickArea; + } else { + touchable = event.getX() > getWidth() - getPaddingRight() - mShowPwDrawable.getIntrinsicWidth() - mExtraClickArea && event.getX() < getWidth() - getPaddingRight() + mExtraClickArea; + } + return touchable; + } + + + private void showPasswordVisibilityIndicator(boolean shouldShowIcon) { + if (shouldShowIcon) { + Drawable drawable = mPasswordVisible ? mShowPwDrawable : mHidePwDrawable; + mShowingIcon = true; + setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, drawable, null); + } else { + // reset drawable + setCompoundDrawablesRelative(null, null, null, null); + mShowingIcon = false; + } + } + + /** + * This method toggles the visibility of the icon and takes care of switching the input type + * of the view to be able to see the password afterwards. + *

+ * This method may only be called if there is an icon visible + */ + private void togglePasswordIconVisibility() { + mPasswordVisible = !mPasswordVisible; + handlePasswordInputVisibility(); + showPasswordVisibilityIndicator(true); + } + + /** + * This method is called when restoring the state (e.g. on orientation change) + */ + private void handlePasswordInputVisibility() { + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + if (mPasswordVisible) { + setTransformationMethod(null); + } else { + setTransformationMethod(mTransformationMethod); + } + setSelection(selectionStart, selectionEnd); + + } + + /** + * Convenience class to save / restore the state of icon. + */ + protected static class SavedState extends BaseSavedState { + + private final boolean mShowingIcon; + private final boolean mPasswordVisible; + + private SavedState(Parcelable superState, boolean sI, boolean pV) { + super(superState); + mShowingIcon = sI; + mPasswordVisible = pV; + } + + private SavedState(Parcel in) { + super(in); + mShowingIcon = in.readByte() != 0; + mPasswordVisible = in.readByte() != 0; + } + + public boolean isShowingIcon() { + return mShowingIcon; + } + + public boolean isPasswordVisible() { + return mPasswordVisible; + } + + @Override + public void writeToParcel(Parcel destination, int flags) { + super.writeToParcel(destination, flags); + destination.writeByte((byte) (mShowingIcon ? 1 : 0)); + destination.writeByte((byte) (mPasswordVisible ? 1 : 0)); + } + + public static final Creator CREATOR = new Creator() { + + @Override + public PassEdittext2.SavedState createFromParcel(Parcel in) { + return new PassEdittext2.SavedState(in); + } + + @Override + public PassEdittext2.SavedState[] newArray(int size) { + return new PassEdittext2.SavedState[size]; + } + + }; + } +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/PasswordEditText.java b/app/src/main/java/com/project/survey/widget/edittext/PasswordEditText.java new file mode 100644 index 0000000..1e7fc19 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/PasswordEditText.java @@ -0,0 +1,323 @@ +package com.project.survey.widget.edittext; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.appcompat.widget.AppCompatEditText; + +import com.project.survey.R; +import com.project.survey.util.DensityUtils; +import com.project.survey.util.ResUtils; + + +/** + * 支持显示密码的输入框 + * + * @author xuexiang + * @since 2019/1/14 下午10:08 + */ +public class PasswordEditText extends AppCompatEditText { + /** + * 增大点击区域 + */ + private int mExtraClickArea; + + private final static int ALPHA_ICON_ENABLED = (int) (255 * 0.54f); + private final static int ALPHA_ICON_DISABLED = (int) (255 * 0.38f); + + private Drawable mShowPwDrawable; + private Drawable mHidePwDrawable; + private boolean mPasswordVisible; + private boolean mShowingIcon; + private boolean mSetErrorCalled; + private boolean mHoverShowsPw; + private boolean mHandlingHoverEvent; + private PasswordTransformationMethod mTransformationMethod; + + public PasswordEditText(Context context) { + this(context, null); + } + + public PasswordEditText(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.PasswordEditTextStyle); + } + + public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initAttrs(context, attrs, defStyleAttr); + } + + public void initAttrs(Context context, AttributeSet attrs, int defStyleAttr) { + mExtraClickArea = DensityUtils.dp2px(context, 20); + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PasswordEditText, defStyleAttr, 0); + boolean useNonMonospaceFont; + boolean enableIconAlpha; + try { + mShowPwDrawable = ResUtils.getDrawableAttrRes(getContext(), typedArray, R.styleable.PasswordEditText_pet_iconShow); + if (mShowPwDrawable == null) { + mShowPwDrawable = ResUtils.getVectorDrawable(getContext(), R.drawable.pet_icon_visibility_24dp); + } + mHidePwDrawable = ResUtils.getDrawableAttrRes(getContext(), typedArray, R.styleable.PasswordEditText_pet_iconHide); + if (mHidePwDrawable == null) { + mHidePwDrawable = ResUtils.getVectorDrawable(getContext(), R.drawable.pet_icon_visibility_off_24dp); + } + mHoverShowsPw = typedArray.getBoolean(R.styleable.PasswordEditText_pet_hoverShowsPw, false); + useNonMonospaceFont = typedArray.getBoolean(R.styleable.PasswordEditText_pet_nonMonospaceFont, false); + enableIconAlpha = typedArray.getBoolean(R.styleable.PasswordEditText_pet_enableIconAlpha, true); + boolean isAsteriskStyle = typedArray.getBoolean(R.styleable.PasswordEditText_pet_isAsteriskStyle, false); + if (isAsteriskStyle) { + mTransformationMethod = AsteriskPasswordTransformationMethod.getInstance(); + } else { + mTransformationMethod = PasswordTransformationMethod.getInstance(); + } + } finally { + typedArray.recycle(); + } + + if (enableIconAlpha) { + mHidePwDrawable.setAlpha(ALPHA_ICON_ENABLED); + mShowPwDrawable.setAlpha(ALPHA_ICON_DISABLED); + } + + if (useNonMonospaceFont) { + setTypeface(Typeface.DEFAULT); + } + + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence seq, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() > 0) { + if (mSetErrorCalled) { + setCompoundDrawablesRelative(null, null, null, null); + mSetErrorCalled = false; + showPasswordVisibilityIndicator(true); + } + if (!mShowingIcon) { + showPasswordVisibilityIndicator(true); + } + } else { + // hides the indicator if no text inside text field + mPasswordVisible = false; + handlePasswordInputVisibility(); + showPasswordVisibilityIndicator(false); + } + + } + }); + + handlePasswordInputVisibility(); + } + + public PasswordEditText setExtraClickAreaSize(int extraClickArea) { + mExtraClickArea = extraClickArea; + return this; + } + + /** + * 设置密码输入框的样式 + * + * @param transformationMethod + * @return + */ + public PasswordEditText setPasswordTransformationMethod(PasswordTransformationMethod transformationMethod) { + mTransformationMethod = transformationMethod; + return this; + } + + /** + * 设置密码输入框的样式 + * + * @param isAsteriskStyle + * @return + */ + public PasswordEditText setIsAsteriskStyle(boolean isAsteriskStyle) { + if (isAsteriskStyle) { + mTransformationMethod = AsteriskPasswordTransformationMethod.getInstance(); + } else { + mTransformationMethod = PasswordTransformationMethod.getInstance(); + } + return this; + } + + private boolean isRtl() { + return getLayoutDirection() == LAYOUT_DIRECTION_RTL; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState, mShowingIcon, mPasswordVisible); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + mShowingIcon = savedState.isShowingIcon(); + mPasswordVisible = savedState.isPasswordVisible(); + handlePasswordInputVisibility(); + showPasswordVisibilityIndicator(mShowingIcon); + } + + @Override + public void setError(CharSequence error) { + super.setError(error); + mSetErrorCalled = true; + + } + + @Override + public void setError(CharSequence error, Drawable icon) { + super.setError(error, icon); + mSetErrorCalled = true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mShowingIcon) { + return super.onTouchEvent(event); + } else { + boolean touchable = isTouchable(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mHoverShowsPw) { + if (touchable) { + togglePasswordIconVisibility(); + // prevent keyboard from coming up + event.setAction(MotionEvent.ACTION_CANCEL); + mHandlingHoverEvent = true; + } + } + break; + case MotionEvent.ACTION_UP: + if (mHandlingHoverEvent || touchable) { + togglePasswordIconVisibility(); + // prevent keyboard from coming up + event.setAction(MotionEvent.ACTION_CANCEL); + mHandlingHoverEvent = false; + } + break; + default: + break; + } + return super.onTouchEvent(event); + } + } + + private boolean isTouchable(MotionEvent event) { + boolean touchable; + if (isRtl()) { + touchable = event.getX() > getPaddingLeft() - mExtraClickArea && event.getX() < getPaddingLeft() + mShowPwDrawable.getIntrinsicWidth() + mExtraClickArea; + } else { + touchable = event.getX() > getWidth() - getPaddingRight() - mShowPwDrawable.getIntrinsicWidth() - mExtraClickArea && event.getX() < getWidth() - getPaddingRight() + mExtraClickArea; + } + return touchable; + } + + + private void showPasswordVisibilityIndicator(boolean shouldShowIcon) { + if (shouldShowIcon) { + Drawable drawable = mPasswordVisible ? mShowPwDrawable : mHidePwDrawable; + mShowingIcon = true; + setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, drawable, null); + } else { + // reset drawable + setCompoundDrawablesRelative(null, null, null, null); + mShowingIcon = false; + } + } + + /** + * This method toggles the visibility of the icon and takes care of switching the input type + * of the view to be able to see the password afterwards. + *

+ * This method may only be called if there is an icon visible + */ + private void togglePasswordIconVisibility() { + mPasswordVisible = !mPasswordVisible; + handlePasswordInputVisibility(); + showPasswordVisibilityIndicator(true); + } + + /** + * This method is called when restoring the state (e.g. on orientation change) + */ + private void handlePasswordInputVisibility() { + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + if (mPasswordVisible) { + setTransformationMethod(null); + } else { + setTransformationMethod(mTransformationMethod); + } + setSelection(selectionStart, selectionEnd); + + } + + /** + * Convenience class to save / restore the state of icon. + */ + protected static class SavedState extends BaseSavedState { + + private final boolean mShowingIcon; + private final boolean mPasswordVisible; + + private SavedState(Parcelable superState, boolean sI, boolean pV) { + super(superState); + mShowingIcon = sI; + mPasswordVisible = pV; + } + + private SavedState(Parcel in) { + super(in); + mShowingIcon = in.readByte() != 0; + mPasswordVisible = in.readByte() != 0; + } + + public boolean isShowingIcon() { + return mShowingIcon; + } + + public boolean isPasswordVisible() { + return mPasswordVisible; + } + + @Override + public void writeToParcel(Parcel destination, int flags) { + super.writeToParcel(destination, flags); + destination.writeByte((byte) (mShowingIcon ? 1 : 0)); + destination.writeByte((byte) (mPasswordVisible ? 1 : 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]; + } + + }; + } +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/materialedittext/MaterialEditText.java b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/MaterialEditText.java new file mode 100644 index 0000000..3576b11 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/MaterialEditText.java @@ -0,0 +1,1788 @@ +package com.project.survey.widget.edittext.materialedittext; + +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Editable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.text.method.TransformationMethod; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; + + +import com.project.survey.R; +import com.project.survey.util.DensityUtils; +import com.project.survey.util.DrawableUtils; +import com.project.survey.util.ResUtils; +import com.project.survey.util.ThemeUtils; +import com.project.survey.widget.XUI; +import com.project.survey.widget.edittext.AsteriskPasswordTransformationMethod; +import com.project.survey.widget.edittext.materialedittext.validation.METLengthChecker; +import com.project.survey.widget.edittext.materialedittext.validation.METValidator; +import com.project.survey.widget.edittext.materialedittext.validation.NotAllowEmptyValidator; +import com.project.survey.widget.edittext.materialedittext.validation.RegexpValidator; +import com.project.survey.widget.util.Utils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.github.inflationx.calligraphy3.HasTypeface; + + +/** + * Material Design 输入框 + * + * @author XUE + * @since 2019/3/20 16:47 + */ +@Keep +public class MaterialEditText extends AppCompatEditText implements HasTypeface { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({FLOATING_LABEL_NONE, FLOATING_LABEL_NORMAL, FLOATING_LABEL_HIGHLIGHT}) + public @interface FloatingLabelType { + } + + public static final int FLOATING_LABEL_NONE = 0; + public static final int FLOATING_LABEL_NORMAL = 1; + public static final int FLOATING_LABEL_HIGHLIGHT = 2; + + /** + * the spacing between the main text and the inner top padding. + */ + private int extraPaddingTop; + + /** + * the spacing between the main text and the inner bottom padding. + */ + private int extraPaddingBottom; + + /** + * the extra spacing between the main text and the left, actually for the left icon. + */ + private int extraPaddingLeft; + + /** + * the extra spacing between the main text and the right, actually for the right icon. + */ + private int extraPaddingRight; + + /** + * the floating label's text size. + */ + private int floatingLabelTextSize; + + /** + * the floating label's text color. + */ + private int floatingLabelTextColor; + + /** + * the bottom texts' size. + */ + private int bottomTextSize; + + /** + * the spacing between the main text and the floating label. + */ + private int floatingLabelPadding; + + /** + * the spacing between the main text and the bottom components (bottom ellipsis, helper/error text, characters counter). + */ + private int bottomSpacing; + + /** + * whether the floating label should be shown. default is false. + */ + private boolean floatingLabelEnabled; + + /** + * whether to highlight the floating label's text color when focused (with the main color). default is true. + */ + private boolean highlightFloatingLabel; + + /** + * the base color of the line and the texts. default is black. + */ + private int baseColor; + + /** + * inner top padding + */ + private int innerPaddingTop; + + /** + * inner bottom padding + */ + private int innerPaddingBottom; + + /** + * inner left padding + */ + private int innerPaddingLeft; + + /** + * inner right padding + */ + private int innerPaddingRight; + + /** + * the underline's highlight color, and the highlight color of the floating label if app:highlightFloatingLabel is set true in the xml. default is black(when app:darkTheme is false) or white(when app:darkTheme is true) + */ + private int primaryColor; + + /** + * the color for when something is wrong.(e.g. exceeding max characters) + */ + private int errorColor; + + /** + * min characters count limit. 0 means no limit. default is 0. NOTE: the character counter will increase the View's height. + */ + private int minCharacters; + + /** + * max characters count limit. 0 means no limit. default is 0. NOTE: the character counter will increase the View's height. + */ + private int maxCharacters; + + /** + * whether to show the bottom ellipsis in singleLine mode. default is false. NOTE: the bottom ellipsis will increase the View's height. + */ + private boolean singleLineEllipsis; + + /** + * Always show the floating label, instead of animating it in/out. False by default. + */ + private boolean floatingLabelAlwaysShown; + + /** + * Always show the helper text, no matter if the edit text is focused. False by default. + */ + private boolean helperTextAlwaysShown; + + /** + * bottom ellipsis's height + */ + private int bottomEllipsisSize; + + /** + * min bottom lines count. + */ + private int minBottomLines; + + /** + * reserved bottom text lines count, no matter if there is some helper/error text. + */ + private int minBottomTextLines; + + /** + * real-time bottom lines count. used for bottom extending/collapsing animation. + */ + private float currentBottomLines; + + /** + * bottom lines count. + */ + private float bottomLines; + + /** + * Helper text at the bottom + */ + private String helperText; + + /** + * Helper text color + */ + private int helperTextColor = -1; + + /** + * error text for manually invoked + */ + private String tempErrorText; + + /** + * animation fraction of the floating label (0 as totally hidden). + */ + private float floatingLabelFraction; + + /** + * whether the floating label is being shown. + */ + private boolean floatingLabelShown; + + /** + * the floating label's focusFraction + */ + private float focusFraction; + + /** + * The font used for the accent texts (floating label, error/helper text, character counter, etc.) + */ + private Typeface accentTypeface; + + /** + * Text for the floatLabel if different from the hint + */ + private CharSequence floatingLabelText; + + /** + * Whether or not to show the underline. Shown by default + */ + private boolean hideUnderline; + + /** + * Underline's color + */ + private int underlineColor; + /** + * Underline's height + */ + private int underlineHeight; + private int underlineHeightFocused; + /** + * Whether to validate as soon as the text has changed. False by default + */ + private boolean autoValidate; + + /** + * Whether the characters count is valid + */ + private boolean charactersCountValid; + + /** + * Whether use animation to show/hide the floating label. + */ + private boolean floatingLabelAnimating; + + /** + * Whether check the characters count at the beginning it's shown. + */ + private boolean checkCharactersCountAtBeginning; + + /** + * Left Icon + */ + private Bitmap[] iconLeftBitmaps; + + /** + * Right Icon + */ + private Bitmap[] iconRightBitmaps; + + /** + * Clear Button + */ + private Bitmap[] clearButtonBitmaps; + /** + * showPwIcon Button + */ + private Bitmap[] showPwIconBitmaps; + /** + * hidePwIcon Button + */ + private Bitmap[] hidePwIconBitmaps; + + private boolean passwordVisible; + + /** + * Auto validate when focus lost. + */ + private boolean validateOnFocusLost; + + private boolean showClearButton; + private boolean showPasswordButton; + private boolean firstShown; + private int iconSize; + private int iconOuterWidth; + private int iconOuterHeight; + private int iconPadding; + private boolean actionButtonTouched; + private boolean actionButtonClicking; + private ColorStateList textColorStateList; + private ColorStateList textColorHintStateList; + private ArgbEvaluator focusEvaluator = new ArgbEvaluator(); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + StaticLayout textLayout; + ObjectAnimator labelAnimator; + ObjectAnimator labelFocusAnimator; + ObjectAnimator bottomLinesAnimator; + OnFocusChangeListener innerFocusChangeListener; + OnFocusChangeListener outerFocusChangeListener; + private List validators = new ArrayList<>(); + private METLengthChecker lengthChecker; + private PasswordTransformationMethod mTransformationMethod; + + public MaterialEditText(Context context) { + this(context, null); + } + + public MaterialEditText(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.MaterialEditTextStyle); + } + + public MaterialEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + iconSize = getPixel(32); + iconOuterWidth = getPixel(24); + iconOuterHeight = getPixel(32); + + bottomSpacing = getResources().getDimensionPixelSize(R.dimen.default_edittext_components_spacing); + bottomEllipsisSize = getResources().getDimensionPixelSize(R.dimen.default_bottom_ellipsis_height); + + // default baseColor is black + int defaultBaseColor = Color.BLACK; + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialEditText, defStyleAttr, 0); + textColorStateList = ResUtils.getColorStateListAttrRes(context, typedArray, R.styleable.MaterialEditText_met_textColor); + textColorHintStateList = ResUtils.getColorStateListAttrRes(context, typedArray, R.styleable.MaterialEditText_met_textColorHint); + baseColor = typedArray.getColor(R.styleable.MaterialEditText_met_baseColor, defaultBaseColor); + + primaryColor = typedArray.getColor(R.styleable.MaterialEditText_met_primaryColor, ThemeUtils.resolveColor(getContext(), R.attr.colorPrimary, baseColor)); + setFloatingLabelInternal(typedArray.getInt(R.styleable.MaterialEditText_met_floatingLabel, 0)); + errorColor = typedArray.getColor(R.styleable.MaterialEditText_met_errorColor, ThemeUtils.resolveColor(getContext(), R.attr.xui_config_color_error_text)); + + boolean allowEmpty = typedArray.getBoolean(R.styleable.MaterialEditText_met_allowEmpty, true); + if (!allowEmpty) { + String errorEmpty = typedArray.getString(R.styleable.MaterialEditText_met_errorEmpty); + if (!TextUtils.isEmpty(errorEmpty)) { + validators.add(new NotAllowEmptyValidator(errorEmpty)); + } else { + validators.add(new NotAllowEmptyValidator(ResUtils.getString(context, R.string.xui_met_not_allow_empty))); + } + } + minCharacters = typedArray.getInt(R.styleable.MaterialEditText_met_minCharacters, 0); + maxCharacters = typedArray.getInt(R.styleable.MaterialEditText_met_maxCharacters, 0); + singleLineEllipsis = typedArray.getBoolean(R.styleable.MaterialEditText_met_singleLineEllipsis, false); + helperText = typedArray.getString(R.styleable.MaterialEditText_met_helperText); + helperTextColor = typedArray.getColor(R.styleable.MaterialEditText_met_helperTextColor, -1); + minBottomTextLines = typedArray.getInt(R.styleable.MaterialEditText_met_minBottomTextLines, 0); + String fontPathForAccent = typedArray.getString(R.styleable.MaterialEditText_met_accentTypeface); + if (fontPathForAccent != null) { + accentTypeface = XUI.getDefaultTypeface(fontPathForAccent); + textPaint.setTypeface(accentTypeface); + } + String fontPathForView = typedArray.getString(R.styleable.MaterialEditText_met_typeface); + if (fontPathForView != null) { + Typeface typeface = XUI.getDefaultTypeface(fontPathForView); + setTypeface(typeface); + } + floatingLabelText = typedArray.getString(R.styleable.MaterialEditText_met_floatingLabelText); + if (floatingLabelText == null) { + floatingLabelText = getHint(); + } + floatingLabelPadding = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_floatingLabelPadding, bottomSpacing); + floatingLabelTextSize = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_floatingLabelTextSize, getResources().getDimensionPixelSize(R.dimen.default_floating_label_text_size)); + floatingLabelTextColor = typedArray.getColor(R.styleable.MaterialEditText_met_floatingLabelTextColor, -1); + floatingLabelAnimating = typedArray.getBoolean(R.styleable.MaterialEditText_met_floatingLabelAnimating, true); + bottomTextSize = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_bottomTextSize, getResources().getDimensionPixelSize(R.dimen.default_bottom_text_size)); + hideUnderline = typedArray.getBoolean(R.styleable.MaterialEditText_met_hideUnderline, false); + underlineColor = typedArray.getColor(R.styleable.MaterialEditText_met_underlineColor, -1); + underlineHeight = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_underlineHeight, getPixel(1)); + underlineHeightFocused = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_underlineHeightFocused, getPixel(2)); + autoValidate = typedArray.getBoolean(R.styleable.MaterialEditText_met_autoValidate, false); + iconLeftBitmaps = generateIconBitmaps(typedArray.getResourceId(R.styleable.MaterialEditText_met_iconLeft, -1)); + iconRightBitmaps = generateIconBitmaps(typedArray.getResourceId(R.styleable.MaterialEditText_met_iconRight, -1)); + + showClearButton = typedArray.getBoolean(R.styleable.MaterialEditText_met_clearButton, false); + clearButtonBitmaps = generateIconBitmaps(DrawableUtils.getBitmapByDrawableId(getContext(), R.drawable.xui_ic_default_clear_btn)); + showPasswordButton = typedArray.getBoolean(R.styleable.MaterialEditText_met_passWordButton, false); + boolean isAsteriskStyle = typedArray.getBoolean(R.styleable.MaterialEditText_met_isAsteriskStyle, false); + if (isAsteriskStyle) { + mTransformationMethod = AsteriskPasswordTransformationMethod.getInstance(); + } else { + mTransformationMethod = PasswordTransformationMethod.getInstance(); + } + if (showPasswordButton) { + handleSwitchPasswordInputVisibility(); + } + + showPwIconBitmaps = generateIconBitmaps(DrawableUtils.getBitmapByDrawableId(getContext(), R.drawable.pet_icon_visibility_24dp)); + hidePwIconBitmaps = generateIconBitmaps(DrawableUtils.getBitmapByDrawableId(getContext(), R.drawable.pet_icon_visibility_off_24dp)); + + String regexp = typedArray.getString(R.styleable.MaterialEditText_met_regexp); + if (!TextUtils.isEmpty(regexp)) { + String errorMessage = typedArray.getString(R.styleable.MaterialEditText_met_errorMessage); + if (!TextUtils.isEmpty(errorMessage)) { + validators.add(new RegexpValidator(errorMessage, regexp)); + } else { + validators.add(new RegexpValidator(ResUtils.getString(context, R.string.xui_met_input_error), regexp)); + } + } + + iconPadding = typedArray.getDimensionPixelSize(R.styleable.MaterialEditText_met_iconPadding, getPixel(8)); + floatingLabelAlwaysShown = typedArray.getBoolean(R.styleable.MaterialEditText_met_floatingLabelAlwaysShown, false); + helperTextAlwaysShown = typedArray.getBoolean(R.styleable.MaterialEditText_met_helperTextAlwaysShown, false); + validateOnFocusLost = typedArray.getBoolean(R.styleable.MaterialEditText_met_validateOnFocusLost, false); + checkCharactersCountAtBeginning = typedArray.getBoolean(R.styleable.MaterialEditText_met_checkCharactersCountAtBeginning, true); + typedArray.recycle(); + + int[] paddings = new int[]{ + android.R.attr.padding, // 0 + android.R.attr.paddingLeft, // 1 + android.R.attr.paddingTop, // 2 + android.R.attr.paddingRight, // 3 + android.R.attr.paddingBottom // 4 + }; + TypedArray paddingsTypedArray = context.obtainStyledAttributes(attrs, paddings); + int padding = paddingsTypedArray.getDimensionPixelSize(0, 0); + innerPaddingLeft = paddingsTypedArray.getDimensionPixelSize(1, padding); + innerPaddingTop = paddingsTypedArray.getDimensionPixelSize(2, padding); + innerPaddingRight = paddingsTypedArray.getDimensionPixelSize(3, padding); + innerPaddingBottom = paddingsTypedArray.getDimensionPixelSize(4, padding); + paddingsTypedArray.recycle(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + setBackground(null); + } else { + setBackgroundDrawable(null); + } + if (singleLineEllipsis) { + TransformationMethod transformationMethod = getTransformationMethod(); + setSingleLine(); + setTransformationMethod(transformationMethod); + } + initMinBottomLines(); + initPadding(); + initText(); + initFloatingLabel(); + initTextWatcher(); + checkCharactersCount(); + } + + private void initText() { + if (!TextUtils.isEmpty(getText())) { + CharSequence text = getText(); + setText(null); + resetHintTextColor(); + setText(text); + setSelection(text != null ? text.length() : 0); + floatingLabelFraction = 1; + floatingLabelShown = true; + } else { + resetHintTextColor(); + } + resetTextColor(); + } + + private void initTextWatcher() { + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + checkCharactersCount(); + if (autoValidate) { + validate(); + } else { + setError(null); + } + postInvalidate(); + } + }); + } + + public MaterialEditText setIconLeft(@DrawableRes int res) { + iconLeftBitmaps = generateIconBitmaps(res); + initPadding(); + return this; + } + + public MaterialEditText setIconLeft(Drawable drawable) { + iconLeftBitmaps = generateIconBitmaps(drawable); + initPadding(); + return this; + } + + public MaterialEditText setIconLeft(Bitmap bitmap) { + iconLeftBitmaps = generateIconBitmaps(bitmap); + initPadding(); + return this; + } + + public MaterialEditText setIconRight(@DrawableRes int res) { + iconRightBitmaps = generateIconBitmaps(res); + initPadding(); + return this; + } + + public MaterialEditText setIconRight(Drawable drawable) { + iconRightBitmaps = generateIconBitmaps(drawable); + initPadding(); + return this; + } + + public MaterialEditText setIconRight(Bitmap bitmap) { + iconRightBitmaps = generateIconBitmaps(bitmap); + initPadding(); + return this; + } + + public boolean isShowClearButton() { + return showClearButton; + } + + public boolean isShowPasswordButton() { + return showPasswordButton; + } + + public MaterialEditText setShowClearButton(boolean show) { + showClearButton = show; + correctPaddings(); + return this; + } + + /** + * 设置密码输入框的样式 + * + * @param transformationMethod + * @return + */ + public MaterialEditText setPasswordTransformationMethod(PasswordTransformationMethod transformationMethod) { + mTransformationMethod = transformationMethod; + return this; + } + + private Bitmap[] generateIconBitmaps(@DrawableRes int origin) { + if (origin == -1) { + return null; + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + int size = Math.max(options.outWidth, options.outHeight); + options.inSampleSize = size > iconSize ? size / iconSize : 1; + options.inJustDecodeBounds = false; + return generateIconBitmaps(BitmapFactory.decodeResource(getResources(), origin, options)); + } + + private Bitmap[] generateIconBitmaps(Drawable drawable) { + if (drawable == null) { + return null; + } + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return generateIconBitmaps(Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false)); + } + + private Bitmap[] generateIconBitmaps(Bitmap origin) { + if (origin == null) { + return null; + } + Bitmap[] iconBitmaps = new Bitmap[4]; + origin = scaleIcon(origin); + iconBitmaps[0] = origin.copy(Bitmap.Config.ARGB_8888, true); + Canvas canvas = new Canvas(iconBitmaps[0]); + canvas.drawColor(baseColor & 0x00ffffff | (Utils.isLight(baseColor) ? 0xff000000 : 0x8a000000), PorterDuff.Mode.SRC_IN); + iconBitmaps[1] = origin.copy(Bitmap.Config.ARGB_8888, true); + canvas = new Canvas(iconBitmaps[1]); + canvas.drawColor(primaryColor, PorterDuff.Mode.SRC_IN); + iconBitmaps[2] = origin.copy(Bitmap.Config.ARGB_8888, true); + canvas = new Canvas(iconBitmaps[2]); + canvas.drawColor(baseColor & 0x00ffffff | (Utils.isLight(baseColor) ? 0x4c000000 : 0x42000000), PorterDuff.Mode.SRC_IN); + iconBitmaps[3] = origin.copy(Bitmap.Config.ARGB_8888, true); + canvas = new Canvas(iconBitmaps[3]); + canvas.drawColor(errorColor, PorterDuff.Mode.SRC_IN); + return iconBitmaps; + } + + private Bitmap scaleIcon(Bitmap origin) { + int width = origin.getWidth(); + int height = origin.getHeight(); + int size = Math.max(width, height); + if (size == iconSize) { + return origin; + } else if (size > iconSize) { + int scaledWidth; + int scaledHeight; + if (width > iconSize) { + scaledWidth = iconSize; + scaledHeight = (int) (iconSize * ((float) height / width)); + } else { + scaledHeight = iconSize; + scaledWidth = (int) (iconSize * ((float) width / height)); + } + return Bitmap.createScaledBitmap(origin, scaledWidth, scaledHeight, false); + } else { + return origin; + } + } + + public float getFloatingLabelFraction() { + return floatingLabelFraction; + } + + public MaterialEditText setFloatingLabelFraction(float floatingLabelFraction) { + this.floatingLabelFraction = floatingLabelFraction; + invalidate(); + return this; + } + + public float getFocusFraction() { + return focusFraction; + } + + public MaterialEditText setFocusFraction(float focusFraction) { + this.focusFraction = focusFraction; + invalidate(); + return this; + } + + public float getCurrentBottomLines() { + return currentBottomLines; + } + + public MaterialEditText setCurrentBottomLines(float currentBottomLines) { + this.currentBottomLines = currentBottomLines; + initPadding(); + return this; + } + + public boolean isFloatingLabelAlwaysShown() { + return floatingLabelAlwaysShown; + } + + public MaterialEditText setFloatingLabelAlwaysShown(boolean floatingLabelAlwaysShown) { + this.floatingLabelAlwaysShown = floatingLabelAlwaysShown; + invalidate(); + return this; + } + + public boolean isHelperTextAlwaysShown() { + return helperTextAlwaysShown; + } + + public MaterialEditText setHelperTextAlwaysShown(boolean helperTextAlwaysShown) { + this.helperTextAlwaysShown = helperTextAlwaysShown; + invalidate(); + return this; + } + + @Nullable + public Typeface getAccentTypeface() { + return accentTypeface; + } + + /** + * Set typeface used for the accent texts (floating label, error/helper text, character counter, etc.) + */ + public MaterialEditText setAccentTypeface(Typeface accentTypeface) { + this.accentTypeface = accentTypeface; + this.textPaint.setTypeface(accentTypeface); + postInvalidate(); + return this; + } + + public boolean isHideUnderline() { + return hideUnderline; + } + + /** + * Set whether or not to hide the underline (shown by default). + *

+ * The positions of text below will be adjusted accordingly (error/helper text, character counter, ellipses, etc.) + *

+ * NOTE: You probably don't want to hide this if you have any subtext features of this enabled, as it can look weird to not have a dividing line between them. + */ + public MaterialEditText setHideUnderline(boolean hideUnderline) { + this.hideUnderline = hideUnderline; + initPadding(); + postInvalidate(); + return this; + } + + /** + * get the color of the underline for normal state + */ + public int getUnderlineColor() { + return underlineColor; + } + + /** + * Set the color of the underline for normal state + * + * @param color + */ + public MaterialEditText setUnderlineColor(int color) { + this.underlineColor = color; + postInvalidate(); + return this; + } + + public CharSequence getFloatingLabelText() { + return floatingLabelText; + } + + /** + * Set the floating label text. + *

+ * Pass null to force fallback to use hint's value. + * + * @param floatingLabelText + */ + public MaterialEditText setFloatingLabelText(@Nullable CharSequence floatingLabelText) { + this.floatingLabelText = floatingLabelText == null ? getHint() : floatingLabelText; + postInvalidate(); + return this; + } + + public int getFloatingLabelTextSize() { + return floatingLabelTextSize; + } + + public MaterialEditText setFloatingLabelTextSize(int size) { + floatingLabelTextSize = size; + initPadding(); + return this; + } + + public int getFloatingLabelTextColor() { + return floatingLabelTextColor; + } + + public MaterialEditText setFloatingLabelTextColor(int color) { + this.floatingLabelTextColor = color; + postInvalidate(); + return this; + } + + public int getBottomTextSize() { + return bottomTextSize; + } + + public void setBottomTextSize(int size) { + bottomTextSize = size; + initPadding(); + } + + private int getPixel(int dp) { + return DensityUtils.dp2px(getContext(), dp); + } + + private void initPadding() { + extraPaddingTop = floatingLabelEnabled ? floatingLabelTextSize + floatingLabelPadding : floatingLabelPadding; + textPaint.setTextSize(bottomTextSize); + Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); + extraPaddingBottom = (int) ((textMetrics.descent - textMetrics.ascent) * currentBottomLines) + (hideUnderline ? bottomSpacing : bottomSpacing * 2); + extraPaddingLeft = getStartIcon() == null ? 0 : (iconOuterWidth + iconPadding); + extraPaddingRight = getEndIcon() == null ? 0 : (iconOuterWidth + iconPadding); + correctPaddings(); + } + + /** + * calculate {@link #minBottomLines} + */ + private void initMinBottomLines() { + boolean extendBottom = minCharacters > 0 || maxCharacters > 0 || singleLineEllipsis || tempErrorText != null || helperText != null; + currentBottomLines = minBottomLines = minBottomTextLines > 0 ? minBottomTextLines : extendBottom ? 1 : 0; + } + + /** + * use {@link #setPaddings(int, int, int, int)} instead, or the paddingTop and the paddingBottom may be set incorrectly. + */ + @Deprecated + @Override + public final void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom); + } + + /** + * Use this method instead of {@link #setPadding(int, int, int, int)} to automatically set the paddingTop and the paddingBottom correctly. + */ + public MaterialEditText setPaddings(int left, int top, int right, int bottom) { + innerPaddingTop = top; + innerPaddingBottom = bottom; + innerPaddingLeft = left; + innerPaddingRight = right; + correctPaddings(); + return this; + } + + /** + * Set paddings to the correct values + */ + private void correctPaddings() { + int buttonsWidthLeft = 0, buttonsWidthRight = 0; + int buttonsWidth = iconOuterWidth * getButtonsCount(); + if (isRTL()) { + buttonsWidthLeft = buttonsWidth; + } else { + buttonsWidthRight = buttonsWidth; + } + super.setPaddingRelative(innerPaddingLeft + extraPaddingLeft + buttonsWidthLeft, innerPaddingTop + extraPaddingTop, innerPaddingRight + extraPaddingRight + buttonsWidthRight, innerPaddingBottom + extraPaddingBottom); + } + + private int getButtonsCount() { + return isShowClearButton() || isShowPasswordButton() ? 1 : 0; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!firstShown) { + firstShown = true; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + adjustBottomLines(); + } + } + + /** + * @return True, if adjustments were made that require the view to be invalidated. + */ + private boolean adjustBottomLines() { + // Bail out if we have a zero width; lines will be adjusted during next layout. + if (getWidth() == 0) { + return false; + } + int destBottomLines; + textPaint.setTextSize(bottomTextSize); + if (tempErrorText != null || helperText != null) { + Layout.Alignment alignment = (getGravity() & Gravity.END) == Gravity.END || isRTL() ? + Layout.Alignment.ALIGN_OPPOSITE : (getGravity() & Gravity.START) == Gravity.START ? + Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_CENTER; + textLayout = new StaticLayout(tempErrorText != null ? tempErrorText : helperText, textPaint, getWidth() - getBottomTextLeftOffset() - getBottomTextRightOffset() - getPaddingLeft() - getPaddingRight(), alignment, 1.0f, 0.0f, true); + destBottomLines = Math.max(textLayout.getLineCount(), minBottomTextLines); + } else { + destBottomLines = minBottomLines; + } + if (bottomLines != destBottomLines) { + getBottomLinesAnimator(destBottomLines).start(); + } + bottomLines = destBottomLines; + return true; + } + + /** + * get inner top padding, not the real paddingTop + */ + public int getInnerPaddingTop() { + return innerPaddingTop; + } + + /** + * get inner bottom padding, not the real paddingBottom + */ + public int getInnerPaddingBottom() { + return innerPaddingBottom; + } + + /** + * get inner left padding, not the real paddingLeft + */ + public int getInnerPaddingLeft() { + return innerPaddingLeft; + } + + /** + * get inner right padding, not the real paddingRight + */ + public int getInnerPaddingRight() { + return innerPaddingRight; + } + + private void initFloatingLabel() { + // observe the text changing + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (floatingLabelEnabled) { + if (s.length() == 0) { + if (floatingLabelShown) { + floatingLabelShown = false; + getLabelAnimator().reverse(); + } + } else if (!floatingLabelShown) { + floatingLabelShown = true; + getLabelAnimator().start(); + } + } + } + }); + // observe the focus state to animate the floating label's text color appropriately + innerFocusChangeListener = new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (floatingLabelEnabled && highlightFloatingLabel) { + if (hasFocus) { + getLabelFocusAnimator().start(); + } else { + getLabelFocusAnimator().reverse(); + } + } + if (validateOnFocusLost && !hasFocus) { + validate(); + } + if (outerFocusChangeListener != null) { + outerFocusChangeListener.onFocusChange(v, hasFocus); + } + } + }; + super.setOnFocusChangeListener(innerFocusChangeListener); + } + + public boolean isValidateOnFocusLost() { + return validateOnFocusLost; + } + + public MaterialEditText setValidateOnFocusLost(boolean validate) { + this.validateOnFocusLost = validate; + return this; + } + + public MaterialEditText setBaseColor(int color) { + if (baseColor != color) { + baseColor = color; + } + initText(); + postInvalidate(); + return this; + } + + public MaterialEditText setPrimaryColor(int color) { + primaryColor = color; + postInvalidate(); + return this; + } + + /** + * Same function as {@link #setTextColor(int)}. (Directly overriding the built-in one could cause some error, so use this method instead.) + */ + public MaterialEditText setMetTextColor(int color) { + textColorStateList = ColorStateList.valueOf(color); + resetTextColor(); + return this; + } + + /** + * Same function as {@link #setTextColor(ColorStateList)}. (Directly overriding the built-in one could cause some error, so use this method instead.) + */ + public MaterialEditText setMetTextColor(ColorStateList colors) { + textColorStateList = colors; + resetTextColor(); + return this; + } + + private void resetTextColor() { + if (textColorStateList == null) { + textColorStateList = new ColorStateList(new int[][]{new int[]{android.R.attr.state_enabled}, EMPTY_STATE_SET}, new int[]{baseColor & 0x00ffffff | 0xdf000000, baseColor & 0x00ffffff | 0x44000000}); + setTextColor(textColorStateList); + } else { + setTextColor(textColorStateList); + } + } + + /** + * Same function as {@link #setHintTextColor(int)}. (The built-in one is a final method that can't be overridden, so use this method instead.) + */ + public MaterialEditText setMetHintTextColor(int color) { + textColorHintStateList = ColorStateList.valueOf(color); + resetHintTextColor(); + return this; + } + + /** + * Same function as {@link #setHintTextColor(ColorStateList)}. (The built-in one is a final method that can't be overridden, so use this method instead.) + */ + public MaterialEditText setMetHintTextColor(ColorStateList colors) { + textColorHintStateList = colors; + resetHintTextColor(); + return this; + } + + private void resetHintTextColor() { + if (textColorHintStateList == null) { + setHintTextColor(baseColor & 0x00ffffff | 0x44000000); + } else { + setHintTextColor(textColorHintStateList); + } + } + + private void setFloatingLabelInternal(int mode) { + switch (mode) { + case FLOATING_LABEL_NORMAL: + floatingLabelEnabled = true; + highlightFloatingLabel = false; + break; + case FLOATING_LABEL_HIGHLIGHT: + floatingLabelEnabled = true; + highlightFloatingLabel = true; + break; + default: + floatingLabelEnabled = false; + highlightFloatingLabel = false; + break; + } + } + + public MaterialEditText setFloatingLabel(@FloatingLabelType int mode) { + setFloatingLabelInternal(mode); + initPadding(); + return this; + } + + public int getFloatingLabelPadding() { + return floatingLabelPadding; + } + + public MaterialEditText setFloatingLabelPadding(int padding) { + floatingLabelPadding = padding; + postInvalidate(); + return this; + } + + public boolean isFloatingLabelAnimating() { + return floatingLabelAnimating; + } + + public MaterialEditText setFloatingLabelAnimating(boolean animating) { + floatingLabelAnimating = animating; + return this; + } + + public MaterialEditText setSingleLineEllipsis() { + return setSingleLineEllipsis(true); + } + + public MaterialEditText setSingleLineEllipsis(boolean enabled) { + singleLineEllipsis = enabled; + initMinBottomLines(); + initPadding(); + postInvalidate(); + return this; + } + + public int getMaxCharacters() { + return maxCharacters; + } + + public MaterialEditText setMaxCharacters(int max) { + maxCharacters = max; + initMinBottomLines(); + initPadding(); + postInvalidate(); + return this; + } + + public int getMinCharacters() { + return minCharacters; + } + + public MaterialEditText setMinCharacters(int min) { + minCharacters = min; + initMinBottomLines(); + initPadding(); + postInvalidate(); + return this; + } + + public int getMinBottomTextLines() { + return minBottomTextLines; + } + + public MaterialEditText setMinBottomTextLines(int lines) { + minBottomTextLines = lines; + initMinBottomLines(); + initPadding(); + postInvalidate(); + return this; + } + + public boolean isAutoValidate() { + return autoValidate; + } + + public MaterialEditText setAutoValidate(boolean autoValidate) { + this.autoValidate = autoValidate; + if (autoValidate) { + validate(); + } + return this; + } + + public int getErrorColor() { + return errorColor; + } + + public MaterialEditText setErrorColor(int color) { + errorColor = color; + postInvalidate(); + return this; + } + + public MaterialEditText setHelperText(CharSequence helperText) { + this.helperText = helperText == null ? null : helperText.toString(); + if (adjustBottomLines()) { + postInvalidate(); + } + return this; + } + + public String getHelperText() { + return helperText; + } + + public int getHelperTextColor() { + return helperTextColor; + } + + public MaterialEditText setHelperTextColor(int color) { + helperTextColor = color; + postInvalidate(); + return this; + } + + /** + * 设置输入框是否允许为空 + * + * @param allowEmpty + * @param errorEmpty + * @return + */ + public MaterialEditText setAllowEmpty(boolean allowEmpty, String errorEmpty) { + boolean updateError = false; + Iterator it = validators.iterator(); + while (it.hasNext()) { + METValidator item = it.next(); + if (item instanceof NotAllowEmptyValidator) { + if (allowEmpty) { + it.remove(); + } else { + if (!TextUtils.isEmpty(errorEmpty)) { + item.setErrorMessage(errorEmpty); + } else { + item.setErrorMessage(ResUtils.getString(getContext(), R.string.xui_met_not_allow_empty)); + } + updateError = true; + } + break; + } + } + if (!allowEmpty && !updateError) { + if (!TextUtils.isEmpty(errorEmpty)) { + validators.add(new NotAllowEmptyValidator(errorEmpty)); + } else { + validators.add(new NotAllowEmptyValidator(ResUtils.getString(getContext(), R.string.xui_met_not_allow_empty))); + } + } + return this; + } + + @Override + public void setError(CharSequence errorText) { + tempErrorText = errorText == null ? null : errorText.toString(); + if (adjustBottomLines()) { + postInvalidate(); + } + } + + @Override + public CharSequence getError() { + return tempErrorText; + } + + /** + * only used to draw the bottom line + */ + private boolean isInternalValid() { + return tempErrorText == null && isCharactersCountValid(); + } + + /** + * if the main text matches the regex + * + * @deprecated use the new validator interface to add your own custom validator + */ + @Deprecated + public boolean isValid(String regex) { + if (regex == null) { + return false; + } + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(getText()); + return matcher.matches(); + } + + /** + * check if the main text matches the regex, and set the error text if not. + * + * @return true if it matches the regex, false if not. + * @deprecated use the new validator interface to add your own custom validator + */ + @Deprecated + public boolean validate(String regex, CharSequence errorText) { + boolean isValid = isValid(regex); + if (!isValid) { + setError(errorText); + } + postInvalidate(); + return isValid; + } + + /** + * Run validation on a single validator instance + * + * @param validator Validator to check + * @return True if valid, false if not + */ + public boolean validateWith(@NonNull METValidator validator) { + CharSequence text = getText(); + boolean isValid = validator.isValid(text, text.length() == 0); + if (!isValid) { + setError(validator.getErrorMessage()); + } + postInvalidate(); + return isValid; + } + + /** + * Check all validators, sets the error text if not + *

+ * NOTE: this stops at the first validator to report invalid. + * + * @return True if all validators pass, false if not + */ + public boolean validate() { + if (validators == null || validators.isEmpty()) { + return true; + } + + CharSequence text = getText(); + boolean isEmpty = TextUtils.isEmpty(text); + + boolean isValid = true; + for (METValidator validator : validators) { + //noinspection ConstantConditions + isValid = isValid && validator.isValid(text, isEmpty); + if (!isValid) { + setError(validator.getErrorMessage()); + break; + } + } + if (isValid) { + setError(null); + } + + postInvalidate(); + return isValid; + } + + public boolean hasValidators() { + return this.validators != null && !this.validators.isEmpty(); + } + + /** + * Adds a new validator to the View's list of validators + *

+ * This will be checked with the others in {@link #validate()} + * + * @param validator Validator to add + * @return This instance, for easy chaining + */ + public MaterialEditText addValidator(METValidator validator) { + if (validators == null) { + this.validators = new ArrayList<>(); + } + this.validators.add(validator); + return this; + } + + public MaterialEditText clearValidators() { + if (this.validators != null) { + this.validators.clear(); + } + return this; + } + + @Nullable + public List getValidators() { + return this.validators; + } + + public MaterialEditText setLengthChecker(METLengthChecker lengthChecker) { + this.lengthChecker = lengthChecker; + return this; + } + + /** + * 清除内容 + */ + public void clear() { + if (!TextUtils.isEmpty(getText())) { + setText(null); + } + } + + /** + * 获取输入的内容 + * + * @return + */ + public String getEditValue() { + return getEditableText().toString().trim(); + } + + @Override + public void setEnabled(boolean enabled) { + super.setFocusable(enabled); + super.setFocusableInTouchMode(enabled); + super.setEnabled(enabled); + } + + /** + * 输入的内容是否为空 + * + * @return + */ + public boolean isEmpty() { + return TextUtils.isEmpty(getEditValue()); + } + + /** + * 输入的内容是否不为空 + * + * @return + */ + public boolean isNotEmpty() { + return !TextUtils.isEmpty(getEditValue()); + } + + @Override + public void setOnFocusChangeListener(OnFocusChangeListener listener) { + if (innerFocusChangeListener == null) { + super.setOnFocusChangeListener(listener); + } else { + outerFocusChangeListener = listener; + } + } + + private ObjectAnimator getLabelAnimator() { + if (labelAnimator == null) { + labelAnimator = ObjectAnimator.ofFloat(this, "floatingLabelFraction", 0f, 1f); + } + labelAnimator.setDuration(floatingLabelAnimating ? 300 : 0); + return labelAnimator; + } + + private ObjectAnimator getLabelFocusAnimator() { + if (labelFocusAnimator == null) { + labelFocusAnimator = ObjectAnimator.ofFloat(this, "focusFraction", 0f, 1f); + } + return labelFocusAnimator; + } + + private ObjectAnimator getBottomLinesAnimator(float destBottomLines) { + if (bottomLinesAnimator == null) { + bottomLinesAnimator = ObjectAnimator.ofFloat(this, "currentBottomLines", destBottomLines); + } else { + bottomLinesAnimator.cancel(); + bottomLinesAnimator.setFloatValues(destBottomLines); + } + return bottomLinesAnimator; + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + + int startX = getScrollX() + (getStartIcon() == null ? 0 : iconOuterWidth + iconPadding); + int endX = getScrollX() + (getEndIcon() == null ? getWidth() : getWidth() - iconOuterWidth - iconPadding); + int lineStartY = getScrollY() + getHeight() - getPaddingBottom(); + + // draw the icon(s) + drawIcons(canvas, startX, endX, lineStartY); + + // draw the action button + drawActionButton(canvas, startX, endX, lineStartY); + + // draw the underline + lineStartY = drawUnderline(canvas, startX, endX, lineStartY); + + textPaint.setTextSize(bottomTextSize); + Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); + float relativeHeight = -textMetrics.ascent - textMetrics.descent; + float bottomTextPadding = bottomTextSize + textMetrics.ascent + textMetrics.descent; + + // draw the characters counter + if ((hasFocus() && hasCharactersCounter()) || !isCharactersCountValid()) { + textPaint.setColor(isCharactersCountValid() ? (baseColor & 0x00ffffff | 0x44000000) : errorColor); + String charactersCounterText = getCharactersCounterText(); + canvas.drawText(charactersCounterText, isRTL() ? startX : endX + getPaddingEnd() - textPaint.measureText(charactersCounterText), lineStartY + bottomSpacing + relativeHeight, textPaint); + } + + // draw the bottom text + drawBottomText(canvas, startX, endX, lineStartY, bottomTextPadding); + + // draw the floating label + drawFloatingLabel(canvas, startX, endX); + + // draw the bottom ellipsis + drawBottomEllipsis(canvas, startX, endX, lineStartY); + + // draw the original things + super.onDraw(canvas); + } + + private Bitmap[] getStartIcon() { + return isRTL() ? iconRightBitmaps : iconLeftBitmaps; + } + + private Bitmap[] getEndIcon() { + return isRTL() ? iconLeftBitmaps : iconRightBitmaps; + } + + /** + * 画图标 + * + * @param canvas + * @param startX + * @param endX + * @param lineStartY + */ + private void drawIcons(@NonNull Canvas canvas, int startX, int endX, int lineStartY) { + paint.setAlpha(255); + if (getStartIcon() != null) { + Bitmap icon = getStartIcon()[!isInternalValid() ? 3 : !isEnabled() ? 2 : hasFocus() ? 1 : 0]; + int iconLeft = startX - iconPadding - iconOuterWidth + (iconOuterWidth - icon.getWidth()) / 2; + int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - icon.getHeight()) / 2; + canvas.drawBitmap(icon, iconLeft, iconTop, paint); + } + if (getEndIcon() != null) { + Bitmap icon = getEndIcon()[!isInternalValid() ? 3 : !isEnabled() ? 2 : hasFocus() ? 1 : 0]; + int iconRight = endX + iconPadding + (iconOuterWidth - icon.getWidth()) / 2; + int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - icon.getHeight()) / 2; + canvas.drawBitmap(icon, iconRight, iconTop, paint); + } + } + + /** + * 画清除按钮 + * + * @param canvas + * @param startX + * @param endX + * @param lineStartY + */ + private void drawActionButton(@NonNull Canvas canvas, int startX, int endX, int lineStartY) { + if (hasFocus() && isEnabled() && !TextUtils.isEmpty(getText()) && (showClearButton || showPasswordButton)) { + paint.setAlpha(255); + int buttonLeft = isRTL() ? startX : endX - iconOuterWidth; + Bitmap actionButtonBitmap; + if (showClearButton) { + actionButtonBitmap = clearButtonBitmaps[0]; + } else { + actionButtonBitmap = passwordVisible ? showPwIconBitmaps[0] : hidePwIconBitmaps[0]; + } + int iconTop = lineStartY + bottomSpacing - iconOuterHeight + (iconOuterHeight - actionButtonBitmap.getHeight()) / 2; + canvas.drawBitmap(actionButtonBitmap, buttonLeft, iconTop, paint); + } + } + + /** + * 画下划线 + * + * @param canvas + * @param startX + * @param endX + * @param lineStartY + * @return + */ + private int drawUnderline(@NonNull Canvas canvas, int startX, int endX, int lineStartY) { + if (!hideUnderline) { + lineStartY += bottomSpacing; + if (!isInternalValid()) { // not valid + paint.setColor(errorColor); + canvas.drawRect(startX, lineStartY, endX, lineStartY + underlineHeightFocused, paint); + } else if (!isEnabled()) { // disabled + paint.setColor(underlineColor != -1 ? underlineColor : baseColor & 0x00ffffff | 0x44000000); + float interval = getPixel(1); + for (float xOffset = 0; xOffset < getWidth(); xOffset += interval * 3) { + canvas.drawRect(startX + xOffset, lineStartY, startX + xOffset + interval, lineStartY + underlineHeight, paint); + } + } else if (hasFocus()) { // focused + paint.setColor(primaryColor); + canvas.drawRect(startX, lineStartY, endX, lineStartY + underlineHeightFocused, paint); + } else { // normal + paint.setColor(underlineColor != -1 ? underlineColor : baseColor & 0x00ffffff | 0x1E000000); + canvas.drawRect(startX, lineStartY, endX, lineStartY + underlineHeight, paint); + } + } + return lineStartY; + } + + /** + * 画底部文字 + * + * @param canvas + * @param startX + * @param endX + * @param lineStartY + * @param bottomTextPadding + */ + private void drawBottomText(@NonNull Canvas canvas, int startX, int endX, int lineStartY, float bottomTextPadding) { + if (textLayout != null) { + if (tempErrorText != null || ((helperTextAlwaysShown || hasFocus()) && !TextUtils.isEmpty(helperText))) { // error text or helper text + textPaint.setColor(tempErrorText != null ? errorColor : helperTextColor != -1 ? helperTextColor : (baseColor & 0x00ffffff | 0x44000000)); + canvas.save(); + if (isRTL()) { + canvas.translate(endX - textLayout.getWidth(), lineStartY + bottomSpacing - bottomTextPadding); + } else { + canvas.translate(startX + getBottomTextLeftOffset(), lineStartY + bottomSpacing - bottomTextPadding); + } + textLayout.draw(canvas); + canvas.restore(); + } + } + } + + private void drawFloatingLabel(@NonNull Canvas canvas, int startX, int endX) { + if (floatingLabelEnabled && !TextUtils.isEmpty(floatingLabelText)) { + textPaint.setTextSize(floatingLabelTextSize); + // calculate the text color + textPaint.setColor((Integer) focusEvaluator.evaluate(focusFraction * (isEnabled() ? 1 : 0), floatingLabelTextColor != -1 ? floatingLabelTextColor : (baseColor & 0x00ffffff | 0x44000000), primaryColor)); + + // calculate the horizontal position + float floatingLabelWidth = textPaint.measureText(floatingLabelText.toString()); + int floatingLabelStartX; + if ((getGravity() & Gravity.RIGHT) == Gravity.RIGHT || isRTL()) { + floatingLabelStartX = (int) (endX - floatingLabelWidth); + } else if ((getGravity() & Gravity.LEFT) == Gravity.LEFT) { + floatingLabelStartX = startX; + } else { + floatingLabelStartX = startX + (int) (getInnerPaddingLeft() + (getWidth() - getInnerPaddingLeft() - getInnerPaddingRight() - floatingLabelWidth) / 2); + } + + // calculate the vertical position + int distance = floatingLabelPadding; + int floatingLabelStartY = (int) (innerPaddingTop + floatingLabelTextSize + floatingLabelPadding - distance * (floatingLabelAlwaysShown ? 1 : floatingLabelFraction) + getScrollY()); + + // calculate the alpha + int alpha = ((int) ((floatingLabelAlwaysShown ? 1 : floatingLabelFraction) * 0xff * (0.74f * focusFraction * (isEnabled() ? 1 : 0) + 0.26f) * (floatingLabelTextColor != -1 ? 1 : Color.alpha(floatingLabelTextColor) / 255F))); + textPaint.setAlpha(alpha); + + // draw the floating label + canvas.drawText(floatingLabelText.toString(), floatingLabelStartX, floatingLabelStartY, textPaint); + } + } + + private void drawBottomEllipsis(@NonNull Canvas canvas, int startX, int endX, int lineStartY) { + if (hasFocus() && singleLineEllipsis && getScrollX() != 0) { + paint.setColor(isInternalValid() ? primaryColor : errorColor); + float startY = lineStartY + bottomSpacing; + int ellipsisStartX; + if (isRTL()) { + ellipsisStartX = endX; + } else { + ellipsisStartX = startX; + } + int signum = isRTL() ? -1 : 1; + canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize / 2F, startY + bottomEllipsisSize / 2F, bottomEllipsisSize / 2F, paint); + canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize * 5 / 2F, startY + bottomEllipsisSize / 2F, bottomEllipsisSize / 2F, paint); + canvas.drawCircle(ellipsisStartX + signum * bottomEllipsisSize * 9 / 2F, startY + bottomEllipsisSize / 2F, bottomEllipsisSize / 2F, paint); + } + } + + private boolean isRTL() { + return getLayoutDirection() == LAYOUT_DIRECTION_RTL; + } + + private int getBottomTextLeftOffset() { + return isRTL() ? getCharactersCounterWidth() : getBottomEllipsisWidth(); + } + + private int getBottomTextRightOffset() { + return isRTL() ? getBottomEllipsisWidth() : getCharactersCounterWidth(); + } + + private int getCharactersCounterWidth() { + return hasCharactersCounter() ? (int) textPaint.measureText(getCharactersCounterText()) : 0; + } + + private int getBottomEllipsisWidth() { + return singleLineEllipsis ? (bottomEllipsisSize * 5 + getPixel(4)) : 0; + } + + private void checkCharactersCount() { + if ((!firstShown && !checkCharactersCountAtBeginning) || !hasCharactersCounter()) { + charactersCountValid = true; + } else { + CharSequence text = getText(); + int count = text == null ? 0 : checkLength(text); + charactersCountValid = (count >= minCharacters && (maxCharacters <= 0 || count <= maxCharacters)); + } + } + + public boolean isCharactersCountValid() { + return charactersCountValid; + } + + private boolean hasCharactersCounter() { + return minCharacters > 0 || maxCharacters > 0; + } + + private String getCharactersCounterText() { + String text; + if (minCharacters <= 0) { + text = isRTL() ? maxCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + maxCharacters; + } else if (maxCharacters <= 0) { + text = isRTL() ? "+" + minCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + minCharacters + "+"; + } else { + text = isRTL() ? maxCharacters + "-" + minCharacters + " / " + checkLength(getText()) : checkLength(getText()) + " / " + minCharacters + "-" + maxCharacters; + } + return text; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (singleLineEllipsis && getScrollX() > 0 && event.getAction() == MotionEvent.ACTION_DOWN && event.getX() < getPixel(4 * 5) && event.getY() > getHeight() - extraPaddingBottom - innerPaddingBottom && event.getY() < getHeight() - innerPaddingBottom) { + setSelection(0); + return false; + } + if (hasFocus() && (showClearButton || showPasswordButton) && isEnabled()) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (insideActionButton(event)) { + actionButtonTouched = true; + actionButtonClicking = true; + return true; + } + case MotionEvent.ACTION_MOVE: + if (actionButtonClicking && !insideActionButton(event)) { + actionButtonClicking = false; + } + if (actionButtonTouched) { + return true; + } + break; + case MotionEvent.ACTION_UP: + if (actionButtonClicking) { + if (showClearButton) { + if (!TextUtils.isEmpty(getText())) { + setText(null); + } + } else { + togglePasswordIconVisibility(); + } + actionButtonClicking = false; + } + if (actionButtonTouched) { + actionButtonTouched = false; + return true; + } + actionButtonTouched = false; + break; + case MotionEvent.ACTION_CANCEL: + actionButtonTouched = false; + actionButtonClicking = false; + break; + default: + break; + } + } + return super.onTouchEvent(event); + } + + private boolean insideActionButton(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int startX = getStartIcon() == null ? 0 : iconOuterWidth + iconPadding; + int endX = getEndIcon() == null ? getWidth() : getWidth() - iconOuterWidth - iconPadding; + int buttonLeft = isRTL() ? startX : endX - iconOuterWidth; + int buttonTop = getScrollY() + getHeight() - getPaddingBottom() + bottomSpacing - iconOuterHeight; + return (x >= buttonLeft && x < buttonLeft + iconOuterWidth && y >= buttonTop && y < buttonTop + iconOuterHeight); + } + + /** + * 密码显示切换 + */ + private void handleSwitchPasswordInputVisibility() { + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + if (passwordVisible) { + setTransformationMethod(null); + } else { + setTransformationMethod(mTransformationMethod); + + } + setSelection(selectionStart, selectionEnd); + } + + /** + * 密码显示切换 + */ + private void togglePasswordIconVisibility() { + passwordVisible = !passwordVisible; + handleSwitchPasswordInputVisibility(); + } + + private int checkLength(CharSequence text) { + if (lengthChecker == null) { + return text.length(); + } + return lengthChecker.getLength(text); + } + + @Override + public void setTypeface(Typeface typeface) { + super.setTypeface(typeface); + if (textPaint != null) { + textPaint.setTypeface(typeface); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METLengthChecker.java b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METLengthChecker.java new file mode 100644 index 0000000..6ac786f --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METLengthChecker.java @@ -0,0 +1,13 @@ +package com.project.survey.widget.edittext.materialedittext.validation; + +/** + * 长度验证 + * + * @author xuexiang + * @since 2018/11/26 下午5:06 + */ +public abstract class METLengthChecker { + + public abstract int getLength(CharSequence text); + +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METValidator.java b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METValidator.java new file mode 100644 index 0000000..af86cd5 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/METValidator.java @@ -0,0 +1,43 @@ +package com.project.survey.widget.edittext.materialedittext.validation; + +import androidx.annotation.NonNull; + +/** + * 自定义校验器 + * + * @author xuexiang + * @since 2018/11/26 下午5:06 + */ +public abstract class METValidator { + + /** + * Error message that the view will display if validation fails. + *

+ * This is protected, so you can change this dynamically in your {@link #isValid(CharSequence, boolean)} + * implementation. If necessary, you can also interact with this via its getter and setter. + */ + protected String errorMessage; + + public METValidator(@NonNull String errorMessage) { + this.errorMessage = errorMessage; + } + + public void setErrorMessage(@NonNull String errorMessage) { + this.errorMessage = errorMessage; + } + + @NonNull + public String getErrorMessage() { + return this.errorMessage; + } + + /** + * Abstract method to implement your own validation checking. + * + * @param text The CharSequence representation of the text in the EditText field. Cannot be null, but may be empty. + * @param isEmpty Boolean indicating whether or not the text param is empty + * @return True if valid, false if not + */ + public abstract boolean isValid(@NonNull CharSequence text, boolean isEmpty); + +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/NotAllowEmptyValidator.java b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/NotAllowEmptyValidator.java new file mode 100644 index 0000000..f3614dd --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/NotAllowEmptyValidator.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.project.survey.widget.edittext.materialedittext.validation; + +import androidx.annotation.NonNull; + +/** + * 非空检验 + * + * @author xuexiang + * @since 2019/5/14 10:27 + */ +public class NotAllowEmptyValidator extends METValidator { + + public NotAllowEmptyValidator(@NonNull String errorMessage) { + super(errorMessage); + } + + @Override + public boolean isValid(@NonNull CharSequence text, boolean isEmpty) { + return !isEmpty; + } +} diff --git a/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/RegexpValidator.java b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/RegexpValidator.java new file mode 100644 index 0000000..be787b5 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/edittext/materialedittext/validation/RegexpValidator.java @@ -0,0 +1,31 @@ +package com.project.survey.widget.edittext.materialedittext.validation; + +import androidx.annotation.NonNull; + +import java.util.regex.Pattern; + +/** + * 正则表达式验证 + * + * @author xuexiang + * @since 2018/11/26 下午5:06 + */ +public class RegexpValidator extends METValidator { + + private Pattern pattern; + + public RegexpValidator(@NonNull String errorMessage, @NonNull String regex) { + super(errorMessage); + pattern = Pattern.compile(regex); + } + + public RegexpValidator(@NonNull String errorMessage, @NonNull Pattern pattern) { + super(errorMessage); + this.pattern = pattern; + } + + @Override + public boolean isValid(@NonNull CharSequence text, boolean isEmpty) { + return pattern.matcher(text).matches(); + } +} diff --git a/app/src/main/java/com/project/survey/widget/util/Utils.java b/app/src/main/java/com/project/survey/widget/util/Utils.java new file mode 100644 index 0000000..fa10410 --- /dev/null +++ b/app/src/main/java/com/project/survey/widget/util/Utils.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.project.survey.widget.util; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LightingColorFilter; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RelativeLayout.LayoutParams; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +/** + * 工具类(不建议外部调用) + * + * @author xuexiang + * @since 2018/11/26 下午5:07 + */ +public final class Utils { + + private Utils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 得到设备屏幕的宽度 + */ + public static int getScreenWidth(Context context) { + return context.getResources().getDisplayMetrics().widthPixels; + } + + /** + * 得到设备屏幕的高度 + */ + public static int getScreenHeight(Context context) { + return context.getResources().getDisplayMetrics().heightPixels; + } + + private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height"; + + /** + * 计算状态栏高度 getStatusBarHeight + * + * @param context 上下文 + * @return 状态栏高度 + */ + public static int getStatusBarHeight(Context context) { + if (context == null) { + return getStatusBarHeight(); + } + return getInternalDimensionSize(context.getResources(), + STATUS_BAR_HEIGHT_RES_NAME); + } + + /** + * 计算状态栏高度 getStatusBarHeight + * + * @return 状态栏高度 + */ + public static int getStatusBarHeight() { + return getInternalDimensionSize(Resources.getSystem(), + STATUS_BAR_HEIGHT_RES_NAME); + } + + private static int getInternalDimensionSize(Resources res, String key) { + int result = 0; + int resourceId = res.getIdentifier(key, "dimen", "android"); + if (resourceId > 0) { + result = res.getDimensionPixelSize(resourceId); + } + return result; + } + + /** + * get ListView height according to every children + * + * @param view + * @return + */ + public static int getListViewHeightBasedOnChildren(ListView view) { + int height = getAbsListViewHeightBasedOnChildren(view); + ListAdapter adapter; + int adapterCount; + if (view != null && (adapter = view.getAdapter()) != null + && (adapterCount = adapter.getCount()) > 0) { + height += view.getDividerHeight() * (adapterCount - 1); + } + return height; + } + + /** + * get AbsListView height according to every children + * + * @param view + * @return + */ + public static int getAbsListViewHeightBasedOnChildren(AbsListView view) { + ListAdapter adapter; + if (view == null || (adapter = view.getAdapter()) == null) { + return 0; + } + + int height = 0; + for (int i = 0; i < adapter.getCount(); i++) { + View item = adapter.getView(i, null, view); + if (item instanceof ViewGroup) { + item.setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + } + item.measure(0, 0); + height += item.getMeasuredHeight(); + } + height += view.getPaddingTop() + view.getPaddingBottom(); + return height; + } + + /** + * View设备背景 + * + * @param context 上下文 + * @param view 控件 + * @param resId 资源id + */ + public static void setBackground(Context context, View view, int resId) { + if (view == null) { + return; + } + Bitmap bm = BitmapFactory.decodeResource(context.getResources(), resId); + BitmapDrawable bd = new BitmapDrawable(context.getResources(), bm); + view.setBackground(bd); + } + + /** + * 释放图片资源 + * + * @param view 控件 + */ + public static void recycleBackground(View view) { + Drawable d = view.getBackground(); + //别忘了把背景设为null,避免onDraw刷新背景时候出现used a recycled bitmap错误 + view.setBackgroundResource(0); + if (d != null && d instanceof BitmapDrawable) { + Bitmap bmp = ((BitmapDrawable) d).getBitmap(); + if (bmp != null && !bmp.isRecycled()) { + bmp.recycle(); + } + } + if (d != null) { + d.setCallback(null); + } + } + + /** + * 遍历View,清除所有ImageView的缓存 + * + * @param view + */ + public static void clearImageView(View view) { + if (view instanceof ViewGroup) { + ViewGroup parent = (ViewGroup) view; + int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + clearImageView(parent.getChildAt(i)); + } + } else if (view instanceof ImageView) { + clearImgMemory((ImageView) view); + } + } + + /** + * 清空图片的内存 + */ + public static void clearImgMemory(ImageView imageView) { + Drawable d = imageView.getDrawable(); + if (d != null && d instanceof BitmapDrawable) { + Bitmap bmp = ((BitmapDrawable) d).getBitmap(); + if (bmp != null && !bmp.isRecycled()) { + bmp.recycle(); + } + } + imageView.setImageBitmap(null); + if (d != null) { + d.setCallback(null); + } + } + + /** + * 放大缩小图片 + * + * @param bitmap 源Bitmap + * @param w 宽 + * @param h 高 + * @return 目标Bitmap + */ + public static Bitmap zoom(Bitmap bitmap, int w, int h) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + Matrix matrix = new Matrix(); + float scaleWidth = ((float) w / width); + float scaleHeight = ((float) h / height); + matrix.postScale(scaleWidth, scaleHeight); + return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); + } + + /** + * 安静关闭 IO + * + * @param closeables closeables + */ + public static void closeIOQuietly(final Closeable... closeables) { + if (closeables == null) { + return; + } + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Indicates if this file represents a file on the underlying file system. + * + * @param filePath 文件路径 + * @return 是否存在文件 + */ + public static boolean isFileExist(String filePath) { + if (TextUtils.isEmpty(filePath)) { + return false; + } + + File file = new File(filePath); + return (file.exists() && file.isFile()); + } + + /** + * 获取bitmap + * + * @param filePath 文件路径 + * @return bitmap + */ + public static Bitmap getBitmap(String filePath) { + if (!isFileExist(filePath)) { + return null; + } + return BitmapFactory.decodeFile(filePath); + } + + /** + * 检查是否为空指针 + * + * @param object + * @param hint + */ + public static void checkNull(Object object, String hint) { + if (null == object) { + throw new NullPointerException(hint); + } + } + + /** + * 检查是否为空指针 + * + * @param t + * @param message + */ + public static T checkNotNull(T t, String message) { + if (t == null) { + throw new NullPointerException(message); + } + return t; + } + + /** + * 旋转图片 + * + * @param angle 旋转角度 + * @param bitmap 要旋转的图片 + * @return 旋转后的图片 + */ + public static Bitmap rotate(Bitmap bitmap, int angle) { + Matrix matrix = new Matrix(); + matrix.postRotate(angle); + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), + bitmap.getHeight(), matrix, true); + } + + /** + * 将Drawable转化为Bitmap + * + * @param drawable Drawable + * @return Bitmap + */ + public static Bitmap getBitmapFromDrawable(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, drawable + .getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + return bitmap; + } + + /** + * 将Drawable转化为Bitmap + * + * @param drawable Drawable + * @return Bitmap + */ + public static Bitmap getBitmapFromDrawable(Drawable drawable, int color) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, drawable + .getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(color, PorterDuff.Mode.SRC_IN); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); + canvas = new Canvas(bitmap); + canvas.drawColor(color, PorterDuff.Mode.SRC_IN); + return bitmap; + } + + /** + * 获取应用的图标 + * + * @param context + * @return + */ + public static Drawable getAppIcon(Context context) { + try { + PackageManager pm = context.getPackageManager(); + ApplicationInfo info = pm.getApplicationInfo(context.getPackageName(), 0); + return info.loadIcon(pm); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 支持?attrs属性 http://stackoverflow.com/questions/27986204 :As mentioned here on API < 21 you can't use attrs to color in xml drawable. + * + * @return 支持?attrs属性 + */ + public static boolean isSupportColorAttrs() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + public static boolean isLight(int color) { + return Math.sqrt( + Color.red(color) * Color.red(color) * .241 + + Color.green(color) * Color.green(color) * .691 + + Color.blue(color) * Color.blue(color) * .068) > 130; + } + + public static boolean isNullOrEmpty(@Nullable CharSequence string) { + return string == null || string.length() == 0; + } + + /** + * 获取数值的位数,例如9返回1,99返回2,999返回3 + * + * @param number 要计算位数的数值,必须>0 + * @return 数值的位数,若传的参数小于等于0,则返回0 + */ + public static int getNumberDigits(int number) { + if (number <= 0) { + return 0; + } + return (int) (Math.log10(number) + 1); + } + + /** + * 设置Drawable的颜色 + * 这里不对Drawable进行mutate(),会影响到所有用到这个Drawable的地方,如果要避免,请先自行mutate() + */ + public static ColorFilter setDrawableTintColor(Drawable drawable, @ColorInt int tintColor) { + LightingColorFilter colorFilter = new LightingColorFilter(Color.argb(255, 0, 0, 0), tintColor); + if (drawable != null) { + drawable.setColorFilter(colorFilter); + } + return colorFilter; + } + + public static Application getApplicationByReflect() { + try { + @SuppressLint("PrivateApi") + Class activityThread = Class.forName("android.app.ActivityThread"); + Object thread = activityThread.getMethod("currentActivityThread").invoke(null); + Object app = activityThread.getMethod("getApplication").invoke(thread); + if (app == null) { + throw new NullPointerException("you should init first"); + } + return (Application) app; + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + throw new NullPointerException("you should init first"); + } + + /** + * 类型强转 + * + * @param object 需要强转的对象 + * @param clazz 需要强转的类型 + * @param + * @return 类型强转结果 + */ + public static T cast(final Object object, Class clazz) { + return clazz != null && clazz.isInstance(object) ? (T) object : null; + } + +} diff --git a/app/src/main/res/drawable/about.xml b/app/src/main/res/drawable/about.xml new file mode 100644 index 0000000..78b5cf0 --- /dev/null +++ b/app/src/main/res/drawable/about.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/arrow_right.xml b/app/src/main/res/drawable/arrow_right.xml new file mode 100644 index 0000000..1108b51 --- /dev/null +++ b/app/src/main/res/drawable/arrow_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/bg_btn_login.xml b/app/src/main/res/drawable/bg_btn_login.xml new file mode 100644 index 0000000..ddbf5b9 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_login.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_pwd_sc.xml b/app/src/main/res/drawable/bg_pwd_sc.xml new file mode 100644 index 0000000..e3cab8d --- /dev/null +++ b/app/src/main/res/drawable/bg_pwd_sc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_top_blue.xml b/app/src/main/res/drawable/bg_top_blue.xml index 56d44a6..059ace1 100644 --- a/app/src/main/res/drawable/bg_top_blue.xml +++ b/app/src/main/res/drawable/bg_top_blue.xml @@ -4,5 +4,5 @@ + android:startColor="@color/colorPrimary" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/change_pwd.xml b/app/src/main/res/drawable/change_pwd.xml new file mode 100644 index 0000000..451b75d --- /dev/null +++ b/app/src/main/res/drawable/change_pwd.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/login_error.xml b/app/src/main/res/drawable/login_error.xml new file mode 100644 index 0000000..edef016 --- /dev/null +++ b/app/src/main/res/drawable/login_error.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/pet_icon_visibility_24dp.xml b/app/src/main/res/drawable/pet_icon_visibility_24dp.xml new file mode 100644 index 0000000..f152113 --- /dev/null +++ b/app/src/main/res/drawable/pet_icon_visibility_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/pet_icon_visibility_off_24dp.xml b/app/src/main/res/drawable/pet_icon_visibility_off_24dp.xml new file mode 100644 index 0000000..5d7e38b --- /dev/null +++ b/app/src/main/res/drawable/pet_icon_visibility_off_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/pwd_hide.xml b/app/src/main/res/drawable/pwd_hide.xml new file mode 100644 index 0000000..921dbc2 --- /dev/null +++ b/app/src/main/res/drawable/pwd_hide.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/pwd_show.xml b/app/src/main/res/drawable/pwd_show.xml new file mode 100644 index 0000000..756ac6f --- /dev/null +++ b/app/src/main/res/drawable/pwd_show.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/switch_project.xml b/app/src/main/res/drawable/switch_project.xml new file mode 100644 index 0000000..c3c9fdc --- /dev/null +++ b/app/src/main/res/drawable/switch_project.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/xui_ic_default_clear_btn.xml b/app/src/main/res/drawable/xui_ic_default_clear_btn.xml new file mode 100644 index 0000000..966fbb6 --- /dev/null +++ b/app/src/main/res/drawable/xui_ic_default_clear_btn.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..be99bca --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +