一、popupwindow源码解析
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
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;
}
}
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消失。<?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>
<?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>
<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类似。