diff --git a/buildSrc/src/main/kotlin/com/quyunshuo/androidbaseframemvvm/buildsrc/DependencyConfig.kt b/buildSrc/src/main/kotlin/com/quyunshuo/androidbaseframemvvm/buildsrc/DependencyConfig.kt index cb73a8a..42bb60a 100644 --- a/buildSrc/src/main/kotlin/com/quyunshuo/androidbaseframemvvm/buildsrc/DependencyConfig.kt +++ b/buildSrc/src/main/kotlin/com/quyunshuo/androidbaseframemvvm/buildsrc/DependencyConfig.kt @@ -50,7 +50,6 @@ object DependencyConfig { const val ARoute = "1.5.1" // 阿里路由 const val ARouteCompiler = "1.5.1" // 阿里路由 APT const val RecyclerViewAdapter = "3.0.4" // RecyclerViewAdapter - const val StatusBar = "1.5.1" // 状态栏 const val EventBus = "3.2.0" // 事件总线 const val PermissionX = "1.4.0" // 权限申请 const val LeakCanary = "2.7" // 检测内存泄漏 @@ -146,7 +145,6 @@ object DependencyConfig { const val ARouteCompiler = "com.alibaba:arouter-compiler:${Version.ARouteCompiler}" const val RecyclerViewAdapter = "com.github.CymChad:BaseRecyclerViewAdapterHelper:${Version.RecyclerViewAdapter}" - const val StatusBar = "com.jaeger.statusbarutil:library:${Version.StatusBar}" const val EventBus = "org.greenrobot:eventbus:${Version.EventBus}" const val EventBusAPT = "org.greenrobot:eventbus-annotation-processor:${Version.EventBus}" const val PermissionX = "com.permissionx.guolindev:permissionx:${Version.PermissionX}" diff --git a/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/BarUtils.java b/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/BarUtils.java new file mode 100644 index 0000000..693243e --- /dev/null +++ b/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/BarUtils.java @@ -0,0 +1,674 @@ +package com.quyunshuo.androidbaseframemvvm.base.utils; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Point; +import android.os.Build; +import android.util.TypedValue; +import android.view.Display; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; + +import java.lang.reflect.Method; + +import static android.Manifest.permission.EXPAND_STATUS_BAR; + +import com.quyunshuo.androidbaseframemvvm.base.BaseApplication; + +/** + * 各种栏的工具类 + *

+ * getStatusBarHeight : 获取状态栏高度(px) + * setStatusBarVisibility : 设置状态栏是否可见 + * isStatusBarVisible : 判断状态栏是否可见 + * setStatusBarLightMode : 设置状态栏是否为浅色模式 + * isStatusBarLightMode : 判断状态栏是否为浅色模式 + * addMarginTopEqualStatusBarHeight : 为 view 增加 MarginTop 为状态栏高度 + * subtractMarginTopEqualStatusBarHeight: 为 view 减少 MarginTop 为状态栏高度 + * setStatusBarColor : 设置状态栏颜色 + * setStatusBarColor4Drawer : 为 DrawerLayout 设置状态栏颜色 + * transparentStatusBar : 透明状态栏 + * getActionBarHeight : 获取 ActionBar 高度 + * setNotificationBarVisibility : 设置通知栏是否可见 + * getNavBarHeight : 获取导航栏高度 + * setNavBarVisibility : 设置导航栏是否可见 + * isNavBarVisible : 判断导航栏是否可见 + * setNavBarColor : 设置导航栏颜色 + * getNavBarColor : 获取导航栏颜色 + * isSupportNavBar : 判断是否支持导航栏 + * setNavBarLightMode : 设置导航栏是否为浅色模式 + * isNavBarLightMode : 判断导航栏是否为浅色模式 + * + * @author Qu Yunshuo + * @since 2021/7/15 10:42 上午 + */ +public final class BarUtils { + + /////////////////////////////////////////////////////////////////////////// + // status bar + /////////////////////////////////////////////////////////////////////////// + + private static final String TAG_STATUS_BAR = "TAG_STATUS_BAR"; + private static final String TAG_OFFSET = "TAG_OFFSET"; + private static final int KEY_OFFSET = -123; + + private BarUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * Return the status bar's height. + * + * @return the status bar's height + */ + public static int getStatusBarHeight() { + Resources resources = BaseApplication.context.getResources(); + int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); + return resources.getDimensionPixelSize(resourceId); + } + + /** + * Set the status bar's visibility. + * + * @param activity The activity. + * @param isVisible True to set status bar visible, false otherwise. + */ + public static void setStatusBarVisibility(@NonNull final Activity activity, + final boolean isVisible) { + setStatusBarVisibility(activity.getWindow(), isVisible); + } + + /** + * Set the status bar's visibility. + * + * @param window The window. + * @param isVisible True to set status bar visible, false otherwise. + */ + public static void setStatusBarVisibility(@NonNull final Window window, + final boolean isVisible) { + if (isVisible) { + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + showStatusBarView(window); + addMarginTopEqualStatusBarHeight(window); + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + hideStatusBarView(window); + subtractMarginTopEqualStatusBarHeight(window); + } + } + + /** + * Return whether the status bar is visible. + * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isStatusBarVisible(@NonNull final Activity activity) { + int flags = activity.getWindow().getAttributes().flags; + return (flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0; + } + + /** + * Set the status bar's light mode. + * + * @param activity The activity. + * @param isLightMode True to set status bar light mode, false otherwise. + */ + public static void setStatusBarLightMode(@NonNull final Activity activity, + final boolean isLightMode) { + setStatusBarLightMode(activity.getWindow(), isLightMode); + } + + /** + * Set the status bar's light mode. + * + * @param window The window. + * @param isLightMode True to set status bar light mode, false otherwise. + */ + public static void setStatusBarLightMode(@NonNull final Window window, + final boolean isLightMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + if (isLightMode) { + vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } else { + vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + decorView.setSystemUiVisibility(vis); + } + } + + /** + * Is the status bar light mode. + * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isStatusBarLightMode(@NonNull final Activity activity) { + return isStatusBarLightMode(activity.getWindow()); + } + + /** + * Is the status bar light mode. + * + * @param window The window. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isStatusBarLightMode(@NonNull final Window window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + return (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0; + } + return false; + } + + /** + * Add the top margin size equals status bar's height for view. + * + * @param view The view. + */ + public static void addMarginTopEqualStatusBarHeight(@NonNull View view) { + + view.setTag(TAG_OFFSET); + Object haveSetOffset = view.getTag(KEY_OFFSET); + if (haveSetOffset != null && (Boolean) haveSetOffset) return; + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, + layoutParams.topMargin + getStatusBarHeight(), + layoutParams.rightMargin, + layoutParams.bottomMargin); + view.setTag(KEY_OFFSET, true); + } + + /** + * Subtract the top margin size equals status bar's height for view. + * + * @param view The view. + */ + public static void subtractMarginTopEqualStatusBarHeight(@NonNull View view) { + + Object haveSetOffset = view.getTag(KEY_OFFSET); + if (haveSetOffset == null || !(Boolean) haveSetOffset) return; + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, + layoutParams.topMargin - getStatusBarHeight(), + layoutParams.rightMargin, + layoutParams.bottomMargin); + view.setTag(KEY_OFFSET, false); + } + + private static void addMarginTopEqualStatusBarHeight(@NonNull final Window window) { + View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET); + if (withTag == null) return; + addMarginTopEqualStatusBarHeight(withTag); + } + + private static void subtractMarginTopEqualStatusBarHeight(@NonNull final Window window) { + View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET); + if (withTag == null) return; + subtractMarginTopEqualStatusBarHeight(withTag); + } + + /** + * Set the status bar's color. + * + * @param activity The activity. + * @param color The status bar's color. + */ + public static View setStatusBarColor(@NonNull final Activity activity, + @ColorInt final int color) { + return setStatusBarColor(activity, color, false); + } + + /** + * Set the status bar's color. + * + * @param activity The activity. + * @param color The status bar's color. + * @param isDecor True to add fake status bar in DecorView, + * false to add fake status bar in ContentView. + */ + public static View setStatusBarColor(@NonNull final Activity activity, + @ColorInt final int color, + final boolean isDecor) { + transparentStatusBar(activity); + return applyStatusBarColor(activity, color, isDecor); + } + + + /** + * Set the status bar's color. + * + * @param window The window. + * @param color The status bar's color. + */ + public static View setStatusBarColor(@NonNull final Window window, + @ColorInt final int color) { + return setStatusBarColor(window, color, false); + } + + /** + * Set the status bar's color. + * + * @param window The window. + * @param color The status bar's color. + * @param isDecor True to add fake status bar in DecorView, + * false to add fake status bar in ContentView. + */ + public static View setStatusBarColor(@NonNull final Window window, + @ColorInt final int color, + final boolean isDecor) { + transparentStatusBar(window); + return applyStatusBarColor(window, color, isDecor); + } + + /** + * Set the status bar's color. + * + * @param fakeStatusBar The fake status bar view. + * @param color The status bar's color. + */ + public static void setStatusBarColor(Activity activity, @NonNull final View fakeStatusBar, + @ColorInt final int color) { + if (activity == null) return; + transparentStatusBar(activity); + fakeStatusBar.setVisibility(View.VISIBLE); + ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = getStatusBarHeight(); + fakeStatusBar.setBackgroundColor(color); + } + + /** + * Set the custom status bar. + * + * @param fakeStatusBar The fake status bar view. + */ + public static void setStatusBarCustom(Activity activity, @NonNull final View fakeStatusBar) { + if (activity == null) return; + transparentStatusBar(activity); + fakeStatusBar.setVisibility(View.VISIBLE); + ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams(); + if (layoutParams == null) { + layoutParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + getStatusBarHeight() + ); + fakeStatusBar.setLayoutParams(layoutParams); + } else { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = getStatusBarHeight(); + } + } + + private static View applyStatusBarColor(@NonNull final Activity activity, + final int color, + boolean isDecor) { + return applyStatusBarColor(activity.getWindow(), color, isDecor); + } + + private static View applyStatusBarColor(@NonNull final Window window, + final int color, + boolean isDecor) { + ViewGroup parent = isDecor ? + (ViewGroup) window.getDecorView() : + (ViewGroup) window.findViewById(android.R.id.content); + View fakeStatusBarView = parent.findViewWithTag(TAG_STATUS_BAR); + if (fakeStatusBarView != null) { + if (fakeStatusBarView.getVisibility() == View.GONE) { + fakeStatusBarView.setVisibility(View.VISIBLE); + } + fakeStatusBarView.setBackgroundColor(color); + } else { + fakeStatusBarView = createStatusBarView(window.getContext(), color); + parent.addView(fakeStatusBarView); + } + return fakeStatusBarView; + } + + private static void hideStatusBarView(@NonNull final Activity activity) { + hideStatusBarView(activity.getWindow()); + } + + private static void hideStatusBarView(@NonNull final Window window) { + ViewGroup decorView = (ViewGroup) window.getDecorView(); + View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR); + if (fakeStatusBarView == null) return; + fakeStatusBarView.setVisibility(View.GONE); + } + + private static void showStatusBarView(@NonNull final Window window) { + ViewGroup decorView = (ViewGroup) window.getDecorView(); + View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR); + if (fakeStatusBarView == null) return; + fakeStatusBarView.setVisibility(View.VISIBLE); + } + + private static View createStatusBarView(@NonNull final Context context, + final int color) { + View statusBarView = new View(context); + statusBarView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight())); + statusBarView.setBackgroundColor(color); + statusBarView.setTag(TAG_STATUS_BAR); + return statusBarView; + } + + public static void transparentStatusBar(@NonNull final Activity activity) { + transparentStatusBar(activity.getWindow()); + } + + public static void transparentStatusBar(@NonNull final Window window) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + int vis = window.getDecorView().getSystemUiVisibility(); + window.getDecorView().setSystemUiVisibility(option | vis); + window.setStatusBarColor(Color.TRANSPARENT); + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + /////////////////////////////////////////////////////////////////////////// + // action bar + /////////////////////////////////////////////////////////////////////////// + + /** + * Return the action bar's height. + * + * @return the action bar's height + */ + public static int getActionBarHeight() { + TypedValue tv = new TypedValue(); + if (BaseApplication.context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { + return TypedValue.complexToDimensionPixelSize( + tv.data, BaseApplication.context.getResources().getDisplayMetrics() + ); + } + return 0; + } + + /////////////////////////////////////////////////////////////////////////// + // notification bar + /////////////////////////////////////////////////////////////////////////// + + /** + * Set the notification bar's visibility. + *

Must hold {@code }

+ * + * @param isVisible True to set notification bar visible, false otherwise. + */ + @RequiresPermission(EXPAND_STATUS_BAR) + public static void setNotificationBarVisibility(final boolean isVisible) { + String methodName; + if (isVisible) { + methodName = (Build.VERSION.SDK_INT <= 16) ? "expand" : "expandNotificationsPanel"; + } else { + methodName = (Build.VERSION.SDK_INT <= 16) ? "collapse" : "collapsePanels"; + } + invokePanels(methodName); + } + + private static void invokePanels(final String methodName) { + try { + @SuppressLint("WrongConstant") + Object service = BaseApplication.context.getSystemService("statusbar"); + @SuppressLint("PrivateApi") + Class statusBarManager = Class.forName("android.app.StatusBarManager"); + Method expand = statusBarManager.getMethod(methodName); + expand.invoke(service); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /////////////////////////////////////////////////////////////////////////// + // navigation bar + /////////////////////////////////////////////////////////////////////////// + + /** + * Return the navigation bar's height. + * + * @return the navigation bar's height + */ + public static int getNavBarHeight() { + Resources res = BaseApplication.context.getResources(); + int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId != 0) { + return res.getDimensionPixelSize(resourceId); + } else { + return 0; + } + } + + /** + * Set the navigation bar's visibility. + * + * @param activity The activity. + * @param isVisible True to set navigation bar visible, false otherwise. + */ + public static void setNavBarVisibility(@NonNull final Activity activity, boolean isVisible) { + + setNavBarVisibility(activity.getWindow(), isVisible); + + } + + /** + * Set the navigation bar's visibility. + * + * @param window The window. + * @param isVisible True to set navigation bar visible, false otherwise. + */ + public static void setNavBarVisibility(@NonNull final Window window, boolean isVisible) { + + final ViewGroup decorView = (ViewGroup) window.getDecorView(); + for (int i = 0, count = decorView.getChildCount(); i < count; i++) { + final View child = decorView.getChildAt(i); + final int id = child.getId(); + if (id != View.NO_ID) { + String resourceEntryName = getResNameById(id); + if ("navigationBarBackground".equals(resourceEntryName)) { + child.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); + } + } + } + final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + if (isVisible) { + decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~uiOptions); + } else { + decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | uiOptions); + } + } + + /** + * Return whether the navigation bar visible. + *

Call it in onWindowFocusChanged will get right result.

+ * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarVisible(@NonNull final Activity activity) { + return isNavBarVisible(activity.getWindow()); + } + + /** + * Return whether the navigation bar visible. + *

Call it in onWindowFocusChanged will get right result.

+ * + * @param window The window. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarVisible(@NonNull final Window window) { + boolean isVisible = false; + ViewGroup decorView = (ViewGroup) window.getDecorView(); + for (int i = 0, count = decorView.getChildCount(); i < count; i++) { + final View child = decorView.getChildAt(i); + final int id = child.getId(); + if (id != View.NO_ID) { + String resourceEntryName = getResNameById(id); + if ("navigationBarBackground".equals(resourceEntryName) + && child.getVisibility() == View.VISIBLE) { + isVisible = true; + break; + } + } + } + if (isVisible) { + int visibility = decorView.getSystemUiVisibility(); + isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + } + + return isVisible; + } + + private static String getResNameById(int id) { + try { + return BaseApplication.context.getResources().getResourceEntryName(id); + } catch (Exception ignore) { + return ""; + } + } + + /** + * Set the navigation bar's color. + * + * @param activity The activity. + * @param color The navigation bar's color. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static void setNavBarColor(@NonNull final Activity activity, @ColorInt final int color) { + setNavBarColor(activity.getWindow(), color); + } + + /** + * Set the navigation bar's color. + * + * @param window The window. + * @param color The navigation bar's color. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static void setNavBarColor(@NonNull final Window window, @ColorInt final int color) { + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setNavigationBarColor(color); + } + + /** + * Return the color of navigation bar. + * + * @param activity The activity. + * @return the color of navigation bar + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static int getNavBarColor(@NonNull final Activity activity) { + return getNavBarColor(activity.getWindow()); + } + + /** + * Return the color of navigation bar. + * + * @param window The window. + * @return the color of navigation bar + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static int getNavBarColor(@NonNull final Window window) { + return window.getNavigationBarColor(); + } + + /** + * Return whether the navigation bar visible. + * + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isSupportNavBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + WindowManager wm = (WindowManager) BaseApplication.context.getSystemService(Context.WINDOW_SERVICE); + if (wm == null) return false; + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + Point realSize = new Point(); + display.getSize(size); + display.getRealSize(realSize); + return realSize.y != size.y || realSize.x != size.x; + } + boolean menu = ViewConfiguration.get(BaseApplication.context).hasPermanentMenuKey(); + boolean back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + return !menu && !back; + } + + /** + * Set the nav bar's light mode. + * + * @param activity The activity. + * @param isLightMode True to set nav bar light mode, false otherwise. + */ + public static void setNavBarLightMode(@NonNull final Activity activity, + final boolean isLightMode) { + setNavBarLightMode(activity.getWindow(), isLightMode); + } + + /** + * Set the nav bar's light mode. + * + * @param window The window. + * @param isLightMode True to set nav bar light mode, false otherwise. + */ + public static void setNavBarLightMode(@NonNull final Window window, + final boolean isLightMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + if (isLightMode) { + vis |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } else { + vis &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + decorView.setSystemUiVisibility(vis); + } + } + + /** + * Is the nav bar light mode. + * + * @param activity The activity. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarLightMode(@NonNull final Activity activity) { + return isNavBarLightMode(activity.getWindow()); + } + + /** + * Is the nav bar light mode. + * + * @param window The window. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isNavBarLightMode(@NonNull final Window window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View decorView = window.getDecorView(); + int vis = decorView.getSystemUiVisibility(); + return (vis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0; + } + return false; + } +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/CoilGIFImageLoader.kt b/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/CoilGIFImageLoader.kt new file mode 100644 index 0000000..9b80c34 --- /dev/null +++ b/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/CoilGIFImageLoader.kt @@ -0,0 +1,26 @@ +package com.quyunshuo.androidbaseframemvvm.base.utils + +import android.os.Build.VERSION.SDK_INT +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import com.quyunshuo.androidbaseframemvvm.base.BaseApplication + +/** + * 用于加载 Gif 的 Coil ImageLoader + * + * @author Qu Yunshuo + * @since 2021/9/6 4:26 下午 + */ +object CoilGIFImageLoader { + + val imageLoader = ImageLoader.Builder(BaseApplication.context) + .componentRegistry { + if (SDK_INT >= 28) { + add(ImageDecoderDecoder(BaseApplication.context)) + } else { + add(GifDecoder()) + } + } + .build() +} \ No newline at end of file diff --git a/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/SpannableStringUtils.java b/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/SpannableStringUtils.java new file mode 100644 index 0000000..136b792 --- /dev/null +++ b/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/SpannableStringUtils.java @@ -0,0 +1,1497 @@ +package com.quyunshuo.androidbaseframemvvm.base.utils; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Shader; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.text.Layout; +import android.text.Layout.Alignment; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.method.LinkMovementMethod; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.AlignmentSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.CharacterStyle; +import android.text.style.ClickableSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.LineHeightSpan; +import android.text.style.MaskFilterSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.ReplacementSpan; +import android.text.style.ScaleXSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.TypefaceSpan; +import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; +import android.text.style.UpdateAppearance; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.FloatRange; +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import java.io.InputStream; +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; + +import static android.graphics.BlurMaskFilter.Blur; + +import com.quyunshuo.androidbaseframemvvm.base.BaseApplication; + +/** + * 字符串相关 + * 相关方法: + *
+ *   with()              : 设置控件
+ *   setFlag()           : 设置标识
+ *   setForegroundColor(): 设置前景色
+ *   setBackgroundColor(): 设置背景色
+ *   setLineHeight()     : 设置行高
+ *   setQuoteColor()     : 设置引用线的颜色
+ *   setLeadingMargin()  : 设置缩进
+ *   setBullet()         : 设置列表标记
+ *   setFontSize()       : 设置字体尺寸
+ *   setFontProportion() : 设置字体比例
+ *   setFontXProportion(): 设置字体横向比例
+ *   setStrikethrough()  : 设置删除线
+ *   setUnderline()      : 设置下划线
+ *   setSuperscript()    : 设置上标
+ *   setSubscript()      : 设置下标
+ *   setBold()           : 设置粗体
+ *   setItalic()         : 设置斜体
+ *   setBoldItalic()     : 设置粗斜体
+ *   setFontFamily()     : 设置字体系列
+ *   setTypeface()       : 设置字体
+ *   setAlign()          : 设置对齐
+ *   setClickSpan()      : 设置点击事件
+ *   setUrl()            : 设置超链接
+ *   setBlur()           : 设置模糊
+ *   setShader()         : 设置着色器
+ *   setShadow()         : 设置阴影
+ *   setSpans()          : 设置样式
+ *   append()            : 追加样式字符串
+ *   appendLine()        : 追加一行样式字符串
+ *   appendImage()       : 追加图片
+ *   appendSpace()       : 追加空白
+ *   create()            : 创建样式字符串
+ * 
+ * 示例: + *
+ *   SpanUtils.with(TextView)
+ *       .append("String")
+ *       .append("String")
+ *       .setForegroundColor(ContextCompat.getColor(getContext(), R.color.color666666))
+ *       .setFontSize(14, true)
+ *       .create();
+ * 
+ * + * @author Qu Yunshuo + * @since 2020/9/17 + */ +public final class SpannableStringUtils { + + private static final int COLOR_DEFAULT = 0xFEFFFFFF; + + public static final int ALIGN_BOTTOM = 0; + public static final int ALIGN_BASELINE = 1; + public static final int ALIGN_CENTER = 2; + public static final int ALIGN_TOP = 3; + + @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP}) + @Retention(RetentionPolicy.SOURCE) + public @interface Align { + } + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + public static SpannableStringUtils with(final TextView textView) { + return new SpannableStringUtils(textView); + } + + private TextView mTextView; + private CharSequence mText; + private int flag; + private int foregroundColor; + private int backgroundColor; + private int lineHeight; + private int alignLine; + private int quoteColor; + private int stripeWidth; + private int quoteGapWidth; + private int first; + private int rest; + private int bulletColor; + private int bulletRadius; + private int bulletGapWidth; + private int fontSize; + private boolean fontSizeIsDp; + private float proportion; + private float xProportion; + private boolean isStrikethrough; + private boolean isUnderline; + private boolean isSuperscript; + private boolean isSubscript; + private boolean isBold; + private boolean isItalic; + private boolean isBoldItalic; + private String fontFamily; + private Typeface typeface; + private Alignment alignment; + private int verticalAlign; + private ClickableSpan clickSpan; + private String url; + private float blurRadius; + private Blur style; + private Shader shader; + private float shadowRadius; + private float shadowDx; + private float shadowDy; + private int shadowColor; + private Object[] spans; + + private Bitmap imageBitmap; + private Drawable imageDrawable; + private Uri imageUri; + private int imageResourceId; + private int alignImage; + + private int spaceSize; + private int spaceColor; + + private SerializableSpannableStringBuilder mBuilder; + private boolean isCreated; + + private int mType; + private final int mTypeCharSequence = 0; + private final int mTypeImage = 1; + private final int mTypeSpace = 2; + + private SpannableStringUtils(TextView textView) { + this(); + mTextView = textView; + } + + public SpannableStringUtils() { + mBuilder = new SerializableSpannableStringBuilder(); + mText = ""; + mType = -1; + setDefault(); + } + + private void setDefault() { + flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + foregroundColor = COLOR_DEFAULT; + backgroundColor = COLOR_DEFAULT; + lineHeight = -1; + quoteColor = COLOR_DEFAULT; + first = -1; + bulletColor = COLOR_DEFAULT; + fontSize = -1; + proportion = -1; + xProportion = -1; + isStrikethrough = false; + isUnderline = false; + isSuperscript = false; + isSubscript = false; + isBold = false; + isItalic = false; + isBoldItalic = false; + fontFamily = null; + typeface = null; + alignment = null; + verticalAlign = -1; + clickSpan = null; + url = null; + blurRadius = -1; + shader = null; + shadowRadius = -1; + spans = null; + + imageBitmap = null; + imageDrawable = null; + imageUri = null; + imageResourceId = -1; + + spaceSize = -1; + } + + /** + * Set the span of flag. + * + * @param flag The flag. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setFlag(final int flag) { + this.flag = flag; + return this; + } + + /** + * Set the span of foreground's color. + * + * @param color The color of foreground + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setForegroundColor(@ColorInt final int color) { + this.foregroundColor = color; + return this; + } + + /** + * Set the span of background's color. + * + * @param color The color of background + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setBackgroundColor(@ColorInt final int color) { + this.backgroundColor = color; + return this; + } + + /** + * Set the span of line height. + * + * @param lineHeight The line height, in pixel. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setLineHeight(@IntRange(from = 0) final int lineHeight) { + return setLineHeight(lineHeight, ALIGN_CENTER); + } + + /** + * Set the span of line height. + * + * @param lineHeight The line height, in pixel. + * @param align The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setLineHeight(@IntRange(from = 0) final int lineHeight, + @Align final int align) { + this.lineHeight = lineHeight; + this.alignLine = align; + return this; + } + + /** + * Set the span of quote's color. + * + * @param color The color of quote + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setQuoteColor(@ColorInt final int color) { + return setQuoteColor(color, 2, 2); + } + + /** + * Set the span of quote's color. + * + * @param color The color of quote. + * @param stripeWidth The width of stripe, in pixel. + * @param gapWidth The width of gap, in pixel. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setQuoteColor(@ColorInt final int color, + @IntRange(from = 1) final int stripeWidth, + @IntRange(from = 0) final int gapWidth) { + this.quoteColor = color; + this.stripeWidth = stripeWidth; + this.quoteGapWidth = gapWidth; + return this; + } + + /** + * Set the span of leading margin. + * + * @param first The indent for the first line of the paragraph. + * @param rest The indent for the remaining lines of the paragraph. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setLeadingMargin(@IntRange(from = 0) final int first, + @IntRange(from = 0) final int rest) { + this.first = first; + this.rest = rest; + return this; + } + + /** + * Set the span of bullet. + * + * @param gapWidth The width of gap, in pixel. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setBullet(@IntRange(from = 0) final int gapWidth) { + return setBullet(0, 3, gapWidth); + } + + /** + * Set the span of bullet. + * + * @param color The color of bullet. + * @param radius The radius of bullet, in pixel. + * @param gapWidth The width of gap, in pixel. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setBullet(@ColorInt final int color, + @IntRange(from = 0) final int radius, + @IntRange(from = 0) final int gapWidth) { + this.bulletColor = color; + this.bulletRadius = radius; + this.bulletGapWidth = gapWidth; + return this; + } + + /** + * Set the span of font's size. + * + * @param size The size of font. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setFontSize(@IntRange(from = 0) final int size) { + return setFontSize(size, false); + } + + /** + * Set the span of size of font. + * + * @param size The size of font. + * @param isSp True to use sp, false to use pixel. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) { + this.fontSize = size; + this.fontSizeIsDp = isSp; + return this; + } + + /** + * Set the span of proportion of font. + * + * @param proportion The proportion of font. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setFontProportion(final float proportion) { + this.proportion = proportion; + return this; + } + + /** + * Set the span of transverse proportion of font. + * + * @param proportion The transverse proportion of font. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setFontXProportion(final float proportion) { + this.xProportion = proportion; + return this; + } + + /** + * Set the span of strikethrough. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setStrikethrough() { + this.isStrikethrough = true; + return this; + } + + /** + * Set the span of underline. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setUnderline() { + this.isUnderline = true; + return this; + } + + /** + * Set the span of superscript. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setSuperscript() { + this.isSuperscript = true; + return this; + } + + /** + * Set the span of subscript. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setSubscript() { + this.isSubscript = true; + return this; + } + + /** + * Set the span of bold. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setBold() { + isBold = true; + return this; + } + + /** + * Set the span of italic. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setItalic() { + isItalic = true; + return this; + } + + /** + * Set the span of bold italic. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setBoldItalic() { + isBoldItalic = true; + return this; + } + + /** + * Set the span of font family. + * + * @param fontFamily The font family. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setFontFamily(@NonNull final String fontFamily) { + this.fontFamily = fontFamily; + return this; + } + + /** + * Set the span of typeface. + * + * @param typeface The typeface. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setTypeface(@NonNull final Typeface typeface) { + this.typeface = typeface; + return this; + } + + /** + * Set the span of horizontal alignment. + * + * @param alignment The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setHorizontalAlign(@NonNull final Alignment alignment) { + this.alignment = alignment; + return this; + } + + /** + * Set the span of vertical alignment. + * + * @param align The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setVerticalAlign(@Align final int align) { + this.verticalAlign = align; + return this; + } + + /** + * Set the span of click. + *

Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}

+ * + * @param clickSpan The span of click. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setClickSpan(@NonNull final ClickableSpan clickSpan) { + if (mTextView != null && mTextView.getMovementMethod() == null) { + mTextView.setMovementMethod(LinkMovementMethod.getInstance()); + } + this.clickSpan = clickSpan; + return this; + } + + /** + * Set the span of click. + *

Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}

+ * + * @param color The color of click span. + * @param underlineText True to support underline, false otherwise. + * @param listener The listener of click span. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setClickSpan(@ColorInt final int color, + final boolean underlineText, + final View.OnClickListener listener) { + if (mTextView != null && mTextView.getMovementMethod() == null) { + mTextView.setMovementMethod(LinkMovementMethod.getInstance()); + } + this.clickSpan = new ClickableSpan() { + + @Override + public void updateDrawState(@NonNull TextPaint paint) { + paint.setColor(color); + paint.setUnderlineText(underlineText); + } + + @Override + public void onClick(@NonNull View widget) { + if (listener != null) { + listener.onClick(widget); + } + } + }; + return this; + } + + /** + * Set the span of url. + *

Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}

+ * + * @param url The url. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setUrl(@NonNull final String url) { + if (mTextView != null && mTextView.getMovementMethod() == null) { + mTextView.setMovementMethod(LinkMovementMethod.getInstance()); + } + this.url = url; + return this; + } + + /** + * Set the span of blur. + * + * @param radius The radius of blur. + * @param style The style. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius, + final Blur style) { + this.blurRadius = radius; + this.style = style; + return this; + } + + /** + * Set the span of shader. + * + * @param shader The shader. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setShader(@NonNull final Shader shader) { + this.shader = shader; + return this; + } + + /** + * Set the span of shadow. + * + * @param radius The radius of shadow. + * @param dx X-axis offset, in pixel. + * @param dy Y-axis offset, in pixel. + * @param shadowColor The color of shadow. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius, + final float dx, + final float dy, + final int shadowColor) { + this.shadowRadius = radius; + this.shadowDx = dx; + this.shadowDy = dy; + this.shadowColor = shadowColor; + return this; + } + + + /** + * Set the spans. + * + * @param spans The spans. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils setSpans(@NonNull final Object... spans) { + if (spans.length > 0) { + this.spans = spans; + } + return this; + } + + /** + * Append the text text. + * + * @param text The text. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils append(@NonNull final CharSequence text) { + apply(mTypeCharSequence); + mText = text; + return this; + } + + /** + * Append one line. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendLine() { + apply(mTypeCharSequence); + mText = LINE_SEPARATOR; + return this; + } + + /** + * Append text and one line. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendLine(@NonNull final CharSequence text) { + apply(mTypeCharSequence); + mText = text + LINE_SEPARATOR; + return this; + } + + /** + * Append one image. + * + * @param bitmap The bitmap of image. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@NonNull final Bitmap bitmap) { + return appendImage(bitmap, ALIGN_BOTTOM); + } + + /** + * Append one image. + * + * @param bitmap The bitmap. + * @param align The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) { + apply(mTypeImage); + this.imageBitmap = bitmap; + this.alignImage = align; + return this; + } + + /** + * Append one image. + * + * @param drawable The drawable of image. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@NonNull final Drawable drawable) { + return appendImage(drawable, ALIGN_BOTTOM); + } + + /** + * Append one image. + * + * @param drawable The drawable of image. + * @param align The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@NonNull final Drawable drawable, @Align final int align) { + apply(mTypeImage); + this.imageDrawable = drawable; + this.alignImage = align; + return this; + } + + /** + * Append one image. + * + * @param uri The uri of image. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@NonNull final Uri uri) { + return appendImage(uri, ALIGN_BOTTOM); + } + + /** + * Append one image. + * + * @param uri The uri of image. + * @param align The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@NonNull final Uri uri, @Align final int align) { + apply(mTypeImage); + this.imageUri = uri; + this.alignImage = align; + return this; + } + + /** + * Append one image. + * + * @param resourceId The resource id of image. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@DrawableRes final int resourceId) { + return appendImage(resourceId, ALIGN_BOTTOM); + } + + /** + * Append one image. + * + * @param resourceId The resource id of image. + * @param align The alignment. + * + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendImage(@DrawableRes final int resourceId, @Align final int align) { + apply(mTypeImage); + this.imageResourceId = resourceId; + this.alignImage = align; + return this; + } + + /** + * Append space. + * + * @param size The size of space. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendSpace(@IntRange(from = 0) final int size) { + return appendSpace(size, Color.TRANSPARENT); + } + + /** + * Append space. + * + * @param size The size of space. + * @param color The color of space. + * @return the single {@link SpannableStringUtils} instance + */ + public SpannableStringUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) { + apply(mTypeSpace); + spaceSize = size; + spaceColor = color; + return this; + } + + private void apply(final int type) { + applyLast(); + mType = type; + } + + public SpannableStringBuilder get() { + return mBuilder; + } + + /** + * Create the span string. + * + * @return the span string + */ + public SpannableStringBuilder create() { + applyLast(); + if (mTextView != null) { + mTextView.setText(mBuilder); + } + isCreated = true; + return mBuilder; + } + + private void applyLast() { + if (isCreated) { + return; + } + if (mType == mTypeCharSequence) { + updateCharCharSequence(); + } else if (mType == mTypeImage) { + updateImage(); + } else if (mType == mTypeSpace) { + updateSpace(); + } + setDefault(); + } + + private void updateCharCharSequence() { + if (mText.length() == 0) return; + int start = mBuilder.length(); + if (start == 0 && lineHeight != -1) {// bug of LineHeightSpan when first line + mBuilder.append(Character.toString((char) 2)) + .append("\n") + .setSpan(new AbsoluteSizeSpan(0), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + start = 2; + } + mBuilder.append(mText); + int end = mBuilder.length(); + if (verticalAlign != -1) { + mBuilder.setSpan(new VerticalAlignSpan(verticalAlign), start, end, flag); + } + if (foregroundColor != COLOR_DEFAULT) { + mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag); + } + if (backgroundColor != COLOR_DEFAULT) { + mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag); + } + if (first != -1) { + mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag); + } + if (quoteColor != COLOR_DEFAULT) { + mBuilder.setSpan( + new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth), + start, + end, + flag + ); + } + if (bulletColor != COLOR_DEFAULT) { + mBuilder.setSpan( + new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth), + start, + end, + flag + ); + } + if (fontSize != -1) { + mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag); + } + if (proportion != -1) { + mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag); + } + if (xProportion != -1) { + mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag); + } + if (lineHeight != -1) { + mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag); + } + if (isStrikethrough) { + mBuilder.setSpan(new StrikethroughSpan(), start, end, flag); + } + if (isUnderline) { + mBuilder.setSpan(new UnderlineSpan(), start, end, flag); + } + if (isSuperscript) { + mBuilder.setSpan(new SuperscriptSpan(), start, end, flag); + } + if (isSubscript) { + mBuilder.setSpan(new SubscriptSpan(), start, end, flag); + } + if (isBold) { + mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag); + } + if (isItalic) { + mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag); + } + if (isBoldItalic) { + mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag); + } + if (fontFamily != null) { + mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag); + } + if (typeface != null) { + mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag); + } + if (alignment != null) { + mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag); + } + if (clickSpan != null) { + mBuilder.setSpan(clickSpan, start, end, flag); + } + if (url != null) { + mBuilder.setSpan(new URLSpan(url), start, end, flag); + } + if (blurRadius != -1) { + mBuilder.setSpan( + new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)), + start, + end, + flag + ); + } + if (shader != null) { + mBuilder.setSpan(new ShaderSpan(shader), start, end, flag); + } + if (shadowRadius != -1) { + mBuilder.setSpan( + new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor), + start, + end, + flag + ); + } + if (spans != null) { + for (Object span : spans) { + mBuilder.setSpan(span, start, end, flag); + } + } + } + + private void updateImage() { + int start = mBuilder.length(); + mText = ""; + updateCharCharSequence(); + int end = mBuilder.length(); + if (imageBitmap != null) { + mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag); + } else if (imageDrawable != null) { + mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag); + } else if (imageUri != null) { + mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag); + } else if (imageResourceId != -1) { + mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag); + } + } + + private void updateSpace() { + int start = mBuilder.length(); + mText = "< >"; + updateCharCharSequence(); + int end = mBuilder.length(); + mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag); + } + + static class VerticalAlignSpan extends ReplacementSpan { + + static final int ALIGN_CENTER = 2; + static final int ALIGN_TOP = 3; + + final int mVerticalAlignment; + + VerticalAlignSpan(int verticalAlignment) { + mVerticalAlignment = verticalAlignment; + } + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) { + text = text.subSequence(start, end); + return (int) paint.measureText(text.toString()); + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + text = text.subSequence(start, end); + Paint.FontMetricsInt fm = paint.getFontMetricsInt(); +// int need = height - (v + fm.descent - fm.ascent - spanstartv); +// if (need > 0) { +// if (mVerticalAlignment == ALIGN_TOP) { +// fm.descent += need; +// } else if (mVerticalAlignment == ALIGN_CENTER) { +// fm.descent += need / 2; +// fm.ascent -= need / 2; +// } else { +// fm.ascent -= need; +// } +// } +// need = height - (v + fm.bottom - fm.top - spanstartv); +// if (need > 0) { +// if (mVerticalAlignment == ALIGN_TOP) { +// fm.bottom += need; +// } else if (mVerticalAlignment == ALIGN_CENTER) { +// fm.bottom += need / 2; +// fm.top -= need / 2; +// } else { +// fm.top -= need; +// } +// } + + canvas.drawText(text.toString(), x, y - ((y + fm.descent + y + fm.ascent) / 2 - (bottom + top) / 2), paint); + } + } + + static class CustomLineHeightSpan implements LineHeightSpan { + + private final int height; + + static final int ALIGN_CENTER = 2; + static final int ALIGN_TOP = 3; + + final int mVerticalAlignment; + static Paint.FontMetricsInt sfm; + + CustomLineHeightSpan(int height, int verticalAlignment) { + this.height = height; + mVerticalAlignment = verticalAlignment; + } + + @Override + public void chooseHeight(final CharSequence text, final int start, final int end, + final int spanstartv, final int v, final Paint.FontMetricsInt fm) { + if (sfm == null) { + sfm = new Paint.FontMetricsInt(); + sfm.top = fm.top; + sfm.ascent = fm.ascent; + sfm.descent = fm.descent; + sfm.bottom = fm.bottom; + sfm.leading = fm.leading; + } else { + fm.top = sfm.top; + fm.ascent = sfm.ascent; + fm.descent = sfm.descent; + fm.bottom = sfm.bottom; + fm.leading = sfm.leading; + } + int need = height - (v + fm.descent - fm.ascent - spanstartv); + if (need > 0) { + if (mVerticalAlignment == ALIGN_TOP) { + fm.descent += need; + } else if (mVerticalAlignment == ALIGN_CENTER) { + fm.descent += need / 2; + fm.ascent -= need / 2; + } else { + fm.ascent -= need; + } + } + need = height - (v + fm.bottom - fm.top - spanstartv); + if (need > 0) { + if (mVerticalAlignment == ALIGN_TOP) { + fm.bottom += need; + } else if (mVerticalAlignment == ALIGN_CENTER) { + fm.bottom += need / 2; + fm.top -= need / 2; + } else { + fm.top -= need; + } + } + if (end == ((Spanned) text).getSpanEnd(this)) { + sfm = null; + } +// LogUtils.e(fm, sfm); + } + } + + static class SpaceSpan extends ReplacementSpan { + + private final int width; + private final Paint paint = new Paint(); + + private SpaceSpan(final int width) { + this(width, Color.TRANSPARENT); + } + + private SpaceSpan(final int width, final int color) { + super(); + this.width = width; + paint.setColor(color); + paint.setStyle(Paint.Style.FILL); + } + + @Override + public int getSize(@NonNull final Paint paint, final CharSequence text, + @IntRange(from = 0) final int start, + @IntRange(from = 0) final int end, + @Nullable final Paint.FontMetricsInt fm) { + return width; + } + + @Override + public void draw(@NonNull final Canvas canvas, final CharSequence text, + @IntRange(from = 0) final int start, + @IntRange(from = 0) final int end, + final float x, final int top, final int y, final int bottom, + @NonNull final Paint paint) { + canvas.drawRect(x, top, x + width, bottom, this.paint); + } + } + + static class CustomQuoteSpan implements LeadingMarginSpan { + + private final int color; + private final int stripeWidth; + private final int gapWidth; + + private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) { + super(); + this.color = color; + this.stripeWidth = stripeWidth; + this.gapWidth = gapWidth; + } + + public int getLeadingMargin(final boolean first) { + return stripeWidth + gapWidth; + } + + public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir, + final int top, final int baseline, final int bottom, + final CharSequence text, final int start, final int end, + final boolean first, final Layout layout) { + Paint.Style style = p.getStyle(); + int color = p.getColor(); + + p.setStyle(Paint.Style.FILL); + p.setColor(this.color); + + c.drawRect(x, top, x + dir * stripeWidth, bottom, p); + + p.setStyle(style); + p.setColor(color); + } + } + + static class CustomBulletSpan implements LeadingMarginSpan { + + private final int color; + private final int radius; + private final int gapWidth; + + private Path sBulletPath = null; + + private CustomBulletSpan(final int color, final int radius, final int gapWidth) { + this.color = color; + this.radius = radius; + this.gapWidth = gapWidth; + } + + public int getLeadingMargin(final boolean first) { + return 2 * radius + gapWidth; + } + + public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir, + final int top, final int baseline, final int bottom, + final CharSequence text, final int start, final int end, + final boolean first, final Layout l) { + if (((Spanned) text).getSpanStart(this) == start) { + Paint.Style style = p.getStyle(); + int oldColor = 0; + oldColor = p.getColor(); + p.setColor(color); + p.setStyle(Paint.Style.FILL); + if (c.isHardwareAccelerated()) { + if (sBulletPath == null) { + sBulletPath = new Path(); + // Bullet is slightly better to avoid aliasing artifacts on mdpi devices. + sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW); + } + c.save(); + c.translate(x + dir * radius, (top + bottom) / 2.0f); + c.drawPath(sBulletPath, p); + c.restore(); + } else { + c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p); + } + p.setColor(oldColor); + p.setStyle(style); + } + } + } + + @SuppressLint("ParcelCreator") + static class CustomTypefaceSpan extends TypefaceSpan { + + private final Typeface newType; + + private CustomTypefaceSpan(final Typeface type) { + super(""); + newType = type; + } + + @Override + public void updateDrawState(final TextPaint textPaint) { + apply(textPaint, newType); + } + + @Override + public void updateMeasureState(final TextPaint paint) { + apply(paint, newType); + } + + private void apply(final Paint paint, final Typeface tf) { + int oldStyle; + Typeface old = paint.getTypeface(); + if (old == null) { + oldStyle = 0; + } else { + oldStyle = old.getStyle(); + } + + int fake = oldStyle & ~tf.getStyle(); + if ((fake & Typeface.BOLD) != 0) { + paint.setFakeBoldText(true); + } + + if ((fake & Typeface.ITALIC) != 0) { + paint.setTextSkewX(-0.25f); + } + + paint.getShader(); + + paint.setTypeface(tf); + } + } + + static class CustomImageSpan extends CustomDynamicDrawableSpan { + private Drawable mDrawable; + private Uri mContentUri; + private int mResourceId; + + private CustomImageSpan(final Bitmap b, final int verticalAlignment) { + super(verticalAlignment); + mDrawable = new BitmapDrawable(BaseApplication.context.getResources(), b); + mDrawable.setBounds( + 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight() + ); + } + + private CustomImageSpan(final Drawable d, final int verticalAlignment) { + super(verticalAlignment); + mDrawable = d; + mDrawable.setBounds( + 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight() + ); + } + + private CustomImageSpan(final Uri uri, final int verticalAlignment) { + super(verticalAlignment); + mContentUri = uri; + } + + private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) { + super(verticalAlignment); + mResourceId = resourceId; + } + + @Override + public Drawable getDrawable() { + Drawable drawable = null; + if (mDrawable != null) { + drawable = mDrawable; + } else if (mContentUri != null) { + Bitmap bitmap; + try { + InputStream is = + BaseApplication.context.getContentResolver().openInputStream(mContentUri); + bitmap = BitmapFactory.decodeStream(is); + drawable = new BitmapDrawable(BaseApplication.context.getResources(), bitmap); + drawable.setBounds( + 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() + ); + if (is != null) { + is.close(); + } + } catch (Exception e) { + Log.e("sms", "Failed to loaded content " + mContentUri, e); + } + } else { + try { + drawable = ContextCompat.getDrawable(BaseApplication.context, mResourceId); + drawable.setBounds( + 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() + ); + } catch (Exception e) { + Log.e("sms", "Unable to find resource: " + mResourceId); + } + } + return drawable; + } + } + + static abstract class CustomDynamicDrawableSpan extends ReplacementSpan { + + static final int ALIGN_BOTTOM = 0; + + static final int ALIGN_BASELINE = 1; + + static final int ALIGN_CENTER = 2; + + static final int ALIGN_TOP = 3; + + final int mVerticalAlignment; + + private CustomDynamicDrawableSpan() { + mVerticalAlignment = ALIGN_BOTTOM; + } + + private CustomDynamicDrawableSpan(final int verticalAlignment) { + mVerticalAlignment = verticalAlignment; + } + + public abstract Drawable getDrawable(); + + @Override + public int getSize(@NonNull final Paint paint, final CharSequence text, + final int start, final int end, final Paint.FontMetricsInt fm) { + Drawable d = getCachedDrawable(); + Rect rect = d.getBounds(); + if (fm != null) { + int lineHeight = fm.bottom - fm.top; + if (lineHeight < rect.height()) { + if (mVerticalAlignment == ALIGN_TOP) { + fm.top = fm.top; + fm.bottom = rect.height() + fm.top; + } else if (mVerticalAlignment == ALIGN_CENTER) { + fm.top = -rect.height() / 2 - lineHeight / 4; + fm.bottom = rect.height() / 2 - lineHeight / 4; + } else { + fm.top = -rect.height() + fm.bottom; + fm.bottom = fm.bottom; + } + fm.ascent = fm.top; + fm.descent = fm.bottom; + } + } + return rect.right; + } + + @Override + public void draw(@NonNull final Canvas canvas, final CharSequence text, + final int start, final int end, final float x, + final int top, final int y, final int bottom, @NonNull final Paint paint) { + Drawable d = getCachedDrawable(); + Rect rect = d.getBounds(); + canvas.save(); + float transY; + int lineHeight = bottom - top; + if (rect.height() < lineHeight) { + if (mVerticalAlignment == ALIGN_TOP) { + transY = top; + } else if (mVerticalAlignment == ALIGN_CENTER) { + transY = (bottom + top - rect.height()) / 2; + } else if (mVerticalAlignment == ALIGN_BASELINE) { + transY = y - rect.height(); + } else { + transY = bottom - rect.height(); + } + canvas.translate(x, transY); + } else { + canvas.translate(x, top); + } + d.draw(canvas); + canvas.restore(); + } + + private Drawable getCachedDrawable() { + WeakReference wr = mDrawableRef; + Drawable d = null; + if (wr != null) { + d = wr.get(); + } + if (d == null) { + d = getDrawable(); + mDrawableRef = new WeakReference<>(d); + } + return d; + } + + private WeakReference mDrawableRef; + } + + static class ShaderSpan extends CharacterStyle implements UpdateAppearance { + private Shader mShader; + + private ShaderSpan(final Shader shader) { + this.mShader = shader; + } + + @Override + public void updateDrawState(final TextPaint tp) { + tp.setShader(mShader); + } + } + + static class ShadowSpan extends CharacterStyle implements UpdateAppearance { + private float radius; + private float dx, dy; + private int shadowColor; + + private ShadowSpan(final float radius, + final float dx, + final float dy, + final int shadowColor) { + this.radius = radius; + this.dx = dx; + this.dy = dy; + this.shadowColor = shadowColor; + } + + @Override + public void updateDrawState(final TextPaint tp) { + tp.setShadowLayer(radius, dx, dy, shadowColor); + } + } + + private static class SerializableSpannableStringBuilder extends SpannableStringBuilder + implements Serializable { + + private static final long serialVersionUID = 4909567650765875771L; + } +} \ No newline at end of file