1、效果图
2、前言
1、页面由 MagicIndicator + ViewPager2 + Fragment 实现;
2、下拉框是基于WindowManager实现;
3、我使用PopupWindow实现下拉框时,发现一个问题,PopupWindow 在窗口显示的情况下,无法直接从外部修改布局,必须先dismiss;
PopupWindow源码:
public void setContentView(View contentView) {if (isShowing()) {return;}... ...
}public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {if (isShowing() || !hasContentView()) {return;}... ...
}
4、如果先dismiss再添加,属于重新创建布局,切换生硬,会出现闪烁,影响用户体验,就像这样;
那就没办法了,自己实现;
观摩PopupWindow源码发现它是基于windowManager实现的,照葫芦画瓢,自定义一个
3、自定义下拉框
AffiliatedBottomWindow
package com.example.myapplication.common;import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;import androidx.annotation.NonNull;import com.example.myapplication.util.BarUtils;
import com.example.myapplication.util.CustomizeUtils;
import com.example.myapplication.util.ScreenUtils;/*** 挂靠在某个view下面的悬浮窗* <p>* 和PopupWindow一样都是基于WindowManager实现的;* <p>** 和PopupWindow区别:* PopupWindow 在窗口显示的情况下,无法直接从外部修改布局,必须先dismiss,* 再重新创建,切换会出现闪烁,用户体验差;* <p>* PopupWindow源码:* public void setContentView(View contentView) {* if (isShowing()) {* return;* }* ... ...* }** <p>* 为了避免这种闪烁,基于WindowManager写了这个 AffiliatedBottomWindow 底部挂靠悬浮框;* <p>*** 注意:单例,使用完成后一定要清空,不然无法创建实例 if (instance == null) {}* public void clear() {* instance = null;* }*/
public class AffiliatedBottomWindow {private final Context context;private static CustomizeUtils.AntiShake antiShake;private WindowManager windowManager;@SuppressLint("StaticFieldLeak")private static AffiliatedBottomWindow instance;// 根view@SuppressLint("StaticFieldLeak")private static ViewGroup rootView;// 根view中的子viewprivate ViewGroup rootChildView;// true:使用 根view中的子view 作为内容布局的容器private boolean useRootChildView = false;// 显示/隐藏 过渡动画时长public static int animatorDuration = 200;private WindowManager.LayoutParams windowLayoutParams;private AffiliatedBottomWindow(Context context) {this.context = context;createWindowManager();antiShake = new CustomizeUtils.AntiShake(500);}public WindowManager getWindowManager() {return windowManager;}public void setWindowManager(WindowManager windowManager) {this.windowManager = windowManager;}public AffiliatedBottomWindow getInstance() {return instance;}public WindowManager.LayoutParams getWindowLayoutParams() {return windowLayoutParams;}public void setWindowLayoutParams(WindowManager.LayoutParams windowLayoutParams) {this.windowLayoutParams = windowLayoutParams;}public ViewGroup getRootView() {return rootView;}public void setRootView(ViewGroup rootView) {AffiliatedBottomWindow.rootView = rootView;// 初始化隐藏布局AffiliatedBottomWindow.rootView.setVisibility(View.GONE);}public boolean isUseRootChildView() {return useRootChildView;}public void setUseRootChildView(boolean useRootChildView) {this.useRootChildView = useRootChildView;}// 显示 和 隐藏 过渡透明度动画public static void alphaAnimation(boolean show) {if (antiShake.isFastClick()) {ValueAnimator animator;if (show) {if (rootView.getVisibility() == View.VISIBLE) {return;}rootView.setAlpha(0);rootView.setVisibility(View.VISIBLE);//显示animator = ValueAnimator.ofFloat(0f, 1f);animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(@NonNull ValueAnimator animation) {float progress = (float) animation.getAnimatedValue();rootView.setAlpha(progress);}});animator.setDuration(animatorDuration);animator.start();} else {if (rootView.getVisibility() == View.GONE) {return;}//隐藏animator = ValueAnimator.ofFloat(1f, 0f);animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(@NonNull ValueAnimator animation) {float progress = (float) animation.getAnimatedValue();rootView.setAlpha(progress);}});animator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {super.onAnimationEnd(animation);rootView.setVisibility(View.GONE);}});animator.setDuration(animatorDuration);animator.start();}}}/*** 内容布局插入到 根View中** @param context* @param rootView 根布局* @param affiliatedView 挂靠的View,悬浮框会出现在这个View下面*/public static AffiliatedBottomWindow createInstance(Context context,ViewGroup rootView,View affiliatedView) {if (instance == null) {instance = new AffiliatedBottomWindow(context);// 先设置根viewinstance.setRootView(rootView);// 再设置挂靠view,顺序不能乱instance.setAffiliatedView(affiliatedView);}return instance;}/*** 内容布局插入到 根View中的 子View中** @param context* @param rootView 根布局* @param affiliatedView 挂靠的View,悬浮框会出现在这个View下面* @param rootChildView 根view中的子view* @param useChildContainer 是否将 根view中的子view 作为内容布局的容器*/public static AffiliatedBottomWindow createInstance(Context context,ViewGroup rootView,View affiliatedView,ViewGroup rootChildView,boolean useChildContainer) {if (instance == null) {instance = new AffiliatedBottomWindow(context);instance.setUseRootChildView(useChildContainer);instance.setRootChildView(rootChildView);// 先设置根viewinstance.setRootView(rootView);// 再设置挂靠view,顺序不能乱instance.setAffiliatedView(affiliatedView);}return instance;}public void setAffiliatedView(View affiliatedView) {// 设置悬浮框宽/高windowLayoutParams.width = affiliatedView.getWidth();// 剩余空间高度windowLayoutParams.height = ScreenUtils.getScreenHeight(context) - affiliatedView.getBottom();// 设置悬浮框位置windowLayoutParams.gravity = Gravity.TOP;// 减去状态栏高度,沉浸式布局,// 如果不是沉浸式布局,扩展重写此方法windowLayoutParams.y = affiliatedView.getBottom() - BarUtils.getStatusBarHeight(context);// 显示,当前根视图隐藏了,所以不显示windowManager.addView(rootView, windowLayoutParams);}public void setRootChildView(ViewGroup rootChildView) {this.rootChildView = rootChildView;}private void createWindowManager() {if (windowManager != null) {return;}// 创建 windowManager对象windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);// 创建布局参数windowLayoutParams = new WindowManager.LayoutParams();// 设置窗口类型windowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;// 设置悬浮框不可触摸,默认接收事件,会导致底层view,接收不到事件windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;// 背景颜色,设置为透明windowLayoutParams.format = PixelFormat.TRANSPARENT;}// 插入布局public void insertViewLayout(View view) {if (useRootChildView) {if (rootChildView.getChildCount() == 0) {rootChildView.addView(view);} else {rootChildView.removeAllViews();rootChildView.addView(view);}} else {if (rootView.getChildCount() == 0) {rootView.addView(view);} else {rootView.removeAllViews();rootView.addView(view);}}alphaAnimation(true);}// 隐藏窗口public static void dismiss() {alphaAnimation(false);}// 单例,使用完成后一定要清空// 不然 无法创建实例 if (instance == null) {}public void clear() {instance = null;}
}
4、源码
demo东西比较多,是从自己项目里摘录出来的,扩展了MagicIndicator
核心类:AffiliatedBottomWindow
https://github.com/LanSeLianMa/CustomizeBottomWindow