GVKun编程网logo

Android用PopupWindow实现自定义Dailog(android popupwindow使用)

14

如果您对Android用PopupWindow实现自定义Dailog和androidpopupwindow使用感兴趣,那么这篇文章一定是您不可错过的。我们将详细讲解Android用PopupWindo

如果您对Android用PopupWindow实现自定义Dailogandroid popupwindow使用感兴趣,那么这篇文章一定是您不可错过的。我们将详细讲解Android用PopupWindow实现自定义Dailog的各种细节,并对android popupwindow使用进行深入的分析,此外还有关于android dialog,popupwindow,toast 窗口的添加机制、android Dialog和Popupwindow,不显示、Android PopupWindow、Android PopupWindow 实现自定义菜单弹窗效果的实用技巧。

本文目录一览:

Android用PopupWindow实现自定义Dailog(android popupwindow使用)

Android用PopupWindow实现自定义Dailog(android popupwindow使用)

Android的PopupWindow是个很有用的widget,利用它可以实现悬浮窗体的效果,比如实现一个悬浮的菜单,最常见的应用就是在视频播放界面里,做一个工具栏,用来控制播放进度。本文利用PopupWindow来实现一个通用的Dailog,类似Android系统的AlertDailog,从中学习和掌握有关PopupWindow和Dailog的使用和实现细节。

界面效果如图所示,点击 Click 按钮后,弹出对话框提示。


(1).  CustomDailog的布局

首先定义 CustDailog的布局文件,由系统的AlertDailog可以知道,一个对话框包含了三个要素,一个是Title,即标题,一个是Message,即主体内容,还有一个是Button,即确定和取消的按钮,用来与用户交互。因此,布局设计如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="vertical"
  android:background="@drawable/shape_bg"
  android:layout_margin="10dp">
                                                                                                                                                         
  <TextView   
    android:id="@+id/CustomDlgTitle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textandroid:textSize="20sp"
    android:layout_margin="10dp"
    android:gravity="center"/>
                                                                                                                                                           
  <View
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:background="@android:color/darker_gray"/>
                                                                                                                                                           
  <LinearLayout
    android:id="@+id/CustomDlgContentView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:layout_margin="5dp" />
                                                                                                                                                         
  <TextView
    android:id="@+id/CustomDlgContentText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="15sp"
    android:layout_margin="5dp"
    android:paddingLeft="5sp"/>
                                                                                                                                                       
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:layout_margin="5dp" >
                                                                                                                                                         
    <Button
      android:id="@+id/CustomDlgButtonOK"
      android:layout_width="0dp"
      android:layout_weight="0.5"
      android:layout_height="wrap_content"
      android:visibility="gone"/>
                                                                                                                                                             
    <Button
      android:id="@+id/CustomDlgButtonCancel"
      android:layout_width="0dp"
      android:layout_weight="0.5"
      android:layout_height="wrap_content"     
      android:visibility="gone"/>
                                                                                                                                                      
  </LinearLayout>
                                                                                                                                                       
</LinearLayout>

其中,shap_bg.xml 是Dailog的背景的定义文件,你可以修改此文件,来改变Dailog的背景:

<?xml version="1.0" encoding="UTF-8"?>
<shape android:shape="rectangle"
 xmlns:android="http://schemas.android.com/apk/res/android">
  <solid android:color="#e6ecee" />
  <stroke android:width="1.0dip" android:color="@android:color/darker_gray" />
  <corners android:radius="8.0dip" />
</shape>

(2). CustomDailog的定义

CustomDailog的接口,可以类比AlertDailg的接口定义,主要包括如下一些方法:

1.  setTitle 设置标题
2.  setMessage 设置主体内容
3.  setPositiveButton 设置 “确定” 按钮
4.  setNegativeButton 设置 “取消” 按钮
5.  show   显示
6.  dimiss 消失

其定义如下:

package com.ticktick.popdailog;
                                                                         
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
                                                                           
public class CustomDailog {
                                                                        
  private View mParent;
  private PopupWindow mPopupWindow;
  private LinearLayout mRootLayout; 
  private LayoutParams mLayoutParams; 
                                                                        
  //PopupWindow必须有一个ParentView,所以必须添加这个参数
  public CustomDailog(Context context,View parent) {
                                                                          
    mParent = parent;
                                                                          
    LayoutInflater mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);   
                                                                          
    //加载布局文件
    mRootLayout = (LinearLayout)mInflater.inflate(R.layout.custom_dailog,null); 
                                                                              
    mLayoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
  } 
                                                                        
  //设置Dailog的标题
  public void setTitle(String title) {
    TextView mTitle = (TextView)mRootLayout.findViewById(R.id.CustomDlgTitle);
    mTitle.setText(title);
  }
                                                                        
  //设置Dailog的主体内容
  public void setMessage(String message) {
    TextView mMessage = (TextView)mRootLayout.findViewById(R.id.CustomDlgContentText);
    mMessage.setText(message);
  }
                                                                        
  //设置Dailog的“确定”按钮
  public void setPositiveButton(String text,OnClickListener listener ) {
    final Button buttonOK = (Button)mRootLayout.findViewById(R.id.CustomDlgButtonOK);
    buttonOK.setText(text);
    buttonOK.setonClickListener(listener);
    buttonOK.setVisibility(View.VISIBLE);
  }
                                                                        
  //设置Dailog的“取消”按钮
  public void setNegativeButton(String text,OnClickListener listener ) {
    final Button buttonCancel = (Button)mRootLayout.findViewById(R.id.CustomDlgButtonCancel);
    buttonCancel.setText(text);
    buttonCancel.setonClickListener(listener);
    buttonCancel.setVisibility(View.VISIBLE);
  }
                                                                        
  //替换Dailog的“主体”布局
  public void setContentLayout(View layout) {
                                                                          
    TextView mMessage = (TextView)mRootLayout.findViewById(R.id.CustomDlgContentText);
    mMessage.setVisibility(View.GONE);
                                                                          
    LinearLayout contentLayout = (LinearLayout)mRootLayout.findViewById(R.id.CustomDlgContentView);   
    contentLayout.addView(layout);       
  }
                                                                        
  //设置Dailog的长宽
  public void setLayoutParams(int width,int height) {
    mLayoutParams.width = width;
    mLayoutParams.height = height;
  }
                                                                        
  //显示Dailog
  public void show() {
                                                                        
    if(mPopupWindow == null) {
      mPopupWindow = new PopupWindow(mRootLayout,mLayoutParams.width,mLayoutParams.height);
      mPopupWindow.setFocusable(true);
    }
                                                                          
    mPopupWindow.showAtLocation(mParent,Gravity.CENTER,Gravity.CENTER);
  }
                                                                        
  //取消Dailog的显示
  public void dismiss() {
                                                                          
    if(mPopupWindow == null) {
      return;
    }
                                                                          
    mPopupWindow.dismiss();
  }
}

(3). 在Activity中的使用方法

由于 PopupWindow 的显示必须给一个ParentView,在Activity中使用的话,最简单的方法就是将整个activity的“根View”传递给这个PopupWindow,这样就可以在整个屏幕的正中央来显示Dailog,获取Acitivity的根View的方法如下:

findViewById(android.R.id.content)).getChildAt(0);

因此,上面定义的 CunstomDailog的使用方法如下所示:

final CustomDailog dailog = new CustomDailog(this,getRootLayout());
dailog.setTitle("Warning");
dailog.setMessage("This is ticktick's blog!");
dailog.setPositiveButton("OK",new OnClickListener() {    
  @Override
  public void onClick(View v) {
    dailog.dismiss();     
  }
});
dailog.setNegativeButton("Cancel",new OnClickListener() {    
  @Override
  public void onClick(View v) {
  dailog.dismiss();     
  }
});
dailog.show();

到此为止,整个Dailog的实现就介绍到这里了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

android dialog,popupwindow,toast 窗口的添加机制

android dialog,popupwindow,toast 窗口的添加机制

Dialog 窗口添加机制

代码示例

首先举两个例子: 
例子 1 在 Activity 中

  @OnClick(R.id.but)
    void onClick() {
        Log.d("LiaBin", "activity window token:" + this.getWindow().getAttributes().token);

        Dialog dialog = new ProgressDialog(this);
        dialog.show();
        Log.d("LiaBin", "dialog window token:" + dialog.getWindow().getAttributes().token);
    }
输出结果: 
11-21 03:24:38.038 2040-2040/lbb.demo.first D/LiaBin: activity window token:android.os.BinderProxy@18421fac 
11-21 03:24:38.054 2040-2040/lbb.demo.first D/LiaBin: dialog window token:null

例子 2

 @OnClick(R.id.but)
    void onClick() {
        Dialog dialog = new ProgressDialog(getApplicationContext());
        dialog.show();
    }

例子 3

public class WindowService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //重点关注构造函数的参数
        Dialog dialog = new ProgressDialog(this);
        dialog.setTitle("TestDialogContext");
        dialog.show();
    }
}
输出结果都是: 
E/AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window  token null is not for an application 
E/AndroidRuntime: at android.view.ViewRootImpl.setView(ViewRootImpl.java:566) 
E/AndroidRuntime: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:282) 
E/AndroidRuntime: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85) 
E/AndroidRuntime: at android.app.Dialog.show(Dialog.java:298)

为什么会出现以上两种输出结果,看以下分析。

 

Dialog 源码分析
Dialog 是一系列 XXXDialog 的基类,我们可以 new 任意 Dialog 或者通过 Activity 提供的 onCreateDialog (……)、onPrepareDialog (……) 和 showDialog (……) 等方法来管理我们的 Dialog,但是究其实质都是来源于 Dialog 基类,所以我们对于各种 XXXDialog 来说只用分析 Dialog 的窗口加载就可以了。

public class Dialog implements DialogInterface, Window.Callback,
        KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
    ......
    public Dialog(Context context) {
        this(context, 0, true);
    }
    //构造函数最终都调运了这个默认的构造函数
    Dialog(Context context, int theme, boolean createContextThemeWrapper) {
        //默认构造函数的createContextThemeWrapper为true
        if (createContextThemeWrapper) {
            //默认构造函数的theme为0
            if (theme == 0) {
                TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
                        outValue, true);
                theme = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, theme);
        } else {
            mContext = context;
        }
        //mContext已经从外部传入的context对象获得值(一般是个Activity)!!!非常重要,先记住!!!

        //获取WindowManager对象
        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        //为Dialog创建新的Window
        Window w = PolicyManager.makeNewWindow(mContext);
        mWindow = w;
        //Dialog能够接受到按键事件的原因
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        //关联WindowManager与新Window,特别注意第二个参数token为null,也就是说Dialog没有自己的token
        //一个Window属于Dialog的话,那么该Window的mAppToken对象是null
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }
    ......
}

Dialog 构造函数首先把外部传入的参数 context 对象赋值给了当前类的成员(我们的 Dialog 一般都是在 Activity 中启动的,所以这个 context 一般是个 Activity),然后调用 context.getSystemService (Context.WINDOW_SERVICE) 获取 WindowManager,这个 WindowManager 是哪来的呢?先按照上面说的 context 一般是个 Activity 来看待,可以发现这句实质就是 Activity 的 getSystemService 方法,我们看下源码,如下:

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/app/Activity.java

 @Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        }
        //我们Dialog中获得的WindowManager对象就是这个分支
        if (WINDOW_SERVICE.equals(name)) {
            //Activity的WindowManager
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

看见没有,Dialog 中的 WindowManager 成员实质和 Activity 里面是一样的,也就是共用了一个 WindowManager。

回到 Dialog 的构造函数继续分析,在得到了 WindowManager 之后,程序又新建了一个 Window 对象(类型是 PhoneWindow 类型,和 Activity 的 Window 新建过程类似);接着通过 w.setCallback (this) 设置 Dialog 为当前 window 的回调接口,这样 Dialog 就能够接收事件处理了;接着把从 Activity 拿到的 WindowManager 对象关联到新创建的 Window 中。

总结如下:

1.dialog 使用有自己的 window,不同于 activity 的 window
2.dialog 的 mWindowManager 变量其实就是 activity 对象的 mWindowManager 变量,此时注意因为 window 通过 setWindowManager 方法也会复制自己的 mWindowManager,但这个 mWindowManager 是通过 createLocalWindowManager 返回的。不同于 dialog 的 mWindowManager 变量。不要混淆

//Window

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        //在此处创建mWindowManager 
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

//在WindowManagerImpl类中
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

Activity 的 getSystemService 根本没有创建 WindowManager。类似于 PhoneWindow 和 Window 的关系,WindowManager 是一个接口,具体的实现是 WindowManagerImpl。

Application 的 getSystemService ()源码其实是在 ContextImpl 中:有兴趣的可以看看 APP 启动时 Context 的创建:

 @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

SystemServiceRegistry 类用静态字段及方法中封装了一些服务的代理,其中就包括 WindowManagerService

public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
    
    static {
             ...
             registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx.getDisplay());
            }});
            ...
    }

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/view/WindowManagerImpl.java

public WindowManagerImpl(Display display) {
    this(display, null);
}

private WindowManagerImpl(Display display, Window parentWindow) {
    mDisplay = display;
    mParentWindow = parentWindow;
}

因此 context.getApplicationContext ().getSystemService () 最终可以简化为 new WindowManagerImpl (ctx.getDisplay ())。

 


3.activity 覆盖了 context 的 getSystemService 方法,如果 WINDOW_SERVICE,那么返回的是当前 activity 的 mWindowManager 对象

至此 Dialog 的创建过程 Window 处理已经完毕,很简单,所以接下来我们继续看看 Dialog 的 show 与 cancel 方法,如下:

public void show() {
        ......
        if (!mCreated) {
            //回调Dialog的onCreate方法
            dispatchOnCreate(null);
        }
        //回调Dialog的onStart方法
        onStart();
        //类似于Activity,获取当前新Window的DecorView对象,所以有一种自定义Dialog布局的方式就是重写Dialog的onCreate方法,使用setContentView传入布局,就像前面文章分析Activity类似
        mDecor = mWindow.getDecorView();
        ......
        //获取新Window的WindowManager.LayoutParams参数,和上面分析的Activity一样type为TYPE_APPLICATION
        WindowManager.LayoutParams l = mWindow.getAttributes();
        ......
        try {
            //把一个View添加到Activity共用的windowManager里面去
            mWindowManager.addView(mDecor, l);
            ......
        } finally {
        }
    }

可以看见 Dialog 的新 Window 与 Activity 的 Window 的 type 同样都为 TYPE_APPLICATION,上面介绍 WindowManager.LayoutParams 时 TYPE_APPLICATION 的注释明确说过,普通应用程序窗口 TYPE_APPLICATION 的 token 必须设置为 Activity 的 token 来指定窗口属于谁。所以可以看见,既然 Dialog 和 Activity 共享同一个 WindowManager(也就是上面分析的 WindowManagerImpl),而 WindowManagerImpl 里面有个 Window 类型的 mParentWindow 变量,这个变量在 Activity 的 attach 中创建 WindowManagerImpl 时传入的为当前 Activity 的 Window,而当前 Activity 的 Window 里面的 mAppToken 值又为当前 Activity 的 token,所以 Activity 与 Dialog 共享了同一个 mAppToken 值,只是 Dialog 和 Activity 的 Window 对象不同。

然后这句话是重点,有木有跟 Activity 窗口添加的时候很像,没错

mWindowManager.addView(mDecor, l);

Dialog 机制大概就这些了,现在来分析一下,上面两个代码示例 
第一个问题:

Dialog dialog = new ProgressDialog(this);//为什么这样是正常的?

所以此时 dialog 的 mWindowManager 变量其实就是 activity 对象的 mWindowManager 变量。 
还记得吗?在 WindowManager.addView 实际上执行的是 WindowManagerImpl 的 addView

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/view/WindowManagerImpl.java

public final class WindowManagerImpl implements WindowManager {
    //继承自Object的单例类
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Window mParentWindow;
    
public WindowManagerImpl(Display display) {
    this(display, null);
}

private WindowManagerImpl(Display display, Window parentWindow) {
    mDisplay = display;
    mParentWindow = parentWindow;
}
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        //mParentWindow是上面分析的在Activity中获取WindowManagerImpl实例化时传入的当前Window
        //view是Activity中最顶层的mDecor
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    ......
}

 

所以此时 mParentWindow 其实就是 Activity 的 PhoneWindow 对象,虽然 dialog 有自己的 PhoneWindow,但是这两者完全是两码事,不要混淆 
所以在 WindowManagerGlobal.addView 方法中调用

  

public void addView(View view, ViewGroup.LayoutParams params,
  Display display, Window parentWindow) {
         //...

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
        //依据当前Activity的Window调节sub Window的LayoutParams
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there''s no parent, then hardware acceleration for this view is
            // set from the application''s hardware acceleration setting.
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
           //...
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
           //...
        }
    }

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/view/Window.java

adjustLayoutParamsForSubWindow 方法中

void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
           //...
          
        } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
          //...
        } else {
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) {
                wp.setTitle(mAppName);
            }
        }
        if (wp.packageName == null) {
            wp.packageName = mContext.getPackageName();
        }
        if (mHardwareAccelerated) {
            wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }

调整 wp 的时候,所以最后 wp.token 拿到的就是 Activity 的 mToken,就不为 null,所以在最后 WindowManagerService 的 addWindow 方法中就不会让 ViewRootImpl 中抛异常了,所以一切 OK

第二个问题:

Log.d(“LiaBin”, “dialog window token:” + dialog.getWindow().getAttributes().token); 打印的为什么是null,而不是activitytoken

现在就很好理解了,首先 dialog.getWindow (),那么获取的就是 dialog 的 PhoneWindow, 而 Dialog 的 window 的 mWindowAttributes 的 token 值初始化就为 null

虽然调用了 adjustLayoutParamsForSubWindow 方法,但是并没有调整 Dialog 的 window 的 mWindowAttributes 的 token 值,因为以下代码行就把两者关系断了,调整的是另外一个对象

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

第三个问题:

Dialog dialog = new ProgressDialog(getApplicationContext());为什么会抛异常BadTokenException: Unable to add window – token null is not for an application

因为 mContext 赋值为了 getApplicationContext (),那么

//获取WindowManager对象
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

那么此时的 mWindowManager 就是全局唯一的 mWindowManager 了,而不是 activity 的 mWindowManager。可以看上一篇的分析。调用的其实是 ContextImpl 的 getSystemService 方法

所以在 WindowManagerGlobal.addView 方法中 parentWindow 就为 null 了,所以就不会去调用 adjustLayoutParamsForSubWindow 方法了,所以最后 params 的 token 就为 null 了

在最后 WindowManagerService 的 addWindow 方法,把 param 的 token 取出来一看是 null,就 return WindowManagerGlobal.ADD_NOT_APP_TOKEN; 返回给 ViewRootImpl 的 setView 方法
再来看 ViewRootImpl 的 setView 方法,片段

case WindowManagerGlobal.ADD_NOT_APP_TOKEN:throw new WindowManager.BadTokenException(“Unable to add window – token ” + attrs.token + ” is not for an application”);

所以最后抛 BadTokenException 异常啦

第四个问题:

在服务中调用Dialog dialog = new ProgressDialog(this);为什么要会抛异常

因为 service 中并没有跟 activity 做同样的处理,调用的其实是 ContextImpl 的 getSystemService 方法,所以此时的 mWindowManager 就是全局唯一的 mWindowManager 了,

另外一种情况:

在Activity中使用Dialog的时候,为什么有时候会报错“Unable to add window – token is not valid; is your activity running?”?这种情况一般发生在什么时候?一般发生在Activity进入后台,Dialog没有主动Dismiss掉,然后从后台再次进入App的时候。

Dialog 窗口加载总结

从图中可以看出,Activity 和 Dialog 共用了一个 Token 对象,Dialog 必须依赖于 Activity 而显示(通过别的 context 搞完之后 token 都为 null,最终会在 ViewRootImpl 的 setView 方法中加载时因为 token 为 null 抛出异常),所以 Dialog 的 Context 传入参数一般是一个存在的 Activity,如果 Dialog 弹出来之前 Activity 已经被销毁了,则这个 Dialog 在弹出的时候就会抛出异常,因为 token 不可用了。在 Dialog 的构造函数中我们关联了新 Window 的 callback 事件监听处理,所以当 Dialog 显示时 Activity 无法消费当前的事件。

PopWindow 窗口添加机制

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/PopupWindow.java

public class PopupWindow {
   private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
    ......
    //我们只分析最常用的一种构造函数
    public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            //获取mContext,contentView实质是View,View的mContext都是构造函数传入的,View又层级传递,所以最终这个mContext实质是Activity!!!很重要
            mContext = contentView.getContext();
            //获取Activity的getSystemService的WindowManager
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //进行一些Window类的成员变量初始化赋值操作
        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }
    ......
}

 

其中注意,view 创建的时候都会把一个 context 参数传递进去,context 就是当前的 activity 了,所以其实 contentView.getContext (); 返回的是该 Activity,所以 mWindowManager 共享当前 Activity 的 mWindowManager 变量。同时因为 popupwindow 构造函数的参数是 view,并不是 context,所以并不用担心 getApplicationContext 造成的问题

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        ......
        //anchor是Activity中PopWindow准备依附的View,这个View的token实质也是Activity的Window中的token,也即Activity的token
        //第一步   初始化WindowManager.LayoutParams
        WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
        //第二步
        preparePopup(p);
        ......
        //第三步
        invokePopup(p);
    }
   
createPopupLayout
private WindowManager.LayoutParams createPopupLayout(IBinder token) {
        //实例化一个默认的WindowManager.LayoutParams,其中type=TYPE_APPLICATION
        WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        //设置Gravity
        p.gravity = Gravity.START | Gravity.TOP;
        //设置宽高
        p.width = mLastWidth = mWidth;
        p.height = mLastHeight = mHeight;
        //依据背景设置format
        if (mBackground != null) {
            p.format = mBackground.getOpacity();
        } else {
            p.format = PixelFormat.TRANSLUCENT;
        }
        //设置flags
        p.flags = computeFlags(p.flags);
        //修改type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type类型为子窗口
        p.type = mWindowLayoutType;
        //设置token为Activity的token
        p.token = token;
        ......
        return p;
    }
private void preparePopup(WindowManager.LayoutParams p) {
        ......
        //有无设置PopWindow的background区别
        if (mBackground != null) {
            ......
            //如果有背景则创建一个PopupViewContainer对象的ViewGroup
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            //把背景设置给PopupViewContainer的ViewGroup
            popupViewContainer.setBackground(mBackground);
            //把我们构造函数传入的View添加到这个ViewGroup
            popupViewContainer.addView(mContentView, listParams);
            //返回这个ViewGroup
            mPopupView = popupViewContainer;
        } else {
            //如果没有通过PopWindow的setBackgroundDrawable设置背景则直接赋值当前传入的View为PopWindow的View
            mPopupView = mContentView;
        }
        ......
    }
 

可以看见 preparePopup 方法的作用就是判断设置 View,如果有背景则会在传入的 contentView 外面包一层 PopupViewContainer(实质是一个重写了事件处理的 FrameLayout)之后作为 mPopupView,如果没有背景则直接用 contentView 作为 mPopupView。我们再来看下这里的 PopupViewContainer 类,如下源码:

private class PopupViewContainer extends FrameLayout {
        ......
        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            ......
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            ......
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            ......
            if(xxx) {
                dismiss();
            }
            ......
        }

        @Override
        public void sendAccessibilityEvent(int eventType) {
            ......
        }
    }

可以看见,这个 PopupViewContainer 是一个 PopWindow 的内部私有类,它继承了 FrameLayout,在其中重写了 Key 和 Touch 事件的分发处理逻辑。同时查阅 PopupView 可以发现,PopupView 类自身没有重写 Key 和 Touch 事件的处理,所以如果没有将传入的 View 对象放入封装的 ViewGroup 中,则点击 Back 键或者 PopWindow 以外的区域 PopWindow 是不会消失的(其实 PopWindow 中没有向 Activity 及 Dialog 一样 new 新的 Window,所以不会有新的 callback 设置,也就没法处理事件消费了)。

private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }

这里使用了 Activity 的 WindowManager 将我们的 PopWindow 进行了显示。

到此可以发现,PopWindow 的实质无非也是使用 WindowManager 的 addView、updateViewLayout、removeView 进行一些操作展示。与 Dialog 不同的地方是没有新 new Window 而已(也就没法设置 callback,无法消费事件,也就是前面说的 PopupWindow 弹出后可以继续与依赖的 Activity 进行交互的原因)。

到此 PopWindw 的窗口加载显示机制就分析完毕了,接下来进行总结与应用开发技巧提示。

 

可以看见 preparePopup 方法的作用就是判断设置 View,如果有背景则会在传入的 contentView 外面包一层 PopupViewContainer(实质是一个重写了事件处理的 FrameLayout)之后作为 mPopupView,如果没有背景则直接用 contentView 作为 mPopupView

PopupViewContainer 是一个 PopWindow 的内部私有类,它继承了 FrameLayout,在其中重写了 Key 和 Touch 事件的分发处理逻辑。同时查阅 PopupView 可以发现,PopupView 类自身没有重写 Key 和 Touch 事件的处理,所以如果没有将传入的 View 对象放入封装的 ViewGroup 中,则点击 Back 键或者 PopWindow 以外的区域 PopWindow 是不会消失的(其实 PopWindow 中没有向 Activity 及 Dialog 一样 new 新的 Window,所以不会有新的 callback 设置,也就没法处理事件消费了)。

1. 与 Dialog 不同的地方是没有新 new Window 而已(也就没法设置 callback,无法消费事件,也就是前面说的 PopupWindow 弹出后可以继续与依赖的 Activity 进行交互的原因)。
2. 如果设置了 PopupWindow 的 background,则点击 Back 键或者点击 PopupWindow 以外的区域时 PopupWindow 就会 dismiss;如果不设置 PopupWindow 的 background,则点击 Back 键或者点击 PopupWindow 以外的区域 PopupWindow 不会消失。
另一方面,如果需要全屏的 popupwindow,那么可以使用一下代码

view.showAtLocation(mActivity.getWindow().getDecorView(), Gravity.CENTER, 0, 0);

getWindow ().getDecorView () 就是获取顶层的 DecorView

Toast 窗口添加机制

我们常用的 Toast 窗口其实和前面分析的 Activity、Dialog、PopWindow 都是不同的,因为它和输入法、墙纸类似,都是系统窗口。

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        //new一个Toast对象
        Toast result = new Toast(context);
        //获取前面有篇文章分析的LayoutInflater
        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        //加载解析Toast的布局,实质transient_notification.xml是一个LinearLayout中套了一个@android:id/message的TextView而已
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        //取出布局中的TextView
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        //把我们的文字设置到TextView上
        tv.setText(text);
        //设置一些属性
        result.mNextView = v;
        result.mDuration = duration;
        //返回新建的Toast
        return result;
    }
    public void show() {
        ......
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            //把TN对象和一些参数传递到远程NotificationManagerService中去
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

这里使用了 IBinder 机制,其实是通过远程 NotificationManagerService 服务来管理 toast 的

private static class TN extends ITransientNotification.Stub {
        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        ......
        //仅仅是实例化了一个Handler,非常重要!!!!!!!!
        final Handler mHandler = new Handler(); 
        ......
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don''t do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };
        ......
        //实现了AIDL的show与hide方法
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }
        ......
    }

此时说明 toast 的 type 是 TYPE_TOAST,这里直接 new 了一个 handler,所以如果在子线程中直接显示一个 taost,就会报异常,除非在子线程中调用 Looper 的 prepare 和 looper 方法,才可以在线程中显示 toast。接下来重点分析 handleShow 方法

public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                //如果有必要就通过WindowManager的remove删掉旧的
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                //通过得到的context(一般是ContextImpl的context)获取WindowManager对象(上一篇文章分析的单例的WindowManager)
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ......
                //在把Toast的View添加之前发现Toast的View已经被添加过(有partent)则删掉
                if (mView.getParent() != null) {
                    ......
                    mWM.removeView(mView);
                }
                ......
                //把Toast的View添加到窗口,其中mParams.type在构造函数中赋值为TYPE_TOAST!!!!!!特别重要
                mWM.addView(mView, mParams);
                ......
            }
        }

mWM 此时是全局单例的 WindowManager,调用的是 ContextImpl 的 getSystemService 方法获取

最后总结一下:

通过分析 TN 类的 handler 可以发现,如果想在非 UI 线程使用 Toast 需要自行声明 Looper,否则运行会抛出 Looper 相关的异常;UI 线程不需要,因为系统已经帮忙声明。

1. 在使用 Toast 时 context 参数尽量使用 getApplicationContext (),可以有效的防止静态引用导致的内存泄漏。 因为首先 toast 构造函数中拿到了 toast,所以如果在当前 activity 中弹出一个 toast,然后 finish 掉该 toast,toast 并不依赖 activity,是系统级的窗口,当然也不会随着 activity 的 finish 就消失,只是随着设置时间的到来而消失,所以如果此时设置 toast 显示的时间足够长,那么因为 toast 持有该 activity 的引用,那么该 activty 就一直不能被回收,一直到 toast 消失,造成内存泄漏,所以最好使用 getApplicationContext ()

2. 有时候我们会发现 Toast 弹出过多就会延迟显示,因为上面源码分析可以看见 Toast.makeText 是一个静态工厂方法,每次调用这个方法都会产生一个新的 Toast 对象,当我们在这个新 new 的对象上调用 show 方法就会使这个对象加入到 NotificationManagerService 管理的 mToastQueue 消息显示队列里排队等候显示;所以如果我们不每次都产生一个新的 Toast 对象(使用单例来处理)就不需要排队,也就能及时更新了。

3.Toast 的显示交由远程的 NotificationManagerService 管理是因为 Toast 是每个应用程序都会弹出的,而且位置和 UI 风格都差不多,所以如果我们不统一管理就会出现覆盖叠加现象,同时导致不好控制,所以 Google 把 Toast 设计成为了系统级的窗口类型,由 NotificationManagerService 统一队列管理。

android Dialog和Popupwindow,不显示

android Dialog和Popupwindow,不显示

测试的是华为荣耀平板,在BaseActivity中有个方法,在里面构造dialog或popupwindow,在子类activity中手动点击按钮调用该方法,每次都正常显示弹窗,但是在回调接口中调用基本就不显示,偶尔运气好能显示出来,打印看构造弹窗的context都一样。求大神解答!!!

Android PopupWindow

Android的对话框有两种:PopupWindow和AlertDialog。它们的不同点在于:

  • AlertDialog的位置固定,而PopupWindow的位置可以随意
  • AlertDialog是非阻塞线程的,而PopupWindow是阻塞线程的

PopupWindow的位置按照有无偏移分,可以分为偏移和无偏移两种;按照参照物的不同,可以分为相对于某个控件(Anchor锚)和相对于父控件。具体如下

  • showAsDropDown(View anchor):相对某个控件的位置(正左下方),无偏移
  • showAsDropDown(View anchor, int xoff, int yoff):相对某个控件的位置,有偏移
  • showAtLocation(View parent, int gravity, int x, int y):相对于父控件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以设置偏移或无偏移

下面通过一个Demo讲解(解释看注释):

main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

    <Button
        android:id="@+id/button01"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以自己为Anchor,不偏移" />

    <Button
        android:id="@+id/button02"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以自己为Anchor,有偏移" />

    <Button
        android:id="@+id/button03"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以屏幕中心为参照,不偏移(正中间)" />

    <Button
        android:id="@+id/button04"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以屏幕下方为参照,下方中间" />

</LinearLayout>

popup_window.xml

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="选择状态:"
        android:textColor="@android :color/white"
        android:textSize="20px" />

    <RadioGroup
        android:id="@+id/radioGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <RadioButton android:text="在线" />

        <RadioButton android:text="离线" />

        <RadioButton android:text="隐身" />
    </RadioGroup>

</LinearLayout>

PopupWindowDemoActivity.java

package com.tianjf;

import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.PopupWindow;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener;

public class PopupWindowDemoActivity extends Activity implements OnClickListener,
        OnCheckedChangeListener {

    private Button mbutton01;
    private Button mbutton02;
    private Button mbutton03;
    private Button mbutton04;
    private PopupWindow mPopupWindow;
    // 屏幕的width
    private int mScreenWidth;
    // 屏幕的height
    private int mScreenHeight;
    // PopupWindow的width
    private int mPopupWindowWidth;
    // PopupWindow的height
    private int mPopupWindowHeight;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mbutton01 = (Button) findViewById(R.id.button01);
        mbutton02 = (Button) findViewById(R.id.button02);
        mbutton03 = (Button) findViewById(R.id.button03);
        mbutton04 = (Button) findViewById(R.id.button04);

        mbutton01.setOnClickListener(this);
        mbutton02.setOnClickListener(this);
        mbutton03.setOnClickListener(this);
        mbutton04.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        // 相对某个控件的位置(正左下方),无偏移
        case R.id.button01:
            getPopupWindowInstance();
            mPopupWindow.showAsDropDown(v);
            break;

        // 相对某个控件的位置(正左下方),有偏移
        case R.id.button02:
            getPopupWindowInstance();
            mPopupWindow.showAsDropDown(v, 50, 50);// X、Y方向各偏移50
            break;

        // 相对于父控件的位置,无偏移
        case R.id.button03:
            getPopupWindowInstance();
            mPopupWindow.showAtLocation(v, Gravity.CENTER, 0, 0);
            break;

        // 相对于父控件的位置,有偏移
        case R.id.button04:
            getPopupWindowInstance();
            mPopupWindow.showAtLocation(v, Gravity.BOTTOM, 0, 50);
            break;

        default:
            break;
        }
    }

    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        mPopupWindow.dismiss();
    }

    /*
     * 获取PopupWindow实例
     */
    private void getPopupWindowInstance() {
        if (null != mPopupWindow) {
            mPopupWindow.dismiss();
            return;
        } else {
            initPopuptWindow();
        }
    }

    /*
     * 创建PopupWindow
     */
    private void initPopuptWindow() {
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        View popupWindow = layoutInflater.inflate(R.layout.popup_window, null);
        RadioGroup radioGroup = (RadioGroup) popupWindow.findViewById(R.id.radioGroup);
        radioGroup.setOnCheckedChangeListener(this);

        // 创建一个PopupWindow
        // 参数1:contentView 指定PopupWindow的内容
        // 参数2:width 指定PopupWindow的width
        // 参数3:height 指定PopupWindow的height
        mPopupWindow = new PopupWindow(popupWindow, 100, 130);

        // 获取屏幕和PopupWindow的width和height
        mScreenWidth = getWindowManager().getDefaultDisplay().getWidth();
        mScreenWidth = getWindowManager().getDefaultDisplay().getHeight();
        mPopupWindowWidth = mPopupWindow.getWidth();
        mPopupWindowHeight = mPopupWindow.getHeight();
    }
}


Android PopupWindow 实现自定义菜单弹窗效果

先看最终效果图:
请输入图片描述
原理一个将PopupWindow绑在一个TextView上,处理TextView点击事件来弹框,右边EditText实现输入框,把EditText拿上来是因为在实习过程中碰到不少细节问题。代码:
main.xml //主页面布局

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/bgtitlebar"
    android:orientation="horizontal"
    android:padding="10dp" >

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical" >

        <Button
            android:id="@+id/btn_header_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/btn_back"
            android:onClick="back" />
    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:text="@string/txt_search"
        android:textSize="18sp" />
</RelativeLayout>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_divide_line_search" >

    <LinearLayout
        android:id="@+id/ll_search_txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="6dp"
        android:background="@drawable/bg_tv_search_category" >//因为一个控件只能有一张背景图片,所以把“资讯”的另一张背景图片放在布局上

        <TextView
            android:id="@+id/search_category"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dp"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="3dp"
            android:clickable="true"
            android:drawableRight="@drawable/bg_tv_search_pop"//这个属性可将图片放在控件的左侧
            android:text="@string/message"
            android:textColor="@color/blue"
            android:textSize="19sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="18dp"
        android:layout_toRightOf="@id/ll_search_txt"
        android:background="@drawable/bg_edt_search"
        android:layout_marginTop="9dp"
        >
        <EditText
            android:id="@+id/input_search_cagegory"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="5dp"
            android:background="@drawable/bg_edt_search"
            android:ems="10"
            android:drawableLeft="@drawable/bg_edt_search_icon  
            android:hint="@string/input_search_txt"
            android:singleLine="true" />
    </LinearLayout>
</RelativeLayout>

popup_item.xml // PopupWindow 弹框内容,这里只是简单的TextView,涉及ListView的话要复杂些

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
    android:id="@+id/search_picture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/picture"
    android:clickable="true"
    android:textSize="19sp"
    android:background="@drawable/bg_search_picture"
    />
 <TextView
    android:id="@+id/search_video"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/video"
    android:clickable="true"
    android:textSize="19sp"
    android:background="@drawable/bg_search_video"
    />
  <TextView
    android:id="@+id/search_message"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/message"
    android:clickable="true"
    android:textSize="19sp"
    android:background="@drawable/bg_search_message"
    />


main.java

public class SearchActivity extends Activity {
private PopupWindow window = null;
private LayoutInflater inflater;
private TextView txt_choosecategory;
private TextView message;
private TextView picture;
private TextView video;
private EditText edt_inputcagegory;
private Button btn_header_back;
private View view;
private ListView listView;
private String return_result;
private String category_id;
private String keyword;
    private SearchTask searchTask;
private String category;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_search);
    initHeader();
    initBodyer();
}

private boolean isInput() {
    if (isNullorSpace(edt_inputcagegory.getText().toString())) {
        return false;
    }
    return true;
}

private void initBodyer() {
//      final InputMethodManager imm = (InputMethodManager)      //getSystemService(Context.INPUT_METHOD_SERVICE);
    txt_choosecategory = (TextView) findViewById(R.id.search_category);
    edt_inputcagegory = (EditText) findViewById(R.id.input_search_cagegory);
    edt_inputcagegory.setImeOptions(EditorInfo.IME_ACTION_SEARCH); //这行代码的作用是把键盘右下角的“回车”键变成“搜索”键
    txt_choosecategory.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (window != null) {  //一定要先判断是否为空!
                if (window.isShowing()) {//isShowing()方法判断当前是否已经弹窗,返回true则关闭PopupWindow
                    window.dismiss();
                    window = null;
                }
            } else {
                showWindow();实际这里是实现点击一次弹出PopupWindow,再点击一次关闭掉。
            }

        }
    });
    edt_inputcagegory
            .setOnEditorActionListener(new OnEditorActionListener() {

                @Override
                public boolean onEditorAction(TextView v, int actionId,
                        KeyEvent event) {//对键盘的搜索键监听事件
                    if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                        if (isInput()) {
                            search();
                        } else {
//edt_inputcagegory.requestFocus();                                                          //imm.showSoftInputFromInputMethod(edt_inputcagegory.getWindowToken(), 0);
Toast.makeText(Main.this,"搜索内容不能为空",Toast.LENGH_SHORT);                         

                        }
                    }

                    return false;
                }
            });
}

private void initHeader() {
    btn_header_back = (Button) findViewById(R.id.btn_header_back);
    btn_header_back.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {

            finish();
        }
    });
}
    private boolean isNullorSpace(String str) {
    if (!"".equals(str.trim()) & (str != null))
        return false;
    return true;
}

private void search() {
    //Handle search
}

private void showWindow() {
    inflater = (LayoutInflater) this
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    view = inflater.inflate(R.layout.item_search_category, null, false);//加载布局
    message = (TextView) view.findViewById(R.id.search_message);
    video = (TextView) view.findViewById(R.id.search_video);
    picture = (TextView) view.findViewById(R.id.search_picture);
    message.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            txt_choosecategory.setText(R.string.message);  //错误代码
            window.dismiss();
            window = null;
        }
    });
    video.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            txt_choosecategory.setText(R.string.video);//错误代码
            window.dismiss();
            window = null;
        }
    });
    picture.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            txt_choosecategory.setText(R.string.picture);//错误代码
            window.dismiss();
            window = null;
        }
    });
    if (window == null) {
        window = new PopupWindow(view, LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);//创建PopupWindow,第一个参数是将要展示的内容布局,第二、三个是布局大小(LayoutParamas.WRAP_CONTENT,LayoutParams.WRAP_CONTENT要求API >=11)
    }
    // int[] location = new int[2];
    // window.showAsDropDown(txt_choosecategory);
    // window.showAtLocation(txt_choosecategory, Gravity.NO_GRAVITY,
    // location[0], location[1]);
    window.showAtLocation(txt_choosecategory, Gravity.NO_GRAVITY, 15, 160);//设置PopupWindow位置,第一个参数是指定PopupWindow依附的View,第二个是坐标偏移量相对的起始点,Gravity.NO_GRAVITY= Gravity.LEFT | Gravity.TOP,最后两个是偏移量,这个写死了应该对屏幕适配不太好,而且很麻烦囧,暂时还没找到方便的方法..欢迎大侠指路.
}

需要注意的是:1.本人一开始用PopupWindow就曾经报错: "Unable to add window -- token null is not valid",原因在stackoverflow上找到:To avoid BadTokenException, you need to defer showing the popup until after all the lifecycle methods are called (-> activity window is displayed),简而言之就是要在布局全部加载完之后再去加载PopupWindow,显示PopupWindow过早就会直接crash.
2.在Activity取出string资源文件值,textView.setText(R.string.XXX)是错误的写法应g改为-->textView.setText(getString(R.string.xxx))或者getResources().getString(R.string.xxx),原因:http://stackoverflow.com/questions/11536326/android-settext-r-string-values
描述
(第一句一语道破天机,R.string.xxx仅仅是资源的ID,所以R.String.xxx得到的是资源的编号)

关于Android用PopupWindow实现自定义Dailogandroid popupwindow使用的介绍已经告一段落,感谢您的耐心阅读,如果想了解更多关于android dialog,popupwindow,toast 窗口的添加机制、android Dialog和Popupwindow,不显示、Android PopupWindow、Android PopupWindow 实现自定义菜单弹窗效果的相关信息,请在本站寻找。

本文标签: