Преглед изворни кода

feat: 首页底部栏功能

DoggyZhang пре 3 месеци
родитељ
комит
883b6c9362
53 измењених фајлова са 1412 додато и 120 уклоњено
  1. 2 2
      app/dependencies/releaseRuntimeClasspath.txt
  2. 0 3
      app/src/main/java/com/adealink/weparty/MainActivity.kt
  3. 41 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/BlurAlgorithm.java
  4. 26 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/BlurController.java
  5. 60 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/BlurTarget.java
  6. 200 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/BlurView.java
  7. 14 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/BlurViewCanvas.java
  8. 48 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/BlurViewFacade.java
  9. 47 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/NoOpController.java
  10. 45 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/Noise.java
  11. 258 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/PreDrawBlurController.java
  12. 271 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/RenderNodeBlurController.java
  13. 105 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/RenderScriptBlur.java
  14. 93 0
      app/src/main/java/com/adealink/weparty/commonui/blurview/SizeScaler.java
  15. 1 0
      app/src/main/java/com/adealink/weparty/module/im/Router.kt
  16. 2 0
      app/src/main/java/com/adealink/weparty/module/playmate/Router.kt
  17. 2 2
      app/src/main/java/com/adealink/weparty/ui/category/CategoryActivity.kt
  18. 2 4
      app/src/main/java/com/adealink/weparty/ui/home/GuestHomeFragment.kt
  19. 2 4
      app/src/main/java/com/adealink/weparty/ui/home/HomeFragment.kt
  20. 35 7
      app/src/main/java/com/adealink/weparty/ui/main/MainFragment.kt
  21. 9 0
      app/src/main/java/com/adealink/weparty/ui/main/tab/MainTab.kt
  22. BIN
      app/src/main/res/drawable-nodpi/blue_noise.webp
  23. BIN
      app/src/main/res/drawable-xhdpi/main_home_ic.png
  24. BIN
      app/src/main/res/drawable-xhdpi/main_home_select_ic.png
  25. BIN
      app/src/main/res/drawable-xhdpi/main_me_ic.png
  26. BIN
      app/src/main/res/drawable-xhdpi/main_me_select_ic.png
  27. BIN
      app/src/main/res/drawable-xhdpi/main_message_ic.png
  28. BIN
      app/src/main/res/drawable-xhdpi/main_message_select_ic.png
  29. 9 0
      app/src/main/res/drawable/main_bottom_bar_bg.xml
  30. 6 0
      app/src/main/res/drawable/main_tab_bg.xml
  31. 44 12
      app/src/main/res/layout/fragment_main.xml
  32. 3 18
      app/src/main/res/layout/layout_main_tab.xml
  33. 4 0
      app/src/main/res/values/attrs.xml
  34. 4 0
      app/src/main/res/values/dimens.xml
  35. 1 1
      gradle/libs.versions.toml
  36. 2 3
      module/account/src/main/java/com/adealink/weparty/account/login/LoginDialog.kt
  37. 2 4
      module/account/src/main/java/com/adealink/weparty/account/register/fragment/CompleteUserInfoFragment.kt
  38. 2 4
      module/account/src/main/java/com/adealink/weparty/account/register/fragment/SelectCategoryFragment.kt
  39. 2 4
      module/account/src/main/java/com/adealink/weparty/account/register/fragment/SelectGenderFragment.kt
  40. 11 4
      module/im/src/main/java/com/adealink/weparty/im/list/SessionHomeListFragment.kt
  41. 10 2
      module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt
  42. 0 20
      module/im/src/main/res/layout/fragment_session_list.xml
  43. 2 2
      module/order/src/main/java/com/adealink/weparty/order/OrderDetailActivity.kt
  44. 2 2
      module/order/src/main/java/com/adealink/weparty/order/OrderListActivity.kt
  45. 4 4
      module/playmate/src/main/java/com/adealink/weparty/playmate/list/GuestPlaymateHomeFragment.kt
  46. 4 4
      module/playmate/src/main/java/com/adealink/weparty/playmate/list/PlaymateHomeFragment.kt
  47. 8 1
      module/playmate/src/main/java/com/adealink/weparty/playmate/list/PlaymateHomeListFragment.kt
  48. 9 1
      module/playmate/src/main/java/com/adealink/weparty/playmate/list/PlaymateListFragment.kt
  49. 2 1
      module/playmate/src/main/res/layout/fragment_playmate_list.xml
  50. 2 2
      module/profile/src/main/java/com/adealink/weparty/profile/edit/EditProfileActivity.kt
  51. 10 4
      module/profile/src/main/java/com/adealink/weparty/profile/me/MeFragment.kt
  52. 2 2
      module/profile/src/main/java/com/adealink/weparty/profile/search/SearchActivity.kt
  53. 4 3
      module/profile/src/main/res/layout/fragment_me.xml

+ 2 - 2
app/dependencies/releaseRuntimeClasspath.txt

@@ -232,7 +232,7 @@ com.wenext.android:frame-aab:6.0.3
 com.wenext.android:frame-apm:6.0.1
 com.wenext.android:frame-audio:6.0.0
 com.wenext.android:frame-base:6.0.4
-com.wenext.android:frame-bom:6.1.3
+com.wenext.android:frame-bom:6.1.5
 com.wenext.android:frame-coroutine:6.0.0
 com.wenext.android:frame-crash:6.0.1
 com.wenext.android:frame-data:6.0.0
@@ -261,7 +261,7 @@ com.wenext.android:frame-spi:6.0.0
 com.wenext.android:frame-startup:6.0.1
 com.wenext.android:frame-statistics:6.1.0
 com.wenext.android:frame-storage:6.0.7
-com.wenext.android:frame-util:6.0.3
+com.wenext.android:frame-util:6.0.5
 com.wenext.android:frame-zero:6.0.0
 com.wenext.android:retrofit:6.0.0
 commons-logging:commons-logging:1.2

+ 0 - 3
app/src/main/java/com/adealink/weparty/MainActivity.kt

@@ -46,9 +46,6 @@ class MainActivity : BaseActivity() {
     private var dispatchRouter = false
     override val routeSubPage: Boolean
         get() = !dispatchRouter
-    override val forceFitNavigationBar: Boolean
-        get() = true
-
     override fun onBeforeCreate() {
         super.onBeforeCreate()
         Log.d(TAG, "onCreate")

+ 41 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/BlurAlgorithm.java

@@ -0,0 +1,41 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+
+import androidx.annotation.NonNull;
+
+public interface BlurAlgorithm {
+    /**
+     * @param bitmap     bitmap to be blurred
+     * @param blurRadius blur radius
+     * @return blurred bitmap
+     */
+    Bitmap blur(@NonNull Bitmap bitmap, float blurRadius);
+
+    /**
+     * Frees allocated resources
+     */
+    void destroy();
+
+    /**
+     * @return true if this algorithm returns the same instance of bitmap as it accepted
+     * false if it creates a new instance.
+     * <p>
+     * If you return false from this method, you'll be responsible to swap bitmaps in your
+     * {@link BlurAlgorithm#blur(Bitmap, float)} implementation
+     * (assign input bitmap to your field and return the instance algorithm just blurred).
+     */
+    boolean canModifyBitmap();
+
+    /**
+     * Retrieve the {@link Bitmap.Config} on which the {@link BlurAlgorithm}
+     * can actually work.
+     *
+     * @return bitmap config supported by the given blur algorithm.
+     */
+    @NonNull
+    Bitmap.Config getSupportedBitmapConfig();
+
+    void render(@NonNull Canvas canvas, @NonNull Bitmap bitmap);
+}

+ 26 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/BlurController.java

@@ -0,0 +1,26 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.Canvas;
+
+public interface BlurController extends BlurViewFacade {
+
+    float DEFAULT_SCALE_FACTOR = 4f;
+    float DEFAULT_BLUR_RADIUS = 16f;
+
+    /**
+     * Draws blurred content on given canvas
+     *
+     * @return true if BlurView should proceed with drawing itself and its children
+     */
+    boolean draw(Canvas canvas);
+
+    /**
+     * Must be used to notify Controller when BlurView's size has changed
+     */
+    void updateBlurViewSize();
+
+    /**
+     * Frees allocated resources
+     */
+    void destroy();
+}

+ 60 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/BlurTarget.java

@@ -0,0 +1,60 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.RecordingCanvas;
+import android.graphics.RenderNode;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A FrameLayout that records a snapshot of its children on a RenderNode.
+ * This snapshot is used by the BlurView to apply blur effect.
+ */
+public class BlurTarget extends FrameLayout {
+    // Need both RenderNode (API 29) and RenderEffect (API 31) to be available for a full hardware rendering pipeline
+    static final boolean canUseHardwareRendering = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
+
+    RenderNode renderNode;
+
+    {
+        if (canUseHardwareRendering) {
+            renderNode = new RenderNode("BlurViewHost node");
+        }
+    }
+
+    public BlurTarget(@NonNull Context context) {
+        super(context);
+    }
+
+    public BlurTarget(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public BlurTarget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public BlurTarget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void dispatchDraw(@NonNull Canvas canvas) {
+        if (canUseHardwareRendering && canvas.isHardwareAccelerated()) {
+            renderNode.setPosition(0, 0, getWidth(), getHeight());
+            RecordingCanvas recordingCanvas = renderNode.beginRecording();
+            super.dispatchDraw(recordingCanvas);
+            renderNode.endRecording();
+            canvas.drawRenderNode(renderNode);
+        } else {
+            super.dispatchDraw(canvas);
+        }
+    }
+}

+ 200 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/BlurView.java

@@ -0,0 +1,200 @@
+package com.adealink.weparty.commonui.blurview;
+
+
+import static com.adealink.weparty.commonui.blurview.BlurController.DEFAULT_SCALE_FACTOR;
+import static com.adealink.weparty.commonui.blurview.PreDrawBlurController.TRANSPARENT;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.FrameLayout;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+
+import com.adealink.weparty.R;
+
+
+/**
+ * FrameLayout that blurs its underlying content.
+ * Can have children and draw them over blurred background.
+ */
+public class BlurView extends FrameLayout {
+
+    BlurController blurController = new NoOpController();
+
+    @ColorInt
+    private int overlayColor;
+    private boolean blurAutoUpdate = true;
+
+    public BlurView(Context context) {
+        super(context);
+        init(null, 0);
+    }
+
+    public BlurView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs, 0);
+    }
+
+    public BlurView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(attrs, defStyleAttr);
+    }
+
+    private void init(AttributeSet attrs, int defStyleAttr) {
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.BlurView, defStyleAttr, 0);
+        overlayColor = a.getColor(R.styleable.BlurView_blurOverlayColor, TRANSPARENT);
+        a.recycle();
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        boolean shouldDraw = blurController.draw(canvas);
+        if (shouldDraw) {
+            super.draw(canvas);
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        blurController.updateBlurViewSize();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        blurController.setBlurAutoUpdate(false);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (!isHardwareAccelerated()) {
+            Log.e("BlurView", "BlurView can't be used in not hardware-accelerated window!");
+        } else {
+            blurController.setBlurAutoUpdate(this.blurAutoUpdate);
+        }
+    }
+
+    /**
+     * @param target      the root to start blur from.
+     * @param algorithm   sets the blur algorithm. Ignored on API >= 31 where efficient hardware rendering pipeline is used.
+     * @param scaleFactor a scale factor to downscale the view snapshot before blurring.
+     *                    Helps achieving stronger blur and potentially better performance at the expense of blur precision.
+     *                    The blur radius is essentially the radius * scaleFactor.
+     * @param applyNoise  optional blue noise texture over the blurred content to make it look more natural. True by default.
+     * @return {@link BlurView} to setup needed params.
+     */
+    public BlurViewFacade setupWith(@NonNull BlurTarget target, BlurAlgorithm algorithm, float scaleFactor, boolean applyNoise) {
+        blurController.destroy();
+        if (BlurTarget.canUseHardwareRendering) {
+            // Ignores the blur algorithm, always uses RenderEffect
+            blurController = new RenderNodeBlurController(this, target, overlayColor, scaleFactor, applyNoise);
+        } else {
+            blurController = new PreDrawBlurController(this, target, overlayColor, algorithm, scaleFactor, applyNoise);
+        }
+
+        return blurController;
+    }
+
+    /**
+     * @param rootView    the root to start blur from.
+     *                    BlurAlgorithm is automatically picked based on the API version.
+     *                    It uses RenderEffect on API 31+, and RenderScriptBlur on older versions.
+     * @param scaleFactor a scale factor to downscale the view snapshot before blurring.
+     *                    Helps achieving stronger blur and potentially better performance at the expense of blur precision.
+     *                    The blur radius is essentially the radius * scaleFactor.
+     * @param applyNoise  optional blue noise texture over the blurred content to make it look more natural. True by default.
+     * @return {@link BlurView} to setup needed params.
+     */
+    public BlurViewFacade setupWith(@NonNull BlurTarget rootView, float scaleFactor, boolean applyNoise) {
+        BlurAlgorithm algorithm;
+        if (BlurTarget.canUseHardwareRendering) {
+            // Ignores the blur algorithm, always uses RenderNodeBlurController and RenderEffect
+            algorithm = null;
+        } else {
+            algorithm = new RenderScriptBlur(getContext());
+        }
+        return setupWith(rootView, algorithm, scaleFactor, applyNoise);
+    }
+
+    /**
+     * @param rootView root to start blur from.
+     *                 BlurAlgorithm is automatically picked based on the API version.
+     *                 It uses RenderEffect on API 31+, and RenderScriptBlur on older versions.
+     *                 The {@link DEFAULT_SCALE_FACTOR} scale factor for view snapshot is used.
+     *                 Blue noise texture is applied by default.
+     * @return {@link BlurView} to setup needed params.
+     */
+    public BlurViewFacade setupWith(@NonNull BlurTarget rootView) {
+        return setupWith(rootView, DEFAULT_SCALE_FACTOR, true);
+    }
+
+    // Setters duplicated to be able to conveniently change these settings outside of setupWith chain
+
+    /**
+     * @see BlurViewFacade#setBlurRadius(float)
+     */
+    public BlurViewFacade setBlurRadius(float radius) {
+        return blurController.setBlurRadius(radius);
+    }
+
+    /**
+     * @see BlurViewFacade#setOverlayColor(int)
+     */
+    public BlurViewFacade setOverlayColor(@ColorInt int overlayColor) {
+        this.overlayColor = overlayColor;
+        return blurController.setOverlayColor(overlayColor);
+    }
+
+    /**
+     * @see BlurViewFacade#setBlurAutoUpdate(boolean)
+     */
+    public BlurViewFacade setBlurAutoUpdate(boolean enabled) {
+        this.blurAutoUpdate = enabled;
+        return blurController.setBlurAutoUpdate(enabled);
+    }
+
+    /**
+     * @see BlurViewFacade#setBlurEnabled(boolean)
+     */
+    public BlurViewFacade setBlurEnabled(boolean enabled) {
+        return blurController.setBlurEnabled(enabled);
+    }
+
+    @Override
+    public void setRotation(float rotation) {
+        super.setRotation(rotation);
+        notifyRotationChanged(rotation);
+    }
+
+    @SuppressLint("NewApi")
+    public void notifyRotationChanged(float rotation) {
+        if (usingRenderNode()) {
+            ((RenderNodeBlurController) blurController).updateRotation(rotation);
+        }
+    }
+
+    @SuppressLint("NewApi")
+    public void notifyScaleXChanged(float scaleX) {
+        if (usingRenderNode()) {
+            ((RenderNodeBlurController) blurController).updateScaleX(scaleX);
+        }
+    }
+
+    @SuppressLint("NewApi")
+    public void notifyScaleYChanged(float scaleY) {
+        if (usingRenderNode()) {
+            ((RenderNodeBlurController) blurController).updateScaleY(scaleY);
+        }
+    }
+
+    private boolean usingRenderNode() {
+        return blurController instanceof RenderNodeBlurController;
+    }
+}

+ 14 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/BlurViewCanvas.java

@@ -0,0 +1,14 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+
+import androidx.annotation.NonNull;
+
+// Serves purely as a marker of a Canvas used in BlurView
+// to skip drawing itself and other BlurViews on the View hierarchy snapshot
+public class BlurViewCanvas extends Canvas {
+    public BlurViewCanvas(@NonNull Bitmap bitmap) {
+        super(bitmap);
+    }
+}

+ 48 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/BlurViewFacade.java

@@ -0,0 +1,48 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+
+public interface BlurViewFacade {
+
+    /**
+     * Enables/disables the blur. Enabled by default
+     *
+     * @param enabled true to enable, false otherwise
+     * @return {@link BlurViewFacade}
+     */
+    BlurViewFacade setBlurEnabled(boolean enabled);
+
+    /**
+     * Can be used to stop blur auto update or resume if it was stopped before.
+     * Enabled by default.
+     *
+     * @return {@link BlurViewFacade}
+     */
+    BlurViewFacade setBlurAutoUpdate(boolean enabled);
+
+    /**
+     * @param frameClearDrawable sets the drawable to draw before view hierarchy.
+     *                           Can be used to draw Activity's window background if your root layout doesn't provide any background
+     *                           Optional, by default frame is cleared with a transparent color.
+     * @return {@link BlurViewFacade}
+     */
+    BlurViewFacade setFrameClearDrawable(@Nullable Drawable frameClearDrawable);
+
+    /**
+     * @param radius sets the blur radius. The real blur radius is radius * scaleFactor.
+     *               Default value is {@link BlurController#DEFAULT_BLUR_RADIUS}
+     * @return {@link BlurViewFacade}
+     */
+    BlurViewFacade setBlurRadius(float radius);
+
+    /**
+     * Sets the color overlay to be drawn on top of blurred content
+     *
+     * @param overlayColor int color
+     * @return {@link BlurViewFacade}
+     */
+    BlurViewFacade setOverlayColor(@ColorInt int overlayColor);
+}

+ 47 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/NoOpController.java

@@ -0,0 +1,47 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.Nullable;
+
+// Used in edit mode and in case if no BlurController was set
+public class NoOpController implements BlurController {
+    @Override
+    public boolean draw(Canvas canvas) {
+        return true;
+    }
+
+    @Override
+    public void updateBlurViewSize() {
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    @Override
+    public BlurViewFacade setBlurRadius(float radius) {
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setOverlayColor(int overlayColor) {
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setFrameClearDrawable(@Nullable Drawable windowBackground) {
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setBlurEnabled(boolean enabled) {
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setBlurAutoUpdate(boolean enabled) {
+        return this;
+    }
+}

+ 45 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/Noise.java

@@ -0,0 +1,45 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+
+import androidx.annotation.NonNull;
+
+import com.adealink.weparty.R;
+
+class Noise {
+    private static Paint noisePaint;
+
+    static void apply(Canvas canvas, Context context, int width, int height) {
+        initPaint(context);
+        canvas.drawRect(0, 0, width, height, noisePaint);
+    }
+
+    private static void initPaint(Context context) {
+        if (noisePaint == null) {
+            Bitmap alphaBitmap = getNoiseBitmap(context);
+            noisePaint = new Paint();
+            noisePaint.setAntiAlias(true);
+            noisePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
+            noisePaint.setShader(new BitmapShader(alphaBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
+        }
+    }
+
+    @NonNull
+    private static Bitmap getNoiseBitmap(Context context) {
+        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.blue_noise);
+        Bitmap alphaBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(alphaBitmap);
+        Paint paint = new Paint();
+        paint.setAlpha(38); // 15% opacity
+        canvas.drawBitmap(bitmap, 0, 0, paint);
+        return alphaBitmap;
+    }
+}

+ 258 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/PreDrawBlurController.java

@@ -0,0 +1,258 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Blur Controller that handles all blur logic for the attached View.
+ * It honors View size changes, View animation and Visibility changes.
+ * <p>
+ * The basic idea is to draw the view hierarchy on a bitmap, excluding the attached View,
+ * then blur and draw it on the system Canvas.
+ * <p>
+ * It uses {@link ViewTreeObserver.OnPreDrawListener} to detect when
+ * blur should be updated.
+ * <p>
+ */
+public final class PreDrawBlurController implements BlurController {
+
+    @ColorInt
+    public static final int TRANSPARENT = 0;
+
+    private float blurRadius = DEFAULT_BLUR_RADIUS;
+
+    private final BlurAlgorithm blurAlgorithm;
+    private final float scaleFactor;
+    private final boolean applyNoise;
+    private BlurViewCanvas internalCanvas;
+    private Bitmap internalBitmap;
+
+    @SuppressWarnings("WeakerAccess")
+    final View blurView;
+    private int overlayColor;
+    private final ViewGroup rootView;
+    private final int[] rootLocation = new int[2];
+    private final int[] blurViewLocation = new int[2];
+
+    private final ViewTreeObserver.OnPreDrawListener drawListener = new ViewTreeObserver.OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+            // Not invalidating a View here, just updating the Bitmap.
+            // This relies on the HW accelerated bitmap drawing behavior in Android
+            // If the bitmap was drawn on HW accelerated canvas, it holds a reference to it and on next
+            // drawing pass the updated content of the bitmap will be rendered on the screen
+            updateBlur();
+            return true;
+        }
+    };
+
+    private boolean blurEnabled = true;
+    private boolean initialized;
+
+    @Nullable
+    private Drawable frameClearDrawable;
+
+    /**
+     * @param blurView    View which will draw it's blurred underlying content
+     * @param rootView    Root View where blurView's underlying content starts drawing.
+     *                    Can be Activity's root content layout (android.R.id.content)
+     * @param algorithm   sets the blur algorithm
+     * @param scaleFactor a scale factor to downscale the view snapshot before blurring.
+     *                    Helps achieving stronger blur and potentially better performance at the expense of blur precision.
+     * @param applyNoise  optional blue noise texture over the blurred content to make it look more natural. True by default.
+     */
+    public PreDrawBlurController(@NonNull View blurView,
+                                 @NonNull ViewGroup rootView,
+                                 @ColorInt int overlayColor,
+                                 BlurAlgorithm algorithm,
+                                 float scaleFactor,
+                                 boolean applyNoise) {
+        this.rootView = rootView;
+        this.blurView = blurView;
+        this.overlayColor = overlayColor;
+        this.blurAlgorithm = algorithm;
+        this.scaleFactor = scaleFactor;
+        this.applyNoise = applyNoise;
+
+        int measuredWidth = blurView.getMeasuredWidth();
+        int measuredHeight = blurView.getMeasuredHeight();
+
+        init(measuredWidth, measuredHeight);
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    void init(int measuredWidth, int measuredHeight) {
+        setBlurAutoUpdate(true);
+        SizeScaler sizeScaler = new SizeScaler(scaleFactor);
+        if (sizeScaler.isZeroSized(measuredWidth, measuredHeight)) {
+            // Will be initialized later when the View reports a size change
+            blurView.setWillNotDraw(true);
+            return;
+        }
+
+        blurView.setWillNotDraw(false);
+        SizeScaler.Size bitmapSize = sizeScaler.scale(measuredWidth, measuredHeight);
+        internalBitmap = Bitmap.createBitmap(bitmapSize.width, bitmapSize.height, blurAlgorithm.getSupportedBitmapConfig());
+        internalCanvas = new BlurViewCanvas(internalBitmap);
+        initialized = true;
+        // Usually it's not needed, because `onPreDraw` updates the blur anyway.
+        // But it handles cases when the PreDraw listener is attached to a different Window, for example
+        // when the BlurView is in a Dialog window, but the root is in the Activity.
+        // Previously it was done in `draw`, but it was causing potential side effects and Jetpack Compose crashes
+        updateBlur();
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    void updateBlur() {
+        if (!blurEnabled || !initialized) {
+            return;
+        }
+
+        if (frameClearDrawable == null) {
+            internalBitmap.eraseColor(Color.TRANSPARENT);
+        } else {
+            frameClearDrawable.draw(internalCanvas);
+        }
+
+        internalCanvas.save();
+        setupInternalCanvasMatrix();
+        try {
+            rootView.draw(internalCanvas);
+        } catch (Exception e) {
+            // Can potentially fail on rendering Hardware Bitmaps or something like that
+            Log.e("BlurView", "Error during snapshot capturing", e);
+        }
+        internalCanvas.restore();
+
+        blurAndSave();
+    }
+
+    /**
+     * Set up matrix to draw starting from blurView's position
+     */
+    private void setupInternalCanvasMatrix() {
+        rootView.getLocationOnScreen(rootLocation);
+        blurView.getLocationOnScreen(blurViewLocation);
+
+        int left = blurViewLocation[0] - rootLocation[0];
+        int top = blurViewLocation[1] - rootLocation[1];
+
+        // https://github.com/Dimezis/BlurView/issues/128
+        float scaleFactorH = (float) blurView.getHeight() / internalBitmap.getHeight();
+        float scaleFactorW = (float) blurView.getWidth() / internalBitmap.getWidth();
+
+        float scaledLeftPosition = -left / scaleFactorW;
+        float scaledTopPosition = -top / scaleFactorH;
+
+        internalCanvas.translate(scaledLeftPosition, scaledTopPosition);
+        internalCanvas.scale(1 / scaleFactorW, 1 / scaleFactorH);
+    }
+
+    @Override
+    public boolean draw(Canvas canvas) {
+        if (!blurEnabled || !initialized) {
+            return true;
+        }
+        // Not blurring itself or other BlurViews to not cause recursive draw calls
+        // Related: https://github.com/Dimezis/BlurView/issues/110
+        if (canvas instanceof BlurViewCanvas) {
+            return false;
+        }
+
+        // https://github.com/Dimezis/BlurView/issues/128
+        float scaleFactorH = (float) blurView.getHeight() / internalBitmap.getHeight();
+        float scaleFactorW = (float) blurView.getWidth() / internalBitmap.getWidth();
+
+        canvas.save();
+        // Don't draw outside of the BlurView bounds if parent has clipChildren = false
+        canvas.clipRect(0f, 0f, blurView.getWidth(), blurView.getHeight());
+        canvas.save();
+        canvas.scale(scaleFactorW, scaleFactorH);
+        blurAlgorithm.render(canvas, internalBitmap);
+        // restore scale so we don't upscale the noise texture
+        canvas.restore();
+        if (applyNoise) {
+            Noise.apply(canvas, blurView.getContext(), blurView.getWidth(), blurView.getHeight());
+        }
+        if (overlayColor != TRANSPARENT) {
+            canvas.drawColor(overlayColor);
+        }
+        // restore clip rect
+        canvas.restore();
+        return true;
+    }
+
+    private void blurAndSave() {
+        internalBitmap = blurAlgorithm.blur(internalBitmap, blurRadius);
+        if (!blurAlgorithm.canModifyBitmap()) {
+            internalCanvas.setBitmap(internalBitmap);
+        }
+    }
+
+    @Override
+    public void updateBlurViewSize() {
+        int measuredWidth = blurView.getMeasuredWidth();
+        int measuredHeight = blurView.getMeasuredHeight();
+
+        init(measuredWidth, measuredHeight);
+    }
+
+    @Override
+    public void destroy() {
+        setBlurAutoUpdate(false);
+        blurAlgorithm.destroy();
+        initialized = false;
+    }
+
+    @Override
+    public BlurViewFacade setBlurRadius(float radius) {
+        this.blurRadius = radius;
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setFrameClearDrawable(@Nullable Drawable frameClearDrawable) {
+        this.frameClearDrawable = frameClearDrawable;
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setBlurEnabled(boolean enabled) {
+        this.blurEnabled = enabled;
+        setBlurAutoUpdate(enabled);
+        blurView.invalidate();
+        return this;
+    }
+
+    public BlurViewFacade setBlurAutoUpdate(final boolean enabled) {
+        rootView.getViewTreeObserver().removeOnPreDrawListener(drawListener);
+        blurView.getViewTreeObserver().removeOnPreDrawListener(drawListener);
+        if (enabled) {
+            rootView.getViewTreeObserver().addOnPreDrawListener(drawListener);
+            // Track changes in the blurView window too, for example if it's in a bottom sheet dialog
+            if (rootView.getWindowId() != blurView.getWindowId()) {
+                blurView.getViewTreeObserver().addOnPreDrawListener(drawListener);
+            }
+        }
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setOverlayColor(int overlayColor) {
+        if (this.overlayColor != overlayColor) {
+            this.overlayColor = overlayColor;
+            blurView.invalidate();
+        }
+        return this;
+    }
+}

+ 271 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/RenderNodeBlurController.java

@@ -0,0 +1,271 @@
+package com.adealink.weparty.commonui.blurview;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.RecordingCanvas;
+import android.graphics.RenderEffect;
+import android.graphics.RenderNode;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
+import android.view.ViewTreeObserver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.adealink.weparty.commonui.blurview.SizeScaler.Size;
+
+
+@RequiresApi(api = Build.VERSION_CODES.S)
+public class RenderNodeBlurController implements BlurController {
+    private final int[] targetLocation = new int[2];
+    private final int[] blurViewLocation = new int[2];
+
+    private final BlurView blurView;
+    private final BlurTarget target;
+    private final RenderNode blurNode = new RenderNode("BlurView node");
+    private final float scaleFactor;
+    private final boolean applyNoise;
+
+    private Drawable frameClearDrawable;
+    private int overlayColor;
+    private float blurRadius = 1f;
+    private boolean enabled = true;
+
+    // Potentially cached stuff from the slow software path
+    @Nullable
+    private Bitmap cachedBitmap;
+    @Nullable
+    private RenderScriptBlur fallbackBlur;
+
+    // This tracks BlurView location in scrollable containers, during animations, etc.
+    private final ViewTreeObserver.OnPreDrawListener drawListener = () -> {
+        saveOnScreenLocation();
+        updateRenderNodeProperties();
+        return true;
+    };
+
+    public RenderNodeBlurController(@NonNull BlurView blurView, @NonNull BlurTarget target, int overlayColor, float scaleFactor, boolean applyNoise) {
+        this.blurView = blurView;
+        this.overlayColor = overlayColor;
+        this.target = target;
+        this.scaleFactor = scaleFactor;
+        this.applyNoise = applyNoise;
+        blurView.setWillNotDraw(false);
+        blurView.getViewTreeObserver().addOnPreDrawListener(drawListener);
+    }
+
+    @Override
+    public boolean draw(Canvas canvas) {
+        if (!enabled) {
+            return true;
+        }
+        saveOnScreenLocation();
+
+        if (canvas.isHardwareAccelerated()) {
+            hardwarePath(canvas);
+        } else {
+            // Rendering on a software canvas.
+            // Presumably this is something taking a programmatic screenshot,
+            // or maybe a software-based View/Fragment transition.
+            // This is slow and shouldn't be a common case for this controller.
+            softwarePath(canvas);
+        }
+        return true;
+    }
+
+    // Not doing any scaleFactor-related manipulations here, because RenderEffect blur internally
+    // already scales down the snapshot depending on the blur radius.
+    // https://cs.android.com/android/platform/superproject/main/+/main:external/skia/src/core/SkImageFilterTypes.cpp;drc=61197364367c9e404c7da6900658f1b16c42d0da;l=2103
+    // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/jni/RenderEffect.cpp;l=39;drc=61197364367c9e404c7da6900658f1b16c42d0da?q=nativeCreateBlurEffect&ss=android%2Fplatform%2Fsuperproject%2Fmain
+    private void hardwarePath(Canvas canvas) {
+        // TODO would be good to keep it the size of the BlurView instead of the target, but then the animation
+        //  like translation and rotation would go out of bounds. Not sure if there's a good fix for this
+        blurNode.setPosition(0, 0, target.getWidth(), target.getHeight());
+        updateRenderNodeProperties();
+
+        drawSnapshot();
+
+        canvas.save();
+        // Don't draw outside of the BlurView bounds if parent has clipChildren = false
+        canvas.clipRect(0f, 0f, blurView.getWidth(), blurView.getHeight());
+        // Draw on the system canvas
+        canvas.drawRenderNode(blurNode);
+        if (applyNoise) {
+            Noise.apply(canvas, blurView.getContext(), blurView.getWidth(), blurView.getHeight());
+        }
+        if (overlayColor != Color.TRANSPARENT) {
+            canvas.drawColor(overlayColor);
+        }
+        canvas.restore();
+    }
+
+    private void updateRenderNodeProperties() {
+        float layoutTranslationX = -getLeft();
+        float layoutTranslationY = -getTop();
+
+        // Pivot point for the rotation and scale (in case it's applied)
+        blurNode.setPivotX(blurView.getWidth() / 2f - layoutTranslationX);
+        blurNode.setPivotY(blurView.getHeight() / 2f - layoutTranslationY);
+        blurNode.setTranslationX(layoutTranslationX);
+        blurNode.setTranslationY(layoutTranslationY);
+
+        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+            // There's a bug on API 31 - blurNode doesn't get re-rendered on setting new translation/scale/rotation,
+            // so we need to re-apply the blur effect to trigger a redraw.
+            applyBlur();
+        }
+    }
+
+    private void drawSnapshot() {
+        RecordingCanvas recordingCanvas = blurNode.beginRecording();
+        if (frameClearDrawable != null) {
+            frameClearDrawable.draw(recordingCanvas);
+        }
+        recordingCanvas.drawRenderNode(target.renderNode);
+        // Looks like the order of this doesn't matter
+        applyBlur();
+        blurNode.endRecording();
+    }
+
+    private void softwarePath(Canvas canvas) {
+        SizeScaler sizeScaler = new SizeScaler(scaleFactor);
+        Size original = new Size(blurView.getWidth(), blurView.getHeight());
+        Size scaled = sizeScaler.scale(original);
+        if (cachedBitmap == null || cachedBitmap.getWidth() != scaled.width || cachedBitmap.getHeight() != scaled.height) {
+            cachedBitmap = Bitmap.createBitmap(scaled.width, scaled.height, Bitmap.Config.ARGB_8888);
+        }
+        Canvas softwareCanvas = new Canvas(cachedBitmap);
+
+        softwareCanvas.save();
+        setupCanvasMatrix(softwareCanvas, original, scaled);
+        if (frameClearDrawable != null) {
+            frameClearDrawable.draw(canvas);
+        }
+        try {
+            target.draw(softwareCanvas);
+        } catch (Exception e) {
+            // Can potentially fail on rendering Hardware Bitmaps or something like that
+            Log.e("BlurView", "Error during snapshot capturing", e);
+        }
+        softwareCanvas.restore();
+
+        if (fallbackBlur == null) {
+            fallbackBlur = new RenderScriptBlur(blurView.getContext());
+        }
+        fallbackBlur.blur(cachedBitmap, blurRadius);
+        canvas.save();
+        canvas.scale((float) original.width / scaled.width, (float) original.height / scaled.height);
+        fallbackBlur.render(canvas, cachedBitmap);
+        canvas.restore();
+        if (applyNoise) {
+            Noise.apply(canvas, blurView.getContext(), blurView.getWidth(), blurView.getHeight());
+        }
+        if (overlayColor != Color.TRANSPARENT) {
+            canvas.drawColor(overlayColor);
+        }
+    }
+
+    /**
+     * Set up matrix to draw starting from blurView's position
+     */
+    private void setupCanvasMatrix(Canvas canvas, Size targetSize, Size scaledSize) {
+        // https://github.com/Dimezis/BlurView/issues/128
+        float scaleFactorH = (float) targetSize.height / scaledSize.height;
+        float scaleFactorW = (float) targetSize.width / scaledSize.width;
+
+        float scaledLeftPosition = -getLeft() / scaleFactorW;
+        float scaledTopPosition = -getTop() / scaleFactorH;
+
+        canvas.translate(scaledLeftPosition, scaledTopPosition);
+        canvas.scale(1 / scaleFactorW, 1 / scaleFactorH);
+    }
+
+    private int getTop() {
+        return blurViewLocation[1] - targetLocation[1];
+    }
+
+    private int getLeft() {
+        return blurViewLocation[0] - targetLocation[0];
+    }
+
+    @Override
+    public void updateBlurViewSize() {
+        // No-op, the size is updated in draw method, it's cheap and not called frequently
+    }
+
+    @Override
+    public void destroy() {
+        blurNode.discardDisplayList();
+        if (fallbackBlur != null) {
+            fallbackBlur.destroy();
+            fallbackBlur = null;
+        }
+    }
+
+    @Override
+    public BlurViewFacade setBlurEnabled(boolean enabled) {
+        this.enabled = enabled;
+        blurView.invalidate();
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setBlurAutoUpdate(boolean enabled) {
+        blurView.getViewTreeObserver().removeOnPreDrawListener(drawListener);
+        if (enabled) {
+            blurView.getViewTreeObserver().addOnPreDrawListener(drawListener);
+        }
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setFrameClearDrawable(@Nullable Drawable frameClearDrawable) {
+        this.frameClearDrawable = frameClearDrawable;
+        return this;
+    }
+
+    @Override
+    public BlurViewFacade setBlurRadius(float radius) {
+        this.blurRadius = radius;
+        applyBlur();
+        return this;
+    }
+
+    private void applyBlur() {
+        // scaleFactor is only used to increase the blur radius
+        // because RenderEffect already scales down the snapshot when needed.
+        float realBlurRadius = blurRadius * scaleFactor;
+        RenderEffect blur = RenderEffect.createBlurEffect(realBlurRadius, realBlurRadius, Shader.TileMode.CLAMP);
+        blurNode.setRenderEffect(blur);
+    }
+
+    @Override
+    public BlurViewFacade setOverlayColor(int overlayColor) {
+        if (this.overlayColor != overlayColor) {
+            this.overlayColor = overlayColor;
+            blurView.invalidate();
+        }
+        return this;
+    }
+
+    void updateRotation(float rotation) {
+        blurNode.setRotationZ(-rotation);
+    }
+
+    public void updateScaleX(float scaleX) {
+        blurNode.setScaleX(1 / scaleX);
+    }
+
+    public void updateScaleY(float scaleY) {
+        blurNode.setScaleY(1 / scaleY);
+    }
+
+    private void saveOnScreenLocation() {
+        target.getLocationOnScreen(targetLocation);
+        blurView.getLocationOnScreen(blurViewLocation);
+    }
+}

+ 105 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/RenderScriptBlur.java

@@ -0,0 +1,105 @@
+package com.adealink.weparty.commonui.blurview;
+
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Blur using RenderScript, processed on GPU when device drivers support it.
+ * Requires API 17+
+ *
+ * @deprecated because RenderScript is deprecated and its hardware acceleration is not guaranteed.
+ * On API 31+ an alternative hardware accelerated blur implementation is automatically used.
+ */
+@Deprecated
+public class RenderScriptBlur implements BlurAlgorithm {
+    private final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
+    private final RenderScript renderScript;
+    private final ScriptIntrinsicBlur blurScript;
+    private Allocation outAllocation;
+
+    private int lastBitmapWidth = -1;
+    private int lastBitmapHeight = -1;
+
+    /**
+     * @param context Context to create the {@link RenderScript}
+     */
+    public RenderScriptBlur(@NonNull Context context) {
+        renderScript = RenderScript.create(context);
+        blurScript = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
+    }
+
+    private boolean canReuseAllocation(@NonNull Bitmap bitmap) {
+        return bitmap.getHeight() == lastBitmapHeight && bitmap.getWidth() == lastBitmapWidth;
+    }
+
+    /**
+     * @param bitmap     bitmap to blur
+     * @param blurRadius blur radius (1..25)
+     * @return blurred bitmap
+     */
+    @Override
+    public Bitmap blur(@NonNull Bitmap bitmap, float blurRadius) {
+        try {
+            //Allocation will use the same backing array of pixels as bitmap if created with USAGE_SHARED flag
+            Allocation inAllocation = Allocation.createFromBitmap(renderScript, bitmap);
+
+            if (!canReuseAllocation(bitmap)) {
+                if (outAllocation != null) {
+                    outAllocation.destroy();
+                }
+                outAllocation = Allocation.createTyped(renderScript, inAllocation.getType());
+                lastBitmapWidth = bitmap.getWidth();
+                lastBitmapHeight = bitmap.getHeight();
+            }
+
+            blurScript.setRadius(min(blurRadius, 25f));
+            blurScript.setInput(inAllocation);
+            //do not use inAllocation in forEach. it will cause visual artifacts on blurred Bitmap
+            blurScript.forEach(outAllocation);
+            outAllocation.copyTo(bitmap);
+
+            inAllocation.destroy();
+        } catch (Exception e) {
+            // Can potentially crash because RenderScript context was released by someone else via RenderScript.releaseAllContexts()
+            // Some Glide transformations can cause this.
+            Log.e("BlurView", "RenderScript blur failed. Rendering unblurred snapshot", e);
+        }
+        return bitmap;
+    }
+
+    @Override
+    public final void destroy() {
+        blurScript.destroy();
+        renderScript.destroy();
+        if (outAllocation != null) {
+            outAllocation.destroy();
+        }
+    }
+
+    @Override
+    public boolean canModifyBitmap() {
+        return true;
+    }
+
+    @NonNull
+    @Override
+    public Bitmap.Config getSupportedBitmapConfig() {
+        return Bitmap.Config.ARGB_8888;
+    }
+
+    @Override
+    public void render(@NonNull Canvas canvas, @NonNull Bitmap bitmap) {
+        canvas.drawBitmap(bitmap, 0f, 0f, paint);
+    }
+}

+ 93 - 0
app/src/main/java/com/adealink/weparty/commonui/blurview/SizeScaler.java

@@ -0,0 +1,93 @@
+package com.adealink.weparty.commonui.blurview;
+
+import java.util.Objects;
+
+/**
+ * Scales width and height by [scaleFactor],
+ * and then rounds the size proportionally so the width is divisible by [ROUNDING_VALUE]
+ */
+public class SizeScaler {
+
+    // Bitmap size should be divisible by ROUNDING_VALUE to meet stride requirement.
+    // This will help avoiding an extra bitmap allocation when passing the bitmap to RenderScript for blur.
+    // Usually it's 16, but on Samsung devices it's 64 for some reason.
+    private static final int ROUNDING_VALUE = 64;
+    private final float scaleFactor;
+    private final boolean noStrideAlignment;
+
+    public SizeScaler(float scaleFactor) {
+        this(scaleFactor, false);
+    }
+
+    public SizeScaler(float scaleFactor, boolean noStrideAlignment) {
+        this.scaleFactor = scaleFactor;
+        this.noStrideAlignment = noStrideAlignment;
+    }
+
+    Size scale(int width, int height) {
+        int nonRoundedScaledWidth = downscaleSize(width);
+        int scaledWidth = roundSize(nonRoundedScaledWidth);
+        //Only width has to be aligned to ROUNDING_VALUE
+        float roundingScaleFactor = (float) width / scaledWidth;
+        //Ceiling because rounding or flooring might leave empty space on the View's bottom
+        int scaledHeight = (int) Math.ceil(height / roundingScaleFactor);
+
+        return new Size(scaledWidth, scaledHeight);
+    }
+
+    Size scale(Size size) {
+        return scale(size.width, size.height);
+    }
+
+    boolean isZeroSized(int measuredWidth, int measuredHeight) {
+        return downscaleSize(measuredHeight) == 0 || downscaleSize(measuredWidth) == 0;
+    }
+
+    /**
+     * Rounds a value to the nearest divisible by {@link #ROUNDING_VALUE} to meet stride requirement
+     */
+    private int roundSize(int value) {
+        if (noStrideAlignment) {
+            return value;
+        }
+        if (value % ROUNDING_VALUE == 0) {
+            return value;
+        }
+        return value - (value % ROUNDING_VALUE) + ROUNDING_VALUE;
+    }
+
+    private int downscaleSize(float value) {
+        return (int) Math.ceil(value / scaleFactor);
+    }
+
+    static class Size {
+
+        final int width;
+        final int height;
+
+        Size(int width, int height) {
+            this.width = width;
+            this.height = height;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null || getClass() != o.getClass()) return false;
+            Size size = (Size) o;
+            return width == size.width && height == size.height;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(width, height);
+        }
+
+        @Override
+        public String toString() {
+            return "Size{" +
+                    "width=" + width +
+                    ", height=" + height +
+                    '}';
+        }
+    }
+}

+ 1 - 0
app/src/main/java/com/adealink/weparty/module/im/Router.kt

@@ -17,6 +17,7 @@ interface IM {
     interface SessionList {
         companion object {
             const val PATH = "${Common.PATH}/session_list"
+            const val EXTRA_LAST_ITEM_BOTTOM = "extra_last_item_bottom"
         }
     }
 

+ 2 - 0
app/src/main/java/com/adealink/weparty/module/playmate/Router.kt

@@ -8,6 +8,8 @@ interface Playmate {
         companion object {
             const val PATH = "/playmate"
             const val EXTRA_CATEGORY = "extra_category"
+
+            const val EXTRA_LAST_ITEM_BOTTOM = "extra_last_item_bottom"
         }
 
     }

+ 2 - 2
app/src/main/java/com/adealink/weparty/ui/category/CategoryActivity.kt

@@ -5,7 +5,7 @@ import androidx.core.view.updateLayoutParams
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.databinding.ActivityCategoryBinding
 import com.adealink.weparty.module.category.Category
@@ -29,7 +29,7 @@ class CategoryActivity : BaseActivity() {
         super.initViews()
         setContentView(binding.root)
         binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = getStatusBarHeight(this@CategoryActivity)
+            topMargin = this@CategoryActivity.statusBarHeight()
         }
         inflateCategoryFragment()
     }

+ 2 - 4
app/src/main/java/com/adealink/weparty/ui/home/GuestHomeFragment.kt

@@ -10,8 +10,8 @@ import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.startup.DistributedLoadManager
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.AppModule
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.BaseFragment
@@ -47,9 +47,7 @@ class GuestHomeFragment : BaseFragment(R.layout.fragment_home) {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         logTime(TAG_TIME_APP_START, "GuestHomeFragment.onViewCreated()")
         binding.clTopBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
         dlManager = DistributedLoadManager(lifecycle, stepsLoad())
         activity?.let { dlManager.triggerDistributedLoad(it) }

+ 2 - 4
app/src/main/java/com/adealink/weparty/ui/home/HomeFragment.kt

@@ -10,8 +10,8 @@ import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.startup.DistributedLoadManager
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.AppModule
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.BaseFragment
@@ -48,9 +48,7 @@ class HomeFragment : BaseFragment(R.layout.fragment_home) {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         logTime(TAG_TIME_APP_START, "HomeFragment.onViewCreated()")
         binding.clTopBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
         dlManager = DistributedLoadManager(lifecycle, stepsLoad())
         activity?.let { dlManager.triggerDistributedLoad(it) }

+ 35 - 7
app/src/main/java/com/adealink/weparty/ui/main/MainFragment.kt

@@ -1,18 +1,22 @@
 package com.adealink.weparty.ui.main
 
 import android.annotation.SuppressLint
+import android.graphics.Outline
 import android.os.Bundle
 import android.view.View
+import android.view.ViewOutlineProvider
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updateLayoutParams
 import androidx.fragment.app.Fragment
 import androidx.recyclerview.widget.RecyclerView
+import com.adealink.frame.aab.util.getCompatDimension
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.startup.DistributedLoadManager
+import com.adealink.frame.util.naviBarHeight
 import com.adealink.weparty.AppModule
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.BaseFragment
-import com.adealink.weparty.commonui.ext.gone
-import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BaseTabFragmentStateAdapter
 import com.adealink.weparty.commonui.widget.EmptyFragment
 import com.adealink.weparty.constant.TAG_TIME_APP_START
@@ -45,6 +49,7 @@ class MainFragment : BaseFragment(R.layout.fragment_main), ITabManager by TabMan
     private val binding by viewBinding(FragmentMainBinding::bind)
     private lateinit var dlManager: DistributedLoadManager
     private lateinit var mainPagerAdapter: MainPageAdapter
+
     @SuppressLint("MissingSuperCall")
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         logTime(TAG_TIME_APP_START, "MainFragment.onViewCreated()")
@@ -69,6 +74,7 @@ class MainFragment : BaseFragment(R.layout.fragment_main), ITabManager by TabMan
 
     override fun initViews() {
         super.initViews()
+        initBlur()
         initTabs(MAIN_TABS)
 
         mainPagerAdapter = MainPageAdapter()
@@ -85,8 +91,6 @@ class MainFragment : BaseFragment(R.layout.fragment_main), ITabManager by TabMan
         ) { tabLayout, position ->
             tabLayout.setCustomView(R.layout.layout_main_tab)
             tabLayout.customView?.let { customView ->
-                val customBinding = LayoutMainTabBinding.bind(customView)
-                val tab = getTab(position) ?: return@let
                 updateTabView(
                     tabLayout,
                     0 == position,
@@ -112,18 +116,42 @@ class MainFragment : BaseFragment(R.layout.fragment_main), ITabManager by TabMan
         setDefaultTab(tabKey = arguments?.getString(AppModule.Main.EXTRA_MAIN_TAB))
     }
 
+    private fun initBlur() {
+        binding.blurBar.setClipToOutline(true)
+        binding.blurBar.outlineProvider = object : ViewOutlineProvider() {
+            override fun getOutline(view: View?, outline: Outline?) {
+                outline ?: return
+                binding.blurBar.background?.getOutline(outline)
+                outline.alpha = 1f
+            }
+        }
+//        val radius = 25f
+//        val minBlurRadius = 4f
+//        val step = 4f
+        //set background, if your root layout doesn't have one
+        val windowBackground = activity?.window?.decorView?.background
+        binding.blurBar.setupWith(binding.blurTarget)
+            .setFrameClearDrawable(windowBackground)
+            .setBlurRadius(4f)
+
+        binding.blurBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            bottomMargin =
+                activity.naviBarHeight() + getCompatDimension(R.dimen.main_bottom_bar_margin_bottom).toInt()
+        }
+    }
+
     fun updateTabView(
         tabView: TabLayout.Tab?,
         isSelected: Boolean,
         position: Int
     ) {
+        val tab = getTab(position) ?: return
         tabView?.customView?.let {
             val customViewBinding = LayoutMainTabBinding.bind(it)
-            customViewBinding.tvTab.text = MAIN_TABS.getOrNull(position)?.name?.invoke() ?: ""
             if (isSelected) {
-                customViewBinding.ivTabBg.show()
+                customViewBinding.ivTabBg.setImageResource(tab.iconSelected)
             } else {
-                customViewBinding.ivTabBg.gone()
+                customViewBinding.ivTabBg.setImageResource(tab.icon)
             }
         }
     }

+ 9 - 0
app/src/main/java/com/adealink/weparty/ui/main/tab/MainTab.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.ui.main.tab
 
+import androidx.annotation.DrawableRes
 import androidx.fragment.app.Fragment
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.router.Router
@@ -37,6 +38,8 @@ enum class MainTab(val tab: String, val number: Int) {
 data class Tab(
     val type: MainTab,
     val name: (() -> String),
+    @DrawableRes val icon: Int,
+    @DrawableRes val iconSelected: Int,
     val fragmentBuilder: () -> Fragment
 )
 
@@ -45,6 +48,8 @@ val HOME_TAB = Tab(
     name = {
         getCompatString(R.string.common_main_home_tab)
     },
+    icon = R.drawable.main_home_ic,
+    iconSelected = R.drawable.main_home_select_ic,
     fragmentBuilder = {
         HomeFragment()
     }
@@ -55,6 +60,8 @@ val MESSAGE_TAB = Tab(
     name = {
         getCompatString(R.string.common_main_message_tab)
     },
+    icon = R.drawable.main_message_ic,
+    iconSelected = R.drawable.main_message_select_ic,
     fragmentBuilder = {
         Router.getRouterInstance<BaseFragment>(IM.HomeList.PATH) ?: EmptyFragment()
     }
@@ -65,6 +72,8 @@ val ME_TAB = Tab(
     name = {
         getCompatString(R.string.common_main_me_tab)
     },
+    icon = R.drawable.main_me_ic,
+    iconSelected = R.drawable.main_me_select_ic,
     fragmentBuilder = {
         Router.getRouterInstance<BaseFragment>(Profile.Me.PATH) ?: EmptyFragment()
     }

BIN
app/src/main/res/drawable-nodpi/blue_noise.webp


BIN
app/src/main/res/drawable-xhdpi/main_home_ic.png


BIN
app/src/main/res/drawable-xhdpi/main_home_select_ic.png


BIN
app/src/main/res/drawable-xhdpi/main_me_ic.png


BIN
app/src/main/res/drawable-xhdpi/main_me_select_ic.png


BIN
app/src/main/res/drawable-xhdpi/main_message_ic.png


BIN
app/src/main/res/drawable-xhdpi/main_message_select_ic.png


+ 9 - 0
app/src/main/res/drawable/main_bottom_bar_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="90"
+        android:endColor="#00F1F2F5"
+        android:startColor="#A6FFFFFF"
+        android:type="linear" />
+</shape>

+ 6 - 0
app/src/main/res/drawable/main_tab_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/color_00FFFFFF" />
+    <corners android:radius="30dp" />
+</shape>

+ 44 - 12
app/src/main/res/layout/fragment_main.xml

@@ -4,21 +4,53 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <androidx.viewpager2.widget.ViewPager2
-        android:id="@+id/vp_content"
+    <com.adealink.weparty.commonui.blurview.BlurTarget
+        android:id="@+id/blur_target"
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        app:layout_constraintBottom_toTopOf="@+id/tl_tab"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
 
-    <com.google.android.material.tabs.TabLayout
-        android:id="@+id/tl_tab"
-        android:layout_width="match_parent"
-        android:layout_height="54dp"
+        <androidx.viewpager2.widget.ViewPager2
+            android:id="@+id/vp_content"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </com.adealink.weparty.commonui.blurview.BlurTarget>
+
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="@drawable/main_bottom_bar_bg"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:tabBackground="@null"
-        app:tabIndicatorHeight="0dp"
-        app:tabMode="fixed"
-        app:tabRippleColor="@null" />
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/blur_bar" />
+
+    <com.adealink.weparty.commonui.blurview.BlurView
+        android:id="@+id/blur_bar"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/main_bottom_bar_height"
+        android:layout_marginHorizontal="42dp"
+        android:background="@drawable/main_tab_bg"
+        android:elevation="16dp"
+        app:blurOverlayColor="@color/color_7FFFFFFF"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent">
+
+        <com.google.android.material.tabs.TabLayout
+            android:id="@+id/tl_tab"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:tabBackground="@null"
+            app:tabGravity="fill"
+            app:tabIndicatorHeight="0dp"
+            app:tabMode="fixed"
+            app:tabPadding="0dp"
+            app:tabRippleColor="@null" />
+    </com.adealink.weparty.commonui.blurview.BlurView>
+
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 3 - 18
app/src/main/res/layout/layout_main_tab.xml

@@ -1,32 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
     <androidx.appcompat.widget.AppCompatImageView
         android:id="@+id/iv_tab_bg"
-        android:layout_width="42dp"
-        android:layout_height="22dp"
-        app:layout_constraintBottom_toBottomOf="@id/tv_tab"
-        app:layout_constraintEnd_toEndOf="@id/tv_tab"
-        app:layout_constraintStart_toStartOf="@id/tv_tab"
-        app:layout_constraintTop_toTopOf="@id/tv_tab"
-        app:srcCompat="@drawable/common_home_main_tab_selected_ic" />
-
-    <androidx.appcompat.widget.AppCompatTextView
-        android:id="@+id/tv_tab"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:fontFamily="@font/poppins_semibold"
-        android:includeFontPadding="false"
-        android:textColor="@color/color_FF4E5969"
-        android:textSize="16sp"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
-        tools:text="Online Play" />
+        app:srcCompat="@drawable/common_home_main_tab_selected_ic" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 4 - 0
app/src/main/res/values/attrs.xml

@@ -598,4 +598,8 @@
         <attr name="waveSpeed" format="float" />
         <attr name="showGlow" format="boolean" />
     </declare-styleable>
+
+    <declare-styleable name="BlurView">
+        <attr name="blurOverlayColor" format="color"/>
+    </declare-styleable>
 </resources>

+ 4 - 0
app/src/main/res/values/dimens.xml

@@ -3,6 +3,10 @@
     <dimen name="common_button_height">48dp</dimen>
     <dimen name="common_button_margin_bottom">24dp</dimen>
 
+    <dimen name="main_bottom_bar_height">60dp</dimen>
+    <dimen name="main_bottom_bar_margin_bottom">36dp</dimen>
+    <dimen name="main_content_padding_bottom">120dp</dimen>
+
     <dimen name="wheel_item_width">160dp</dimen>
     <dimen name="wheel_item_height">40dp</dimen>
     <dimen name="wheel_divider_height">1dp</dimen>

+ 1 - 1
gradle/libs.versions.toml

@@ -157,7 +157,7 @@ appleAppauth = "0.11.1"
 tiktok = "2.3.0"
 
 # frame
-frameBom = "6.1.3"
+frameBom = "6.1.5"
 
 frameRouterCompiler = "6.0.0"
 frameTrace = "1.0.0"

+ 2 - 3
module/account/src/main/java/com/adealink/weparty/account/login/LoginDialog.kt

@@ -25,6 +25,7 @@ import com.adealink.frame.router.annotation.RouterUri
 import com.adealink.frame.util.DisplayUtil
 import com.adealink.frame.util.copyToClipBoard
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.App
 import com.adealink.weparty.account.R
 import com.adealink.weparty.account.constant.AccountLoginAuthCancelError
@@ -64,10 +65,8 @@ class LoginDialog : BaseDialogFragment(R.layout.dialog_login) {
         super.initViews()
         activity?.let { act ->
             QMUIStatusBarHelper.setStatusBarLightMode(act)
-
-            val statusBarHeight = DisplayUtil.getStatusBarHeight(act)
             binding.btnClose.updateLayoutParams<ConstraintLayout.LayoutParams> {
-                topMargin = statusBarHeight + 12.dp()
+                topMargin = act.statusBarHeight() + 12.dp()
             }
         }
 

+ 2 - 4
module/account/src/main/java/com/adealink/weparty/account/register/fragment/CompleteUserInfoFragment.kt

@@ -10,9 +10,9 @@ import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.formatTime
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.account.R
 import com.adealink.weparty.account.databinding.FragmentRegisterCompleteUserinfoBinding
 import com.adealink.weparty.account.register.dialog.ModifyAvatarDialog
@@ -34,9 +34,7 @@ class CompleteUserInfoFragment : BaseFragment(R.layout.fragment_register_complet
     override fun initViews() {
         super.initViews()
         binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
         binding.topBar.backCallback = {
             activity?.onBackPressed()

+ 2 - 4
module/account/src/main/java/com/adealink/weparty/account/register/fragment/SelectCategoryFragment.kt

@@ -10,8 +10,8 @@ import com.adealink.frame.base.AppBase
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.AppModule
 import com.adealink.weparty.account.R
 import com.adealink.weparty.account.databinding.FragmentRegisterSelectCategoryBinding
@@ -51,9 +51,7 @@ class SelectCategoryFragment : BaseFragment(R.layout.fragment_register_select_ca
     override fun initViews() {
         super.initViews()
         binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
         binding.topBar.backCallback = {
             activity?.onBackPressed()

+ 2 - 4
module/account/src/main/java/com/adealink/weparty/account/register/fragment/SelectGenderFragment.kt

@@ -5,8 +5,8 @@ import androidx.core.view.updateLayoutParams
 import androidx.fragment.app.activityViewModels
 import com.adealink.frame.aab.util.getCompatColor
 import com.adealink.frame.mvvm.view.viewBinding
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.account.R
 import com.adealink.weparty.account.databinding.FragmentRegisterSelectGenderBinding
 import com.adealink.weparty.account.register.viewmodel.RegisterProfileViewModel
@@ -23,9 +23,7 @@ class SelectGenderFragment : BaseFragment(R.layout.fragment_register_select_gend
     override fun initViews() {
         super.initViews()
         binding.topBar.updateLayoutParams <ConstraintLayout.LayoutParams>{
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
         binding.topBar.backCallback = {
             activity?.onBackPressed()

+ 11 - 4
module/im/src/main/java/com/adealink/weparty/im/list/SessionHomeListFragment.kt

@@ -1,13 +1,15 @@
 package com.adealink.weparty.im.list
 
+import android.os.Bundle
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.updateLayoutParams
 import androidx.fragment.app.viewModels
+import com.adealink.frame.aab.util.getCompatDimension
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseFragment
 import com.adealink.weparty.im.R
 import com.adealink.weparty.im.databinding.FragmentSessionHomeListBinding
@@ -15,6 +17,7 @@ import com.adealink.weparty.im.list.viewmodel.SessionListViewModel
 import com.adealink.weparty.im.viewmodel.IMViewModelFactory
 import com.adealink.weparty.module.im.IM
 import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.R as APP_R
 
 
 @RouterUri(
@@ -30,9 +33,7 @@ class SessionHomeListFragment : BaseFragment(R.layout.fragment_session_home_list
     override fun initViews() {
         super.initViews()
         binding.clTopBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
         binding.ivSearch.onClick {
             goSearch()
@@ -43,6 +44,12 @@ class SessionHomeListFragment : BaseFragment(R.layout.fragment_session_home_list
 
         //val fragment = TUIConversationMinimalistFragment()
         val fragment = SessionListFragment()
+        fragment.arguments = Bundle().apply {
+            putInt(
+                IM.SessionList.EXTRA_LAST_ITEM_BOTTOM,
+                getCompatDimension(APP_R.dimen.main_content_padding_bottom).toInt()
+            )
+        }
         childFragmentManager
             .beginTransaction()
             .add(R.id.fl_content, fragment)

+ 10 - 2
module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt

@@ -15,6 +15,8 @@ import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
 import com.adealink.frame.util.AppUtil
 import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
 import com.adealink.weparty.im.R
 import com.adealink.weparty.im.comp.SessionPermissionComp
 import com.adealink.weparty.im.databinding.FragmentSessionListBinding
@@ -58,8 +60,12 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
     }
 
     private fun initSessionList() {
+        val lastItemBottom = arguments?.getInt(IM.SessionList.EXTRA_LAST_ITEM_BOTTOM) ?: 0
         binding.conversationLayout.layoutManager =
             LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+        binding.conversationLayout.addItemDecoration(
+            VerticalSpaceItemDecoration(10.dp(), lastSpaceHeight = lastItemBottom)
+        )
 
         sessionAdapter.register(OfficialListItemViewBinder(this))
         sessionAdapter.register(SessionListItemViewBinder(this))
@@ -117,8 +123,10 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
         conversationInfo ?: return
         activity?.let { act ->
             Router.build(act, IM.Session.PATH)
-                .putExtra(IM.Session.EXTRA_CHAT_TYPE,
-                    if (conversationInfo.isGroup) V2TIMConversation.V2TIM_GROUP else V2TIMConversation.V2TIM_C2C)
+                .putExtra(
+                    IM.Session.EXTRA_CHAT_TYPE,
+                    if (conversationInfo.isGroup) V2TIMConversation.V2TIM_GROUP else V2TIMConversation.V2TIM_C2C
+                )
                 .putExtra(IM.Session.EXTRA_CHAT_ID, conversationInfo.id)
                 .putExtra(IM.Session.EXTRA_CHAT_NAME, conversationInfo.title)
                 .putExtra(IM.Session.EXTRA_CHAT_DRAFT_TEXT, conversationInfo.draft?.draftText)

+ 0 - 20
module/im/src/main/res/layout/fragment_session_list.xml

@@ -30,24 +30,4 @@
         app:layout_goneMarginTop="0dp"
         tools:listitem="@layout/layout_session_list_item" />
 
-    <!--    <com.scwang.smart.refresh.layout.SmartRefreshLayout-->
-    <!--        android:id="@+id/refresh_layout"-->
-    <!--        android:layout_width="0dp"-->
-    <!--        android:layout_height="0dp"-->
-    <!--        android:layout_marginTop="15dp"-->
-    <!--        app:layout_constraintBottom_toBottomOf="parent"-->
-    <!--        app:layout_constraintEnd_toEndOf="parent"-->
-    <!--        app:layout_constraintStart_toStartOf="parent"-->
-    <!--        app:layout_constraintTop_toBottomOf="@id/v_open_notification"-->
-    <!--        app:layout_goneMarginTop="0dp">-->
-
-    <!--        <androidx.recyclerview.widget.RecyclerView-->
-    <!--            android:id="@+id/v_list"-->
-    <!--            android:layout_width="match_parent"-->
-    <!--            android:layout_height="match_parent"-->
-    <!--            android:clipToPadding="true"-->
-    <!--            tools:listitem="@layout/layout_session_list_item" />-->
-
-    <!--    </com.scwang.smart.refresh.layout.SmartRefreshLayout>-->
-
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 2
module/order/src/main/java/com/adealink/weparty/order/OrderDetailActivity.kt

@@ -5,7 +5,7 @@ import androidx.core.view.updateLayoutParams
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.module.order.Order
 import com.adealink.weparty.order.databinding.ActivityOrderDetailBinding
@@ -28,7 +28,7 @@ class OrderDetailActivity : BaseActivity() {
         super.initViews()
         setContentView(binding.root)
         binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = getStatusBarHeight(this@OrderDetailActivity)
+            topMargin = this@OrderDetailActivity.statusBarHeight()
         }
 
 

+ 2 - 2
module/order/src/main/java/com/adealink/weparty/order/OrderListActivity.kt

@@ -8,7 +8,7 @@ import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.module.order.Order
@@ -37,7 +37,7 @@ class OrderListActivity : BaseActivity(), OrderListItemViewBinder.OrderListListe
         super.initViews()
         setContentView(binding.root)
         binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = getStatusBarHeight(this@OrderListActivity)
+            topMargin = this@OrderListActivity.statusBarHeight()
         }
 
         listAdapter.register(OrderListItemViewBinder())

+ 4 - 4
module/playmate/src/main/java/com/adealink/weparty/playmate/list/GuestPlaymateHomeFragment.kt

@@ -12,7 +12,7 @@ import com.adealink.weparty.commonui.BaseFragment
 import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BaseTabFragmentStateAdapter
-import com.adealink.weparty.databinding.LayoutMainTabBinding
+import com.adealink.weparty.databinding.LayoutHomeTabBinding
 import com.adealink.weparty.module.playmate.Playmate
 import com.adealink.weparty.module.playmate.data.PlaymateCategoryData
 import com.adealink.weparty.playmate.R
@@ -81,7 +81,7 @@ class GuestPlaymateHomeFragment : BaseFragment(R.layout.fragment_playmate_home)
         ) { tabLayout, position ->
             tabLayout.setCustomView(com.adealink.weparty.R.layout.layout_home_tab)
             tabLayout.customView?.let { tabView ->
-                val tabViewBinding = LayoutMainTabBinding.bind(tabView)
+                val tabViewBinding = LayoutHomeTabBinding.bind(tabView)
                 onConfigureTab(tabViewBinding, position)
                 updateTabView(
                     tabLayout,
@@ -105,7 +105,7 @@ class GuestPlaymateHomeFragment : BaseFragment(R.layout.fragment_playmate_home)
         })
     }
 
-    private fun onConfigureTab(binding: LayoutMainTabBinding, position: Int) {
+    private fun onConfigureTab(binding: LayoutHomeTabBinding, position: Int) {
         binding.tvTab.text = pageAdapter.getTabName(position)
     }
 
@@ -114,7 +114,7 @@ class GuestPlaymateHomeFragment : BaseFragment(R.layout.fragment_playmate_home)
         isSelected: Boolean
     ) {
         tabView?.customView?.let { tabView ->
-            val tabViewBinding = LayoutMainTabBinding.bind(tabView)
+            val tabViewBinding = LayoutHomeTabBinding.bind(tabView)
             if (isSelected) {
                 tabViewBinding.ivTabBg.show()
                 tabViewBinding.tvTab.setTextColor(getCompatColor(com.adealink.weparty.R.color.color_FF1D2129))

+ 4 - 4
module/playmate/src/main/java/com/adealink/weparty/playmate/list/PlaymateHomeFragment.kt

@@ -12,7 +12,7 @@ import com.adealink.weparty.commonui.BaseFragment
 import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BaseTabFragmentStateAdapter
-import com.adealink.weparty.databinding.LayoutMainTabBinding
+import com.adealink.weparty.databinding.LayoutHomeTabBinding
 import com.adealink.weparty.module.playmate.Playmate
 import com.adealink.weparty.module.playmate.data.PlaymateCategoryData
 import com.adealink.weparty.playmate.R
@@ -81,7 +81,7 @@ class PlaymateHomeFragment : BaseFragment(R.layout.fragment_playmate_home) {
         ) { tabLayout, position ->
             tabLayout.setCustomView(com.adealink.weparty.R.layout.layout_home_tab)
             tabLayout.customView?.let { tabView ->
-                val tabViewBinding = LayoutMainTabBinding.bind(tabView)
+                val tabViewBinding = LayoutHomeTabBinding.bind(tabView)
                 onConfigureTab(tabViewBinding, position)
                 updateTabView(
                     tabLayout,
@@ -105,7 +105,7 @@ class PlaymateHomeFragment : BaseFragment(R.layout.fragment_playmate_home) {
         })
     }
 
-    private fun onConfigureTab(binding: LayoutMainTabBinding, position: Int) {
+    private fun onConfigureTab(binding: LayoutHomeTabBinding, position: Int) {
         binding.tvTab.text = pageAdapter.getTabName(position)
     }
 
@@ -114,7 +114,7 @@ class PlaymateHomeFragment : BaseFragment(R.layout.fragment_playmate_home) {
         isSelected: Boolean
     ) {
         tabView?.customView?.let { tabView ->
-            val tabViewBinding = LayoutMainTabBinding.bind(tabView)
+            val tabViewBinding = LayoutHomeTabBinding.bind(tabView)
             if (isSelected) {
                 tabViewBinding.ivTabBg.show()
                 tabViewBinding.tvTab.setTextColor(getCompatColor(com.adealink.weparty.R.color.color_FF1D2129))

+ 8 - 1
module/playmate/src/main/java/com/adealink/weparty/playmate/list/PlaymateHomeListFragment.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.playmate.list
 
 import android.os.Bundle
+import com.adealink.frame.aab.util.getCompatDimension
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
@@ -14,6 +15,7 @@ import com.adealink.weparty.module.playmate.data.TAG_PLAYMATE_LIST
 import com.adealink.weparty.playmate.R
 import com.adealink.weparty.playmate.databinding.FragmentPlaymateHomeListBinding
 import com.adealink.weparty.playmate.list.comp.PlaymateTopCategoryComp
+import com.adealink.weparty.R as APP_R
 
 @RouterUri(path = [Playmate.HomeList.PATH], desc = "陪玩列表")
 class PlaymateHomeListFragment : BaseFragment(R.layout.fragment_playmate_home_list) {
@@ -73,7 +75,12 @@ class PlaymateHomeListFragment : BaseFragment(R.layout.fragment_playmate_home_li
         if (listFragment.isAdded) {
             return
         }
-        listFragment.arguments = this@PlaymateHomeListFragment.arguments
+        listFragment.arguments = (this@PlaymateHomeListFragment.arguments ?: Bundle()).apply {
+            putInt(
+                Playmate.Common.EXTRA_LAST_ITEM_BOTTOM,
+                getCompatDimension(APP_R.dimen.main_content_padding_bottom).toInt()
+            )
+        }
         childFragmentManager.beginTransaction()
             .replace(binding.flContent.id, listFragment, Playmate.List.PATH)
             .commitAllowingStateLoss()

+ 9 - 1
module/playmate/src/main/java/com/adealink/weparty/playmate/list/PlaymateListFragment.kt

@@ -45,6 +45,9 @@ class PlaymateListFragment : BaseFragment(R.layout.fragment_playmate_list),
     @BindExtra(Playmate.Common.EXTRA_CATEGORY)
     var category: PlaymateCategoryData? = null
 
+    @BindExtra(Playmate.Common.EXTRA_LAST_ITEM_BOTTOM)
+    var lastItemBottom: Int? = null
+
     private val binding by viewBinding(FragmentPlaymateListBinding::bind)
     private val listAdapter by fastLazy { MultiTypeListAdapter(BaseListDiffUtil()) }
     private val playmateListViewModel by viewModels<PlaymateListViewModel> { PlaymateViewModelFactory() }
@@ -80,7 +83,12 @@ class PlaymateListFragment : BaseFragment(R.layout.fragment_playmate_list),
         listAdapter.register(PlaymateListItemViewBinder(this))
         binding.vList.adapter = listAdapter
         binding.vList.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
-        binding.vList.addItemDecoration(VerticalSpaceItemDecoration(10.dp()))
+        binding.vList.addItemDecoration(
+            VerticalSpaceItemDecoration(
+                10.dp(),
+                lastSpaceHeight = lastItemBottom ?: 0
+            )
+        )
     }
 
     override fun initComponents() {

+ 2 - 1
module/playmate/src/main/res/layout/fragment_playmate_list.xml

@@ -25,10 +25,11 @@
 
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/v_list"
+            style="@style/CommonVerticalFade"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:clipToPadding="true"
             android:paddingTop="8dp"
+            android:requiresFadingEdge="vertical"
             tools:listitem="@layout/item_playmate_home_list" />
 
     </com.scwang.smart.refresh.layout.SmartRefreshLayout>

+ 2 - 2
module/profile/src/main/java/com/adealink/weparty/profile/edit/EditProfileActivity.kt

@@ -10,8 +10,8 @@ import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.BindExtra
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.toast.util.showFailedToast
 import com.adealink.weparty.commonui.toast.util.showToast
@@ -48,7 +48,7 @@ class EditProfileActivity : BaseActivity() {
         super.initViews()
         setContentView(binding.root)
         binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = getStatusBarHeight(this@EditProfileActivity)
+            topMargin = this@EditProfileActivity.statusBarHeight()
         }
 
         binding.tvGenderMale.onClick {

+ 10 - 4
module/profile/src/main/java/com/adealink/weparty/profile/me/MeFragment.kt

@@ -4,14 +4,15 @@ import android.text.SpannableStringBuilder
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.updateLayoutParams
 import androidx.fragment.app.viewModels
+import com.adealink.frame.aab.util.getCompatDimension
 import com.adealink.frame.aab.util.getCompatDrawable
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.ext.findAndSetSpan
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
 import com.adealink.frame.util.onClick
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseFragment
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.widget.BottomDialogFragment
@@ -24,6 +25,7 @@ import com.adealink.weparty.profile.databinding.FragmentMeBinding
 import com.adealink.weparty.profile.me.comp.MeHeaderComp
 import com.adealink.weparty.profile.viewmodel.ProfileViewModel
 import com.adealink.weparty.profile.viewmodel.ProfileViewModelFactory
+import com.adealink.weparty.R as APP_R
 
 @RouterUri(path = [Profile.Me.PATH], desc = "我的页面")
 class MeFragment : BaseFragment(R.layout.fragment_me) {
@@ -35,10 +37,14 @@ class MeFragment : BaseFragment(R.layout.fragment_me) {
     override fun initViews() {
         super.initViews()
         binding.svContent.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = activity?.let { act ->
-                getStatusBarHeight(act)
-            } ?: 0
+            topMargin = activity.statusBarHeight()
         }
+        binding.clContent.setPadding(
+            16.dp(),
+            0.dp(),
+            16.dp(),
+            getCompatDimension(APP_R.dimen.main_content_padding_bottom).toInt()
+        )
         val qrCodeText = getCompatString(R.string.profile_generate_qr_code)
         binding.tvQrCode.text = SpannableStringBuilder(qrCodeText).apply {
             findAndSetSpan(

+ 2 - 2
module/profile/src/main/java/com/adealink/weparty/profile/search/SearchActivity.kt

@@ -8,7 +8,7 @@ import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.util.DisplayUtil.getStatusBarHeight
+import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.module.profile.Profile
@@ -35,7 +35,7 @@ class SearchActivity : BaseActivity(), SearchItemViewBinder.OnResultClickListene
         super.initViews()
         setContentView(binding.root)
         binding.clTop.updateLayoutParams<ConstraintLayout.LayoutParams> {
-            topMargin = getStatusBarHeight(this@SearchActivity)
+            topMargin = this@SearchActivity.statusBarHeight()
         }
 
         listAdapter.register(SearchItemViewBinder(this))

+ 4 - 3
module/profile/src/main/res/layout/fragment_me.xml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/color_FFF1F2F5">
@@ -23,8 +24,8 @@
             android:id="@+id/cl_content"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:paddingHorizontal="16dp"
-            android:paddingBottom="60dp">
+            tools:paddingBottom="60dp"
+            tools:paddingHorizontal="16dp">
 
             <!-- 头部信息 -->
             <include
@@ -307,7 +308,7 @@
         android:id="@+id/btn_logout"
         android:layout_width="wrap_content"
         android:layout_height="28dp"
-        android:layout_marginBottom="24dp"
+        android:layout_marginBottom="200dp"
         android:background="@drawable/profile_logout_button"
         android:gravity="center"
         android:includeFontPadding="false"