Android PopupWindow 详解

本文深入剖析了Android中PopupWindow组件的工作原理,包括构造函数、关键属性及如何通过showAtLocation()和showAsDropDown()方法展示。同时提供了一个包含二级弹窗及点击空白处关闭功能的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


一、popupwindow源码解析

popupwindow随着sdk版本的更替,也有着较大的改动,如项目中需适配较低版本的api,请在使用popupwindow时多加注意,本文就sdk22的源代码做简要分析。

首先,让我们来看一下构造函数
    public PopupWindow() {
        this(null, 0, 0);
    }
    public PopupWindow(View contentView) {
        this(contentView, 0, 0);
    }
    public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            mContext = contentView.getContext();
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }

我们可以看出,在popupwindow中,有几个非常重要的属性 contentView, width, height。contentView 是设置popupwindow要显示的 view,width和height是设置popupwindow的宽和高,这三个属性都是不可缺少的,在构造函数中未设置这三个值的,在显示popupwindow之前,需要通过 setContentView(), setWidth(), setHeight(),去设置参数的值。而focusable 是设置popupwindow是否可以获取焦点,在需要进行焦点UI变更的应用中,需要去设置该值。


popupwindow的显示与dialog的不同,不是单纯的使用show函数还显示,他是通过showAtLocation()或者showAsDropDown()来显示,让我们来看下源码。

    public void showAtLocation(View parent, int gravity, int x, int y) {
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }
    public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        unregisterForScrollChanged();

        mIsShowing = true;
        mIsDropdown = false;

        WindowManager.LayoutParams p = createPopupLayout(token);
        p.windowAnimations = computeAnimationResource();
       
        preparePopup(p);
        if (gravity == Gravity.NO_GRAVITY) {
            gravity = Gravity.TOP | Gravity.START;
        }
        p.gravity = gravity;
        p.x = x;
        p.y = y;
        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
        invokePopup(p);
    }

这种显示方式也可以叫做内部显示,是基于函数中绑定的WindowsToken的内部的相对位置来显示。gravity是在父容器内部显示的位置,x,y为偏移量。

代码中,unregisterForScrollChanged()函数是移除滚动监听,computeAnimationResource()是设置弹出动画,动画的具体效果,需要在show之前通过setAnimationStyle()去设置动画的配置。

而preparePopup()处理显示之前要做的工作,主要是设置布局,让我们来看下源码

    private void preparePopup(WindowManager.LayoutParams p) {
        if (mContentView == null || mContext == null || mWindowManager == null) {
            throw new IllegalStateException("You must specify a valid content view by "
                    + "calling setContentView() before attempting to show the popup.");
        }

        if (mBackground != null) {
            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
            int height = ViewGroup.LayoutParams.MATCH_PARENT;
            if (layoutParams != null &&
                    layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                height = ViewGroup.LayoutParams.WRAP_CONTENT;
            }

            // when a background is available, we embed the content view
            // within another view that owns the background drawable
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackground(mBackground);
            popupViewContainer.addView(mContentView, listParams);

            mPopupView = popupViewContainer;
        } else {
            mPopupView = mContentView;
        }

        mPopupView.setElevation(mElevation);
        mPopupViewInitialLayoutDirectionInherited =
                (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
        mPopupWidth = p.width;
        mPopupHeight = p.height;
    }

可以看到里面主要的逻辑是,当mBackground!=null时,Popupwindow的布局会在mContentView外再包一层PopupWindowContainer,而当mBackground==null时,直接设置Popupwindow的布局 为mContentVIew。

PopupWindowContainer是PopupWindow的一个内部类,因为代码较长,我们贴出来一部分讲解。

    private class PopupViewContainer extends FrameLayout {
        public PopupViewContainer(Context context) {
            super(context);
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
            
            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }
    }

可以看到,PopupWindowContainer继承自FrameLayout,并且绑定的上下文为PopupWindow的上下文,所以说若在show的时候,mBackground!=null则会在mContentView外面再套一层布局,那么这层布局有什么用处呢?

在贴出来的代码中可以看到

if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            }

点击PopupWindow外部,使得PopupWindow消失的监听是在这里写出的,因此,需要实现点击外部消失的功能,除了要设置ousideTouchable以外,还需要设置一个背景,即使这个背景为透明的。除了该监听外,返回键消失的监听也在PopupWindowContainer中实现,这里就不贴出代码了。


下面我们来看下showAsDropDown()

    public void showAsDropDown(View anchor) {
        showAsDropDown(anchor, 0, 0);
    }
    public void showAsDropDown(View anchor, int xoff, int yoff) {
        showAsDropDown(anchor, xoff, yoff, DEFAULT_ANCHORED_GRAVITY);
    }
    public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        if (isShowing() || mContentView == null) {
            return;
        }

        registerForScrollChanged(anchor, xoff, yoff, gravity);

        mIsShowing = true;
        mIsDropdown = true;

        WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
        preparePopup(p);

        updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, gravity));

        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;

        p.windowAnimations = computeAnimationResource();

        invokePopup(p);
    }

showAsDropDown()也可以称为外部显示。anchor是参照的控件,xoff, yoff 是偏移量,是相对于anchor左下角的偏移,gravity 是设置PopupWindow显示的位置,但不是相对于anchor的方向,仅是一个参考。需要注意的是,showAsDropDown(View anchor, int xoff, int yoff, int gravity)是在api19中加入的。

showAsDropDown()的总体过程和showAtLlocation大同小异,下面我们来看下不同的地方。

首先,showAtLocation是取消注册滑动监听,而showAsDropDown是注册滑动监听,所以我们可以看出,showAtlocation不会以为parentView 的滑动而受到影响,而showAsDropDown会受到anchor滑动的影响,会一直跟着anchor的移动。

其次,我们会发现多了一个updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, gravity));的函数,其实呢,findDropDownposition()是设置需要显示的位置到布局参数中,接着调用updateAboveAnchor()设置在指定View之上显示时的Drawable,这里就不详细分析了。


好了,源代码就简单分析到这里,我们来看个Demo吧。

二、一个popupwindow的Demo

这个demo实现的是左弹出的二级popupwindow,以及点击空白处会收起popupwindow的功能。
需要注意的是,点击空白处消失,必须要对popupwindow设置背景图片。




以下是代码:
MainActivity.java
package com.example.liqk.popupwindowdemo;

import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {

    FirstPopupWindow firstPopupWindow;
    Button showPopupWindow;

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

        firstPopupWindow = new FirstPopupWindow(this);
        showPopupWindow = (Button)findViewById(R.id.showPopupWindowButton);

        showPopupWindow.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View view) {
                firstPopupWindow.show(MainActivity.this);
            }
        });

    }



    @Override
    public boolean onCreateOptionsMenu(Menu menu){

        firstPopupWindow.show(MainActivity.this);

        return true;
    }
}

FirstPopupWindow.java
package com.example.liqk.popupwindowdemo;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.SimpleAdapter;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by liqk on 2016/8/8.
 */
public class FirstPopupWindow extends PopupWindow {

    ListView listView;
    Context mContext;
    RedPopupWindow redPopupWindow;
    GreenPopupWindow greenPopupWindow;
    Activity activity;

    public FirstPopupWindow(Context context){
        View view = LayoutInflater.from(context).inflate(R.layout.popupwindow_first, null);
        setContentView(view);

        mContext = context;

        initViews(view);
        initValues(context);
        initListeners();
    }


    private void initViews(View view){
        listView = (ListView)view.findViewById(R.id.popupWindow_listView);
        String[] fuck = {"红色二级弹出框","绿色二级弹出框"};
        Adapter adapter = new ArrayAdapter<String>(mContext,R.layout.listview_row,fuck);
        listView.setAdapter((ListAdapter) adapter);

        redPopupWindow = new RedPopupWindow(this.mContext);
        greenPopupWindow = new GreenPopupWindow(this.mContext);
    }

    private void initValues(Context context){
        // 设置popwindow窗口大小
        setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);

        // 设置菜单显示隐藏动作
        setAnimationStyle(R.style.first_animation_style);
        setBackgroundDrawable(new BitmapDrawable());

        // 点击外面可以隐藏菜单
        setOutsideTouchable(true);

        // 使popwindow获取焦点
        setFocusable(true);
    }

    private void initListeners(){
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                switch (i){
                    case 0:
                        redPopupWindow.show(activity);
                        break;
                    case 1:
                        greenPopupWindow.show(activity);
                        break;
                    default:
                        return;
                }
            }
        });
    }



    public void show(Activity activity) {
        this.activity = activity;
        showAtLocation(activity.getWindow().getDecorView(), Gravity.LEFT, 0, 0);
    }
}
这里需要注意,setbackgrounddrawable()是设置popupwindow的背景,只有设置了这个才能使得setoutsidetouchable生效,即可点击空白处使popupwindow消失。

popupwindow_first.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:background="#0066ff"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/first_title"
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:paddingTop="30dp"
        android:text="第一个弹出框"
        android:textSize="20dp"
        android:textColor="#ffffff"
        />

    <ListView
        android:id="@+id/popupWindow_listView"
        android:paddingTop="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>


</LinearLayout>

在这个弹出框中使用的动画,动画会在另一章中讲解,这里贴出代码:
first_anim_show.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="400"
        android:fromXDelta="-200%p"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="0"/>

</set>
first_anim_hide.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="400"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="-200%p"
        android:toYDelta="0"/>

</set>
style.xml
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="first_animation_style">
        <item name="android:windowEnterAnimation">@anim/first_anim_show</item>
        <item name="android:windowExitAnimation">@anim/first_anim_hide</item>
    </style>

</resources>

其他类似代码就不放上来了,另外两个popupwindow与firstPopupwindow类似。








                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值