目标:
实现一个popup view 自动显示在点击的view的附近,且箭头一直指向该view的水平中心位置
效果图:
思路
自定义view包含PopupWindow的实例 对该实例进行操控
步骤1 创建popupview的布局popview_container.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:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/arrow_icon_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="-13dp"
android:src="@drawable/popup_arrow_up" />
<LinearLayout
android:id="@+id/content_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:background="#ccc"
android:orientation="vertical">
</LinearLayout>
<ImageView
android:id="@+id/arrow_icon_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-13dp"
android:src="@drawable/popup_arrow_down"
android:visibility="gone" />
</LinearLayout>
使用到的两个箭头
popup_arrow_down.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="20dp"
android:viewportWidth="40"
android:viewportHeight="20">
<path
android:pathData="M0,0h40L20,20 0,0z"
android:fillColor="#ccc"/>
</vector>
popup_arrow_up.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="20dp"
android:viewportWidth="40"
android:viewportHeight="20">
<path
android:pathData="M0,20h40L20,0 0,20z"
android:fillColor="#ccc"/>
</vector>
步骤2 popupview item的布局popview_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:minWidth="480dp"
android:layout_height="wrap_content"
android:background="@drawable/popupview_item_bg"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/popup_view_item_icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_marginStart="35dp"
android:src="@drawable/ic_delete" />
<TextView
android:id="@+id/popup_view_item_des"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:layout_marginStart="35dp"
android:layout_marginEnd="20dp"
android:ellipsize="marquee"
android:singleLine="true"/>
</LinearLayout>
<ImageView
android:id="@+id/popup_view_item_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#0f0" />
</LinearLayout>
其中会使用到几个图标和背景
ic_delete.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:fillColor="#0ff"
android:pathData="M40.206,10.682L9.75,10.682c-0.962,0 -1.75,0.788 -1.75,1.75 0,0.963 0.788,1.75 1.75,1.75h1.444L11.194,38.95c0,3.15 2.538,5.688 5.689,5.688h16.234c3.15,0 5.689,-2.538 5.689,-5.688L38.806,14.183h1.444c0.962,0 1.75,-0.788 1.75,-1.75 -0.044,-0.963 -0.831,-1.75 -1.794,-1.75zM19.815,36.15c0,0.7 -0.57,1.27 -1.313,1.27 -0.744,0 -1.313,-0.57 -1.313,-1.313L17.189,21.228c0,-0.7 0.569,-1.27 1.313,-1.27s1.313,0.57 1.313,1.27v14.921zM26.291,36.15c0,0.7 -0.569,1.27 -1.313,1.27s-1.313,-0.57 -1.313,-1.313L23.665,21.228c0,-0.7 0.57,-1.27 1.313,-1.27 0.744,0 1.313,0.57 1.313,1.27v14.921zM32.767,36.15c0,0.7 -0.569,1.27 -1.313,1.27s-1.312,-0.57 -1.312,-1.313L30.142,21.228c0,-0.7 0.568,-1.27 1.312,-1.27 0.744,0 1.313,0.57 1.313,1.27v14.921zM31.542,7.75c0,-0.962 -0.788,-1.75 -1.75,-1.75h-9.627c-0.963,0 -1.75,0.788 -1.75,1.75L18.415,9.5h13.127L31.542,7.75z" />
</vector>
popupview_item_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@color/colorPrimary" />
<!-- Non pressed, Non selected, Non focused and Non enabled -->
<item android:state_pressed="false" android:drawable="@color/transparent" />
</selector>
步骤3 创建PopupView java文件
步骤3.1在PopupView内部新建内部类PopupViewItem 用于创建PopupView的Item
package com.example.testfragment.ui.main;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import com.example.testfragment.R;
import com.example.testfragment.util.ViewUtil;
import java.util.List;
public class PopupView {
private final Context mContext;
private PopupWindow mPopup;
private View mArrow;
//构造函数
public PopupView(Context context) {
mContext = context;
}
public void dismissView() {
if (mPopup == null) {
return;
}
if (mPopup.isShowing()) {
mPopup.dismiss();
}
}
private void clear() {
if (mPopup != null) {
mPopup.dismiss();
mPopup = null;
}
}
private void initView(View view) {
clear();
mPopup = new PopupWindow(view, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
mPopup.setOutsideTouchable(true);
mPopup.setTouchable(true);
mPopup.update();
mPopup.setFocusable(true);
}
//默认以点击view的坐下侧为起点 显示popup
public void showView(View anchor, List<PopupViewItem> itemList) {
dismissView();
if (mContext == null) {
return;
}
if (mContext.getResources() == null) {
return;
}
if (anchor == null) {
return;
}
//start to layout the pop-up view and set its event
View currentPopupLayoutView = View.inflate(mContext, R.layout.popview_container, null);
LinearLayout container = currentPopupLayoutView.findViewById(R.id.content_container);
if (container != null) {
int i = 0;
for (PopupViewItem item : itemList) {
//the last item should not show the divider
i++;
addClickActionItem(container, item, i != itemList.size());
}
}
int popupViewHeightWithAnchor = getPopupViewMeasuredHeightWithAnchor(anchor, currentPopupLayoutView);
int popupViewHeight = getPopupViewMeasuredHeight(currentPopupLayoutView);
int anchorViewHeight = getAnchorViewMeasuredHeight(anchor);
if (anchorViewHeight <= 0 || popupViewHeight <= 0 || popupViewHeightWithAnchor <= 0) {
return;
}
//如果Y方向 空间不够显示 需要在Y方向上反转显示方向
boolean revert = needReverse(anchor, popupViewHeightWithAnchor);
if (revert) {
revertPopupView(currentPopupLayoutView);
}else{
mArrow = currentPopupLayoutView.findViewById(R.id.arrow_icon_up);
}
int anchorViewY = getAnchorViewY(anchor);
int y = revert ? (anchorViewY - popupViewHeight) : (anchorViewY + anchorViewHeight);
//默认 popup view的正中与Anchor的水平正中对齐
//1.计算Anchor x的中心
int anchorXCenter = getAnchorViewX(anchor) + anchor.getWidth() / 2;
//2.获取popup view的宽度
int popupViewWidth = getPopupViewMeasuredWidth(currentPopupLayoutView);
int popupViewStartX = anchorXCenter - popupViewWidth / 2;
//如果水平空间不够显示popup view(被强行挤到屏幕内) 需要挪动箭头图标 计算挪动图标的水平值x
if (popupViewStartX < 0) {
//向左移动箭头(参数<0)
mArrow.setTranslationX(popupViewStartX);
} else if ((popupViewStartX + popupViewWidth) > ViewUtil.getAppScreenWidth(mContext)) {
//向右移动箭头(参数>0)
mArrow.setTranslationX(popupViewStartX + popupViewWidth - ViewUtil.getAppScreenWidth(mContext));
}
initView(currentPopupLayoutView);
showView(anchor, popupViewStartX, y);
}
private void showView(View anchor, int x, int y) {
if (mPopup != null) {
mPopup.showAtLocation(anchor, Gravity.TOP | Gravity.START, x, y);
}
}
//在某个view显示popup view,其中某个view就是anchor
//计算anchor+popup view的高度
private int getPopupViewMeasuredHeightWithAnchor(View anchor, View currentPopupLayoutView) {
if (anchor == null || currentPopupLayoutView == null) {
return -1;
}
//need to call measure() to generate the width and height
currentPopupLayoutView.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
return currentPopupLayoutView.getMeasuredHeight() + anchor.getMeasuredHeight();
}
/**
* @param container popup view
* @param item popup的item项
* @param isShowDivider 是否显示divider
*/
private void addClickActionItem(LinearLayout container, final PopupViewItem item, boolean isShowDivider) {
if (item == null || container == null) {
return;
}
final View inflateView = ViewUtil.addView(R.layout.popview_item, container);
if (inflateView != null) {
if (item.getMinWidth() > -1) {
inflateView.setMinimumWidth(item.getMinWidth());
}
ImageView icon = inflateView.findViewById(R.id.popup_view_item_icon);
icon.setImageDrawable(mContext.getResources().getDrawable(item.getIconResID(), mContext.getTheme()));
TextView tvDes = inflateView.findViewById(R.id.popup_view_item_des);
tvDes.setText(item.getDescriptionStr());
inflateView.setOnClickListener(item.getOnClickListener());
if (!isShowDivider) {
View dividerView = inflateView.findViewById(R.id.popup_view_item_divider);
dividerView.setVisibility(View.GONE);
}
}
}
//如果剩余空间不够显示popup,向上反转
private boolean needReverse(View anchor, int popupViewHeightWithAnchor) {
return mContext != null && popupViewHeightWithAnchor + getAnchorViewY(anchor) > ViewUtil.getAppScreenHeight(mContext);
}
private void revertPopupView(View popupView) {
if (popupView != null) {
View arrowUp = popupView.findViewById(R.id.arrow_icon_up);
View arrowDown = popupView.findViewById(R.id.arrow_icon_down);
if (arrowUp != null && arrowDown != null) {
arrowUp.setVisibility(View.GONE);
arrowDown.setVisibility(View.VISIBLE);
mArrow = arrowDown;
}
}
}
//计算anchorView的X绝对坐标
private int getAnchorViewX(View anchor) {
int[] locationOnScreen = new int[2];
anchor.getLocationOnScreen(locationOnScreen);
return locationOnScreen[0];
}
//计算anchorView的Y绝对坐标
private int getAnchorViewY(View anchor) {
int[] locationOnScreen = new int[2];
anchor.getLocationOnScreen(locationOnScreen);
return locationOnScreen[1];
}
//计算anchorView的高度
private int getAnchorViewMeasuredHeight(View anchor) {
if (anchor == null) {
return -1;
}
return anchor.getMeasuredHeight();
}
//计算PopupView的高度
private int getPopupViewMeasuredHeight(View currentPopupLayoutView) {
if (currentPopupLayoutView == null) {
return -1;
}
//need to call measure() to generate the getMeasuredWidth
int wrapContentSpec = View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
currentPopupLayoutView.measure(wrapContentSpec, wrapContentSpec);
return currentPopupLayoutView.getMeasuredHeight();
}
//计算PopupView的宽度
private int getPopupViewMeasuredWidth(View currentPopupLayoutView) {
if (currentPopupLayoutView == null) {
return -1;
}
currentPopupLayoutView.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
return currentPopupLayoutView.getMeasuredWidth();
}
//内部类 用于创建item
public static class PopupViewItem {
private int iconResID;
private String descriptionStr;
private View.OnClickListener onClickListener;
/**
* if we have not set up this value, it will keep the default with set in R.layout.popview_item
*/
private int minWidth = -1;
public PopupViewItem(int iconResID, String descriptionStr, View.OnClickListener onClickListener) {
this.iconResID = iconResID;
this.descriptionStr = descriptionStr;
this.onClickListener = onClickListener;
}
public PopupViewItem(int iconResID, String descriptionStr, View.OnClickListener onClickListener, int minWidth) {
this.iconResID = iconResID;
this.descriptionStr = descriptionStr;
this.onClickListener = onClickListener;
this.minWidth = minWidth;
}
int getIconResID() {
return iconResID;
}
View.OnClickListener getOnClickListener() {
return onClickListener;
}
String getDescriptionStr() {
return descriptionStr;
}
int getMinWidth() {
return minWidth;
}
}
}
步骤4 编写activity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<ImageView
android:layout_width="200dp"
android:layout_height="100dp"
android:layout_alignParentEnd="false"
android:background="#0f0"
android:onClick="showPopupView1"
android:src="@drawable/ic_launcher" />
<ImageView
android:layout_width="150dp"
android:layout_height="100dp"
android:layout_alignParentEnd="true"
android:background="#0f0"
android:onClick="showPopupView2"
android:src="@drawable/ic_launcher" />
<ImageView
android:layout_width="170dp"
android:layout_height="200dp"
android:layout_alignParentEnd="false"
android:layout_alignParentBottom="true"
android:background="#0f0"
android:onClick="showPopupView3"
android:scaleType="centerInside"
android:src="@drawable/ic_launcher" />
<ImageView
android:layout_width="250dp"
android:layout_height="150dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:background="#0f0"
android:onClick="showPopupView4"
android:src="@drawable/ic_launcher" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#0f0"
android:onClick="showPopupView5"
android:src="@drawable/ic_launcher" />
</RelativeLayout>
步骤5 编写测试代码
package com.example.testfragment;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.example.testfragment.ui.main.PopupView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
Context context;
private PopupView popup1;
private PopupView popup2;
private PopupView popup3;
private PopupView popup4;
private PopupView popup5;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
context = this;
}
public void showPopupView1(View viewClicked) {
popup1 = new PopupView(this);
List<PopupView.PopupViewItem> itemList = new ArrayList<>();
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_launcher, "popup1_1", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item1 click", Toast.LENGTH_SHORT).show();
}
});
if (popup1 != null) {
popup1.dismissView();
}
}
}));
popup1.showView(viewClicked, itemList);
}
public void showPopupView2(View view) {
popup2 = new PopupView(context);
List<PopupView.PopupViewItem> itemList = new ArrayList<>();
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup2_1", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item1 click", Toast.LENGTH_SHORT).show();
}
});
if (popup2 != null) {
popup2.dismissView();
}
}
}));
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup2_2", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item2 click", Toast.LENGTH_SHORT).show();
}
});
if (popup2 != null) {
popup2.dismissView();
}
}
}));
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup2_3", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item3 click", Toast.LENGTH_SHORT).show();
}
});
if (popup2 != null) {
popup2.dismissView();
}
}
}));
popup2.showView(view, itemList);
}
public void showPopupView3(View view) {
popup3 = new PopupView(context);
List<PopupView.PopupViewItem> itemList = new ArrayList<>();
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup3_1", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item1 click", Toast.LENGTH_SHORT).show();
}
});
if (popup3 != null) {
popup3.dismissView();
}
}
}));
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup3_2", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item2 click", Toast.LENGTH_SHORT).show();
}
});
if (popup3 != null) {
popup3.dismissView();
}
}
}, 500));
popup3.showView(view, itemList);
}
public void showPopupView4(View view) {
popup4 = new PopupView(context);
List<PopupView.PopupViewItem> itemList = new ArrayList<>();
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup4_1", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item1 click", Toast.LENGTH_SHORT).show();
}
});
if (popup4 != null) {
popup4.dismissView();
}
}
}));
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_launcher, "popup4_2", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item2 click", Toast.LENGTH_SHORT).show();
}
});
if (popup4 != null) {
popup4.dismissView();
}
}
}));
popup4.showView(view, itemList);
}
public void showPopupView5(View view) {
popup5 = new PopupView(context);
List<PopupView.PopupViewItem> itemList = new ArrayList<>();
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_launcher, "popup5_1", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item1 click", Toast.LENGTH_SHORT).show();
}
});
if (popup5 != null) {
popup5.dismissView();
}
}
}));
itemList.add(new PopupView.PopupViewItem(R.drawable.ic_delete, "popup5_2", new View.OnClickListener() {
@Override
public void onClick(View v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "item2 click", Toast.LENGTH_SHORT).show();
}
});
if (popup5 != null) {
popup5.dismissView();
}
}
}));
popup5.showView(view, itemList);
}
}
/**
* Get app screen width in pixels
* @return app screen width in pixels
*/
public static int getAppScreenWidth(@NonNull Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Point point = new Point();
if (windowManager != null){
windowManager.getDefaultDisplay().getSize(point);
}
return point.x;
}
public static <T extends View> T addView(int resLayout, ViewGroup parentGroup) {
View inflateView = LayoutInflater.from(parentGroup.getContext())
.inflate(resLayout, parentGroup, false);
parentGroup.addView(inflateView);
return (T) inflateView;
}