返回

Android PopupWindow 多屏幕适配终极解决方案

Android

Android 中 PopupWindow 多屏幕适配问题解决

遇到 PopupWindow 在不同屏幕尺寸的设备上显示位置不一致?别慌,这篇文章带你解决这个问题。

问题

原提问者做了一个点击按钮后,从屏幕底部弹出的 PopupWindow。他在 720 x 1280 像素(约 294 ppi)的设备上显示正常,底部弹出。 可是在 480 x 854 像素(约 196 ppi)的设备上,却显示在了屏幕中央。

问题原因

根本原因在于:提问者硬编码 了 PopupWindow 的宽高,并且直接使用 showAtLocation 并传入 Gravity.BOTTOM

  1. 硬编码宽高: popupWindow.setWidth(720)popupWindow.setHeight(350) 这两行代码,把 PopupWindow 的宽高固定死了。720 像素的宽度,对于小屏幕设备,可能就直接超出屏幕了;350像素的高度也是个绝对值,在小屏设备上相对于屏幕过高。

  2. showAtLocationGravity.BOTTOM 的配合: 虽然指定了 Gravity.BOTTOM,但在 showAtLocation 方法,最后两个参数 x, y 的偏移,他是基于像素的绝对偏移, 而不是相对的。 你传了 (0, 0) 。在不同分辨率, 特别是不同比例屏幕上就可能有偏差.

解决方案

咱们分几步走,来彻底解决这个问题:

1. 使用 dp (Density-independent Pixels) 代替 px

这是最重要的一步!dp 是 Android 推荐的尺寸单位,它会自动根据屏幕密度进行缩放。

  • 修改布局文件 (camera_popup.xml):
    将所有用到固定数值的地方,如 layout_widthlayout_heightpaddingmargintextSize 等,把单位 px 改成 dp。 同时最好也用上权重layout_weight属性

    比如:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/popup_element"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        android:orientation="vertical"
        android:padding="3dp">  <!--  这里的 padding 最好也用 dp -->
    
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="1dp"
            android:background="@android:color/transparent"
            android:orientation="vertical">
    
            <TextView
                android:id="@+id/Title"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_gravity="center_horizontal"
                android:layout_weight="1"
                android:background="@drawable/curve_shap"
                android:gravity="center_horizontal|center_vertical"
                android:text="Take Photo"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:textColor="@android:color/darker_gray"
                android:textSize="14sp" /> <!--  这里是 sp, 不用改 -->
    
            <Button
                android:id="@+id/button_Camera"
                android:layout_width="match_parent"
                android:layout_height="40dp"
    
                android:layout_gravity="center_horizontal"
                android:layout_weight="1"
                android:background="@drawable/curve_shap"
                android:text="Camera"
                android:textColor="#3A86CF" />
    
            <Button
                android:id="@+id/button_Gallery"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_gravity="center_horizontal"
                android:layout_weight="1"
                android:background="@drawable/curve_shap"
                android:text="Gallery"
                android:textColor="#3A86CF" />
    
    
        </LinearLayout>
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            >
    
            <Button
                android:id="@+id/btnCancelCamera"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_gravity="center_horizontal"
    
                android:background="@drawable/curve_shap"
                android:text="Cancel"
                android:textColor="#3A86CF"
                android:textSize="16sp"
                android:textStyle="bold" />
        </RelativeLayout>
    
    </LinearLayout>
    
  • 代码中设置宽高,也用 dp
    在创建 popupWindow 后,不要再 setWidthsetHeight 设置具体像素值了。 如果真的希望有一个高度,需要这样:

     // 将 dp 转换为像素
     public static int dpToPx(Context context, int dp) {
         return (int) (dp * context.getResources().getDisplayMetrics().density);
     }
    
     //... 创建 popupWindow 的代码 ...
    // popupWindow.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); // 宽度通常用 MATCH_PARENT
     popupWindow.setWidth(dpToPx(getBaseContext(), 250)); //比如你希望是 250dp 这么宽。
     popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);   // 让高度自适应内容
    
     popupWindow.setFocusable(true);
    // popupWindow.showAtLocation(popupView, Gravity.BOTTOM, 0, 0); // 这行先注释掉, 下面会改
     popupWindow.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
    
     // ... 后面按钮的代码 ...
    

2. 使用 showAsDropDown

PopupWindow 有一个更适合这种 "锚定在某个 View 下方弹出" 的方法:showAsDropDown。它能自动处理位置计算。

// 找到你想要 PopupWindow 贴着弹出的那个 View
// 比如, 这里是那个触发弹出的图片按钮 imgProfilePic
imgProfilePic.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {

    // ... 创建 PopupWindow 的代码 (和上面一样, 设置 dp 宽高) ...
    popupWindow.showAsDropDown(v, 0, 0); //在V的正下方
    //如果需要在下方并有一定偏移
     //  popupWindow.showAsDropDown(v, 0, dpToPx(getBaseContext(), 10));  //  向下偏移 10dp
  }
});

通过确定相对位置,能保证绝对底部展示

  • showAsDropDown(View anchor, int xoff, int yoff):

    • anchor: 就是 PopupWindow 要贴着的那个 View。
    • xoff: 水平偏移量 (以像素为单位)。正值向右,负值向左。
    • yoff: 垂直偏移量 (以像素为单位)。正值向下,负值向上。 (注意这里即使你希望在底部,也不用计算什么屏幕高度了,给个小正值微调就好)
  • showAsDropDown 还可能要处理一个问题 :
    当空间不足, PopupWindow可能会向上显示,这时需要手动检测进行额外判断和处理.

3.处理极端情况:超小屏幕或方向变化

  • 处理小屏幕: 有些设备真的太小了……即使你用了 dp,底部 PopupWindow 还是可能超出屏幕。这种情况你可以在代码里判断一下:

       // 获取屏幕高度
         DisplayMetrics displayMetrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        int screenHeight = displayMetrics.heightPixels;
    
    // 计算PopupWindow 理论高度。  (假设你的 PopupWindow 内容总高度是 contentHeightDp,单位是 dp)
       int popupHeightPx = dpToPx(getBaseContext(), contentHeightDp);
    
    //如果内容高度仍然可能过高(比如大于屏幕高度的一半)进行处理
        if(popupHeightPx > screenHeight /2){
            //要么缩小 PopupWindow内容
            // 要么,将PopupWindow 改成 ScrollView,允许内容滚动.  (这里需要修改布局文件)
    
            //假设修改布局后
             popupWindow.setHeight(screenHeight / 2);   // 最大高度设为屏幕一半
            popupWindow.setOutsideTouchable(true);
        }
    
    
  • 处理横竖屏切换 : 如果你的 App 支持横竖屏切换,记得在 AndroidManifest.xml 里的 Activity 声明中加上:

    <activity
     android:name=".YourActivity"
     android:configChanges="orientation|screenSize|keyboardHidden" />
    
这样, 当屏幕方向变化,Activity 不会重新创建,PopupWindow 也不会自动关闭和重新计算,导致位置错乱。当然了,做了配置后,你需要自己处理 `onConfigurationChanged` 事件,来重新调整布局.

完整代码示例 (结合以上几点)

public class Filter_Screen extends AppCompatActivity {

    private PopupWindow popupWindow;
    private ImageView imgProfilePic;
    private static final int CAMERA_REQUEST = 100;
    private static final int PROFILE_GALLERY = 101;
    // 假设的 PopupWindow 内容高度 (dp)
    private static final int POPUP_CONTENT_HEIGHT_DP = 200;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_filter_screen);

        imgProfilePic = findViewById(R.id.imgProfilePic);

        imgProfilePic.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showPopupWindow(v);
            }
        });
    }

    private void showPopupWindow(View anchorView) {
        LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        View popupView = layoutInflater.inflate(R.layout.camera_popup, null);

        popupWindow = new PopupWindow(popupView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        popupWindow.setFocusable(true);
        popupWindow.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));

        // 处理可能超出屏幕的情况
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        int screenHeight = displayMetrics.heightPixels;
        int popupHeightPx = dpToPx(this, POPUP_CONTENT_HEIGHT_DP);
         if(popupHeightPx > screenHeight /2){
              popupWindow.setHeight(screenHeight / 2);
               popupWindow.setOutsideTouchable(true); // 点击 PopupWindow 外部时关闭
          }
        popupWindow.showAsDropDown(anchorView, 0, dpToPx(this, 5)); // 向下偏移 5dp

        // 按钮点击事件 (和原来基本一样)
        Button btnCamera = popupView.findViewById(R.id.button_Camera);
        Button btnGallery = popupView.findViewById(R.id.button_Gallery);
        Button btnDismiss = popupView.findViewById(R.id.btnCancelCamera);
       btnDismiss.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
              popupWindow.dismiss();
           }
       });

       // 其他事件
       // ...

     }

    // dp 转 px 的工具方法
    public static int dpToPx(Context context, int dp) {
        return (int) (dp * context.getResources().getDisplayMetrics().density);
    }

}

进阶使用技巧

1.自定义动画

setAnimationStyle实现更炫的动画效果,不仅仅局限于上下弹出动画,还能支持渐变等

2.动态更新PopupWindow内容

如果 PopupWindow 内的内容需要经常变化(比如根据网络数据刷新),你可以在显示之后,拿到里面的 View 进行更新,而不用每次都创建新的 PopupWindow。

只要做好以上适配, 再也不用为屏幕碎片化头疼了!