前面已经实现过仿QQ的List抽屉效果以及仿QQ未读消息拖拽效果,具体请见:
Android自定义控件:类QQ抽屉效果
Android自定义控件:类QQ未读消息拖拽效果
趁热打铁,这次我们实现QQ空间的主页全效果,先贴上我们最终的完成效果图:
可以看到,我们实现了如下效果:
- 下拉拖拽视差效果
- 透明状态栏+TitleBar
- 状态栏+TitleBar颜色动态渐变
- 下拉加载更多
- 点击按钮∨弹出PopupWindow list选项+模糊背景效果
- 点击按钮+顶部弹出PopupWindow界面+模糊背景效果
下拉拖拽视差效果
第一步先实现拖拽视差效果,也就是下拉的时候,有一种阻滞感,然后手抬起的时候,会稍微回弹一下。
在实现效果之前,我们先看一下实现原理,我们看一下下面这张图:
实际上呢,一整个视差效果界面,其实就是一个ListView。我们给listView设置了一个headView,然后设置headView 布局的scaleType为centerCrop,取src图片的中部也就是图中绿色部分,这部分是初始显示区域。headView的src图片上下部分实际上是处于界面之外没有显示出来,也就是图中的棕色部分。
下面贴上头布局代码:
list_item_head.xml
然后在Activity中为listView添加头布局:
private void init() {
for (int i = 0; i < 30; i++) {
list.add("user - " + i);
}
lvParallax = (ListView) findViewById(R.id.lv_parallax);
lvParallax.setOverScrollMode(ListView.OVER_SCROLL_NEVER);//滑到底部顶部不显示蓝色阴影
View headerView = View.inflate(this, R.layout.list_item_head, null);//添加header
ImageView ivHead = (ImageView) headerView.findViewById(R.id.iv_head);
lvParallax.initParallaxImageParams(ivHead);
lvParallax.addHeaderView(headerView);
lvParallax.setAdapter(new ParallaxAdapter(this, list));
}
item布局代码就不贴了,我们看看现在运行的效果:
注:为了方便截图,后面的图都是运行在模拟器(480x800)上的效果截图,所以显示效果肯定跟最开始的真机( 720x1280)效果有一定的区别,不过此处只是做演示,这点小事就先忽略啦~ =。=**
既然布局已经完成了,那么我们接下来实现视差拖拽效果。
既然要拖拽,我们肯定要自定义一个ListView并且重写其onTouchEvent以及overScrollBy方法。
首先我们要思考的是,我们如何在自定义控件中拿到我们headView的高度以及图片的高度呢?由于我们的headView参数是在Activity的onCreate中初始化的,但是在onCreate中无法通过getHeight()和getWidth()拿到headView的高度和宽度,因为View组件布局要在onResume回调后完成。那么我们如何在onCreate中拿到headView的高度参数呢?这里我们通过getViewTreeObserver().addOnGlobalLayoutListener()来获得宽度或者高度。这是获得一个view的宽度和高度的方法之一。
OnGlobalLayoutListener 是ViewTreeObserver的内部类,当一个视图树的布局发生改变时,可以被ViewTreeObserver监听到,这是一个注册监听视图树的观察者(observer),在视图树的全局事件改变时得到通知。ViewTreeObserver不能直接实例化,而是通过getViewTreeObserver()获得。
不多说,上代码:
/**
* 初始化ParallaxImage的初始参数
*
* @param imageView
*/
public void initParallaxImageParams(final ImageView imageView) {
this.ivHead = imageView;
//设定ImageView最大高度
imageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
imageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
orignalHeight = imageView.getHeight();
//Log.e("tag", "orignalHeight = " + orignalHeight);
//获取图片的高度
int drawbleHeight = imageView.getDrawable().getIntrinsicHeight();
maxHeight = orignalHeight > drawbleHeight ? orignalHeight * 2 : drawbleHeight;
//Log.e("tag", "maxHeight = " + maxHeight);
}
});
}
在onGlobalLayout中增加一层判断,当headView初始高度大于图片高度时,我们取的上下滑动最大高度是headView*2。因为从根本上来讲,我们肯定是要保证headView上下部分肯定是超出界面之外的,所以这里的maxHeight肯定是要大于headView的高度的。
然后重写overScrollBy方法,overScrollBy会在listview滑动到头的时候执行,可以获取到继续滑动的距离和方向。当滑动到头的时候,我们通过继续滚动的距离,动态设置headView的高度,这样达到一个拖动显示的效果。
/**
* 在listview滑动到头的时候执行,可以获取到继续滑动的距离和方向
* deltaX:继续滑动x方向的距离
* deltaY:继续滑动y方向的距离 负:表示顶部到头 正:表示底部到头
* maxOverScrollX:x方向最大可以滚动的距离
* maxOverScrollY:y方向最大可以滚动的距离
* isTouchEvent: true: 是手指拖动滑动 false:表示fling靠惯性滑动;
*/
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int
scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean
isTouchEvent) {
//Log.e("tag", "deltaY: " + deltaY + " isTouchEvent:" + isTouchEvent);
if (deltaY < 0 && isTouchEvent) {//顶部到头,并且是手动拖到顶部
if (ivHead != null) {
int newHeight = ivHead.getHeight() - deltaY / 3;
if (newHeight > maxHeight) {
newHeight = maxHeight;//限定拖动最大高度范围
}
ivHead.getLayoutParams().height = newHeight;//重新设置ivHead的高度值
//使布局参数生效
ivHead.requestLayout();
}
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
maxOverScrollX, maxOverScrollY, isTouchEvent);
}
最后重写onTouchEvent方法,在这里检测手抬起动作,在手抬起的时候通过一个属性动画回复headView原本的高度:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_UP) {
//放手的时候讲imageHead的高度缓慢从当前高度恢复到最初高度
final ValueAnimator animator = ValueAnimator.ofInt(ivHead.getHeight(), orignalHeight);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int animateValue = (int) animator.getAnimatedValue();
ivHead.getLayoutParams().height = animateValue;
//使布局参数生效
ivHead.requestLayout();
}
});
animator.setInterpolator(new OvershootInterpolator(3.f));//弹性插值器
animator.setDuration(350);
animator.start();
}
return super.onTouchEvent(ev);
}
最后的视差拖拽效果实现如下:
透明状态栏+TitleBar
视差拖拽效果实现完成,当然离我们最终要的漂漂的效果还有距离,距离在哪呢,首先我们没有TitleBar,再接着呢,这个状态栏,也太丑了!!!
下面首先实现透明状态栏
在Activity setContentView(R.layout.activity_main)之后,我们执行下面的代码,要注意的是setStatusBarColor这个方法,也就是设置状态栏颜色的方法,是API21也就是5.0以后才有的方法,在5.0之前是无法实现的,不过现在7.0都出来了,5.0之前的机型应该也不多了。
/**
* 初始化状态栏状态
* 设置Activity状态栏透明效果
* 隐藏ActionBar
*/
private void initState() {
//将状态栏设置成透明色
UIUtils.setBarColor(this, Color.TRANSPARENT);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
/**
* 设置状态栏背景色
* 4.4以下不处理
* 4.4使用默认沉浸式状态栏
* @param color
*/
public static void setBarColor(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window win = activity.getWindow();
View decorView = win.getDecorView();
win.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);//沉浸式状态栏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//android5.0及以上才有透明效果
win.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);//清除flag
//让应用的主体内容占用系统状态栏的空间
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | option);
win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
win.setStatusBarColor(color);//设置状态栏背景色
}
}
}
辣么我们现在的效果如何呢?
嚯,已经成功把状态栏变透明了。
接下来看看TitleBar,由于我们实际上是将整个应用沾满整个屏幕,也就是说App应用主体实际上占用了状态栏的空间并且状态栏背景设置成了透明,所以实现了现在这种应用作为状态栏背景的效果。在应用没有占据全屏的情况下,布局应该是从状态栏之下开始布局的,但是现在应用实际上是从屏幕(0,0)开始布局的,所以在实际应用中,TitleBar的高度应该是设置为状态栏高度+原本期望TitleBar的高度。
下面贴上TitleBar代码
最后将TitleBar和ListView放在一个FrameLayout中,界面上的布局,基本完成。
状态栏+TitleBar颜色动态渐变
基本界面已经实现完成,接下来我们看看怎么实现状态栏和TitleBar颜色渐变。前面我们说了,TitleBar和ListView是放在一个FrameLayout中的。所以思路应该很明确了,就是在这个FrameLayout中动态的设置TitleBar的背景色,由于状态栏实际是透明背景然后被TitleBar充满的,所以实际上我们这里说的状态栏+TitleBar颜色动态渐变其实单修改TitleBar的背景色就可以了。
首先我们实现一个自定义GradientLayout ,在GradientLayout中 给ParallaxListView设置一个OnScrollListener ,将根据ParallaxListView滑动的距离和预设值求出一个fraction值,然后根据fraction和估值器计算出颜色值并且设置给TitleBar达到动态更新TitleBar和状态栏颜色的效果。
由于TitlBar右上角的添加按钮需要根据滑动距离更新背景,所以这里我们增加一个接口OnGradientStateChangeListenr ,TitleBar实现这个接口,然后根据GradientLayout传过去的fraction值以及关键值来更新按钮"+"的状态:
public class GradientLayout extends FrameLayout implements OnScrollListener {
private TitleBar tb_title;
private ParallaxListView plv;
private static final float CRITICAL_VALUE = 0.5f;
private OnGradientStateChangeListenr onGradientStateChangeListenr;
private Context context;
/**
* 设置Gradient状态监听
* @param onGradientStateChangeListenr
*/
public void setOnGradientStateChangeListenr(OnGradientStateChangeListenr onGradientStateChangeListenr){
this.onGradientStateChangeListenr = onGradientStateChangeListenr;
}
public GradientLayout(Context context) {
this(context, null);
}
public GradientLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GradientLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new IllegalArgumentException("only can 2 child in this view");
} else {
if (getChildAt(0) instanceof ParallaxListView) {
plv = (ParallaxListView) getChildAt(0);
plv.setOnScrollListener(this);
} else {
throw new IllegalArgumentException("child(0) must be ParallaxListView");
}
tb_title = (TitleBar) getChildAt(1);
tb_title.setTitleBarListenr(this);
}
}
/**
* 设置title背景色
*
* @param color
*/
public void setTitleBackground(int color) {
tb_title.setBackgroundColor(color);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int
totalItemCount) {
if (firstVisibleItem == 0) {
View headView = view.getChildAt(0);
if (headView != null) {
//如果上滑超过headView高度值一半+title高度,开启伴随动画
float slideValue = Math.abs(headView.getTop()) - headView.getHeight() / 2.f +
tb_title.getHeight();
if (slideValue < 0)
slideValue = 0;
float fraction = slideValue / (headView.getHeight() / 2.f);
if (fraction > 1) {
fraction = 1;
}
//Log.e("tag", "fraction = " + fraction);
excuteAnim(fraction);
}
} else {
float fraction = 1;
excuteAnim(fraction);
}
}
private void excuteAnim(float fraction) {
int color = (int) ColorUtil.evaluateColor(fraction, Color.parseColor("#0000ccff"), Color
.parseColor("#ff00ccff"));
setTitleBackground(color);
onGradientStateChangeListenr.onChange(fraction, CRITICAL_VALUE);
}
/**
* 设置TitleBar text
* @param msg
*/
public void setTitleText(String msg){
tb_title.setTitleText(msg);
}
/**
* Gradient变化临界值监听
*/
public interface OnGradientStateChangeListenr{
/**
* 当fraction超过临界值时回调
* @param fraction
* @param criticalValue
*/
public void onChange(float fraction, float criticalValue);
}
}
TitleBar实现OnGradientStateChangeListenr
/**
* 设置Gradient临界值监听
*
* @param gl
*/
public void setTitleBarListenr(GradientLayout gl) {
gl.setOnGradientStateChangeListenr(new OnGradientStateChangeListenr() {
@Override
public void onChange(float fraction, float criticalValue) {
/**
* 当变化值超过临界值
*/
if (fraction >= criticalValue) {
btn_add.setBackgroundResource(R.mipmap.add_trans);
} else {
btn_add.setBackgroundResource(R.mipmap.add_white);
}
}
})
}
至此我们的效果如下:
下拉加载更多
感觉现在基本已经像一个比较靠谱的demo了,现在继续增加下拉加载更多的功能。其实有了前面的铺垫,下拉加载实现起来其实非常简单。
首先在ParallaxListView监听下拉拖拽的距离,然后在松手的时候根据拖拽距离计算出是否出发加载更多,最后通过接口回调的方式将这个下拉刷新的状态以及结果通知给GradientLayout,GradientLayout又通过接口回调的方式通知TitleBar更新界面。不多说,直接上代码,要注意的一点是,为了独立开ParallaxListView和TitleBar,ParallaxListView和TitleBar的状态更新全部通过父Layout GradientLayout。
ParallaxListView增加刷新接口以及模拟请求数据
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_UP) {
//如果松手时headView滑动的距离大于预设值,回调onRefesh
//Log.e("tag", "ivHead.getHeight() = " + ivHead.getHeight());
//Log.e("tag", "orignalHeight = " + orignalHeight);
if (ivHead.getHeight() - orignalHeight > 60) {
if(onRefeshChangeListener != null){
onRefeshChangeListener.onListRefesh();
if(!isRefeshing){//当前不是刷新状态时
getData();
isRefeshing = true;
}
}
}
//放手的时候讲imageHead的高度缓慢从当前高度恢复到最初高度
final ValueAnimator animator = ValueAnimator.ofInt(ivHead.getHeight(), orignalHeight);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int animateValue = (int) animator.getAnimatedValue();
ivHead.getLayoutParams().height = animateValue;
//使布局参数生效
ivHead.requestLayout();
}
});
animator.setInterpolator(new OvershootInterpolator(3.f));//弹性插值器
animator.setDuration(350);
animator.start();
}
return super.onTouchEvent(ev);
}
/**
* 开启一个线程模拟网络请求操作
*/
private void getData(){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//test
onRefeshChangeListener.onListRefeshFinish(true);
isRefeshing = false;
//onRefeshChangeListener.onRefeshFinish(false);
}
}).start();
}
GradientLayout实现ParallaxListView.OnRefeshChangeListener并且新增一个OnRefeshChangeListener接口用于将状态给TitleBar,实际上GradientLayout相当于ParallaxListView和TitleBar的传话者。
@Override
public void onListRefesh() {
onRefeshChangeListener.onListRefesh();
}
@Override
public void onListRefeshFinish(final boolean isRefeshSuccess) {
UIUtils.runOnUIThread(new Runnable() {
@Override
public void run() {
if(isRefeshSuccess){
//Toast.makeText(UIUtils.getContext(), "refesh success.", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(UIUtils.getContext(), "refesh failed.", Toast.LENGTH_SHORT).show();
}
}
});
//不论刷新成功还是失败,都要通知titleBar刷新完成
onRefeshChangeListener.onListRefeshFinish();
}
/**
* GradientLayout中的子list列表刷新状态监听
*/
public interface OnRefeshChangeListener{
/**
* 开始刷新列表,请求数据
*/
void onListRefesh();
/**
* 刷新列表完成
*/
void onListRefeshFinish();
}
TitleBar实现父Layout的接口,然后通过一个Tween动画实现刷新进度圈圈的旋转:
/**
* 设置TitleBar监听
*
* @param gl
*/
public void setTitleBarListenr(GradientLayout gl) {
gl.setOnGradientStateChangeListenr(new OnGradientStateChangeListenr() {
@Override
public void onChange(float fraction, float criticalValue) {
/**
* 当变化值超过临界值
*/
if (fraction >= criticalValue) {
btn_add.setBackgroundResource(R.mipmap.add_trans);
} else {
btn_add.setBackgroundResource(R.mipmap.add_white);
}
}
});
gl.setOnRefeshChangeListener(new GradientLayout.OnRefeshChangeListener() {
@Override
public void onListRefesh() {
UIUtils.runOnUIThread(new Runnable() {
@Override
public void run() {
iv_title.setVisibility(View.VISIBLE);
//执行动画
Animation anim = AnimationUtils.loadAnimation(context, R.anim.refesh_roate);
anim.setInterpolator(new LinearInterpolator());
iv_title.startAnimation(anim);
}
});
}
@Override
public void onListRefeshFinish() {
UIUtils.runOnUIThread(new Runnable() {
@Override
public void run() {
iv_title.setVisibility(View.INVISIBLE);
iv_title.clearAnimation();
}
});
}
});
}
现在再看看我们的效果,泪流满面,终于实现大部分效果了!
点击按钮∨弹出PopupWindow list选项+模糊背景效果
接下来要实现的是QQ空间好友动态列表选项弹出的效果,QQ是弹出一个屏幕等宽的列表。我们这里实现的稍微跟QQ的有点不一样,我们这里实现的效果更像是3D touch的效果。
先来撸一撸思路,既然是弹出来,首相第一个想到的实现方法,当然是PopupWindow,然后背景虚化,其实网上也有很多的模糊虚化方法,然后再接着就是将我们要添加的View设到屏幕上。OK,思路很清晰简单,然鹅,真的辣么简单吗?
并没有啊!!!一开始就出点了小意外,就是关于WindowManager.LayoutParams,由于这玩意的flag值实在是太多了,网上这类功能相关的资料又比较少,最后好一番折腾,总算是实现了我们要的效果,也就是虚化背景不满全屏,但是不知道为什么,模拟器状态栏依然显示的是半透明状态栏,好在真机上运行都一切正常,然后就妥妥的无视模拟器这个问题了。
先看看我们配置的WindowManager.LayoutParams,这里只列出来我们用到的几个flag值,折腾了小半天,最后也就用到这么几个,委屈的不行,哈哈。
private void initParams() {
params = new WindowManager.LayoutParams();
params.width = MATCH_PARENT;
params.height = MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN //布满屏幕,忽略状态栏
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS //透明状态栏
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; //透明虚拟按键栏
params.format = PixelFormat.TRANSLUCENT;//支持透明
params.gravity = Gravity.LEFT | Gravity.TOP;
}
接下来要思考的是将listView塞进一个空layout,这个地方要注意的是,由于我们这里弹出的listView背景是一个.9图片,所以一定要记住将这个.9图片设置个listView做背景!!!而不是设置给我们的空layout!!!
由于listView宽度我们希望是自适应而不是充满屏幕,所以我们要自定义一个listView,并且根据item的最大宽度设置listView的宽度,下面贴上自定义listView的代码。
public class PopupListView extends ListView {
private Context context;
public PopupListView(Context context) {
this(context, null);
}
public PopupListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PopupListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = measureWidthByChilds() + getPaddingLeft() + getPaddingRight();
super.onMeasure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.UNSPECIFIED),
heightMeasureSpec);//注意,这个地方一定是MeasureSpec.UNSPECIFIED
}
public int measureWidthByChilds() {
int maxWidth = 0;
View view = null;
for (int i = 0; i < getAdapter().getCount(); i++) {
view = getAdapter().getView(i, view, this);
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
if (view.getMeasuredWidth() > maxWidth) {
maxWidth = view.getMeasuredWidth();
}
view = null;
}
return maxWidth;
}
}
有一点比较重要,我们在popupWindow弹出来的时候,需要拦截返回键事件,点击返回键时dismiss掉popupWindow,如何拦截返回键事件呢?我们这里通过一个自定义layout,重写这个layout的dispatchKeyEvent事件然后暴露一个接口,实际上相当于对dispatchKeyEvent事件做了一次传递,然后在popupWindow中实现setDispatchKeyEventListener的回调。
/**
* 拦截WindowManager中view的按键事件,此处主要用于返回键事件拦截
* Created by Horrarndoo on 2017/3/28.
*/
public class PopupRootLayout extends FrameLayout{
private DispatchKeyEventListener mDispatchKeyEventListener;
public PopupRootLayout(Context context) {
super(context);
}
public PopupRootLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PopupRootLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mDispatchKeyEventListener != null) {
return mDispatchKeyEventListener.dispatchKeyEvent(event);
}
return super.dispatchKeyEvent(event);
}
public DispatchKeyEventListener getDispatchKeyEventListener() {
return mDispatchKeyEventListener;
}
public void setDispatchKeyEventListener(DispatchKeyEventListener mDispatchKeyEventListener) {
this.mDispatchKeyEventListener = mDispatchKeyEventListener;
}
//监听接口
public static interface DispatchKeyEventListener {
boolean dispatchKeyEvent(KeyEvent event);
}
}
最后贴上PopupWindow的代码,设置虚化背景和弹出/隐藏ListView都是通过属性动画,比较简单,代码注释也比较全,就不多做解释了。
public class BlurPopupWindow {
/**
* 顶部弹出popupWindow关键字
*/
public static final int KEYWORD_LOCATION_TOP = 1;
/**
* 点击处弹出popupWindow关键字
*/
public static final int KEYWORD_LOCATION_CLICK = 2;
private Activity activity;
private WindowManager.LayoutParams params;
private boolean isDisplay;
private WindowManager windowManager;
private PopupRootLayout rootView;
private ViewGroup contentLayout;
private final int animDuration = 250;//动画执行时间
private boolean isAniming;//动画是否在执行
/**
* BlurPopupWindow构造函数
*
* @param activity 当前弹出/消失BlurPopupWindow的Activity
* @param view 要弹出/消失的view内容
* 默认从点击处弹出/消失popupWindow
*/
public BlurPopupWindow(Activity activity, View view) {
initBlurPopupWindow(activity, view, KEYWORD_LOCATION_CLICK);
}
/**
* BlurPopupWindow构造函数
*
* @param activity 当前弹出/消失BlurPopupWindow的Activity
* @param view 要弹出/消失的view内容
* @param keyword 弹出/消失位置关键字 KEYWORD_LOCATION_TOP:顶部弹出
* KEYWORD_LOCATION_CLICK:点击位置弹出
*/
public BlurPopupWindow(Activity activity, View view, int keyword) {
initBlurPopupWindow(activity, view, keyword);
}
/**
* BlurPopupWindow初始化
*
* @param activity 当前弹出BlurPopupWindow的Activity
* @param view 要弹出/消失的view内容
* @param keyword 弹出/消失位置关键字 KEYWORD_LOCATION_TOP:顶部弹出
* KEYWORD_LOCATION_CLICK:点击位置弹出
*/
private void initBlurPopupWindow(Activity activity, View view, int keyword) {
this.activity = activity;
windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
switch (keyword) {
case KEYWORD_LOCATION_CLICK:
view.setPadding(5, 10, 5, 0);//由于.9图片有部分是透明,往下padding 10个pix,左右padding 5个pix为了美观
view.setBackgroundResource(R.drawable.popup_bg);
break;
case KEYWORD_LOCATION_TOP:
ImageView imageView = (ImageView) view;
imageView.setScaleType(ImageView.ScaleType.FIT_START);
imageView.setImageDrawable(activity.getResources().getDrawable(R.mipmap.popup_top_bg));
break;
default:
break;
}
initLayout(view, keyword);
}
private void initLayout(View view, final int keyword) {
rootView = (PopupRootLayout) View.inflate(activity, R.layout.popupwindow_layout, null);
contentLayout = (ViewGroup) rootView.findViewById(R.id.content_layout);
initParams();
contentLayout.addView(view);
//点击根布局时, 隐藏弹出的popupWindow
rootView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismissPopupWindow(keyword);
}
});
rootView.setDispatchKeyEventListener(new DispatchKeyEventListener() {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (rootView.getParent() != null) {
dismissPopupWindow(keyword);
}
return true;
}
return false;
}
});
}
private void initParams() {
params = new WindowManager.LayoutParams();
params.width = MATCH_PARENT;
params.height = MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN //布满屏幕,忽略状态栏
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS //透明状态栏
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; //透明虚拟按键栏
params.format = PixelFormat.TRANSLUCENT;//支持透明
params.gravity = Gravity.LEFT | Gravity.TOP;
}
/**
* 将bitmap模糊虚化并设置给view background
*
* @param view
* @param bitmap
* @return 虚化后的view
*/
private View getBlurView(View view, Bitmap bitmap) {
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth() / 3, bitmap
.getHeight() / 3, false);
Bitmap blurBitmap = UIUtils.getBlurBitmap(activity, scaledBitmap, 5);
view.setAlpha(0);
view.setBackgroundDrawable(new BitmapDrawable(null, blurBitmap));
alphaAnim(view, 0, 1, animDuration);
return view;
}
/**
* 弹出选项弹窗
* 默认从点击位置弹出
*
* @param locationView
*/
public void displayPopupWindow(View locationView) {
displayPopupWindow(locationView, KEYWORD_LOCATION_CLICK);
}
/**
* 弹出选项弹窗
*
* @param locationView 被点击的view
* @param keyword 弹出位置关键字
*/
public void displayPopupWindow(View locationView, int keyword) {
if (!isAniming) {
isAniming = true;
try {
int[] point = new int[2];
float x = 0;
float y = 0;
contentLayout.measure(0, 0);
switch (keyword) {
case KEYWORD_LOCATION_CLICK:
//得到该view相对于屏幕的坐标
locationView.getLocationOnScreen(point);
x = point[0] + locationView.getWidth() - contentLayout.getMeasuredWidth();
y = point[1] + locationView.getHeight();
break;
case KEYWORD_LOCATION_TOP:
x = 0;
y = 0;
break;
default:
break;
}
contentLayout.setX(x);
contentLayout.setY(y);
View decorView = activity.getWindow().getDecorView();
Bitmap bitmap = UIUtils.viewToBitmap(decorView);//将view转成bitmap
View blurView = getBlurView(rootView, bitmap);//模糊图片
windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
//将处理过的blurView添加到window
windowManager.addView(blurView, params);
//popupWindow动画
popupAnim(contentLayout, 0.f, 1.f, animDuration, keyword, true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 消失popupWindow
* 默认从点击处开始消失
*/
public void dismissPopupWindow() {
dismissPopupWindow(KEYWORD_LOCATION_CLICK);
}
/**
* 消失popupWindow
* @param keyword 消失位置关键字 KEYWORD_LOCATION_TOP:顶部弹出
* KEYWORD_LOCATION_CLICK:点击位置弹出
*/
public void dismissPopupWindow(int keyword) {
if (!isAniming) {
isAniming = true;
if (isDisplay) {
popupAnim(contentLayout, 1.f, 0.f, animDuration, keyword, false);
}
}
}
/**
* 设置透明度属性动画
*
* @param view 要执行属性动画的view
* @param start 起始值
* @param end 结束值
* @param duration 动画持续时间
*/
private void alphaAnim(final View view, int start, int end, int duration) {
ObjectAnimator.ofFloat(view, "alpha", start, end).setDuration(duration).start();
}
/**
* popupWindow属性动画
*
* @param view
* @param start
* @param end
* @param duration
* @param keyword
* @param isToDisplay 显示或消失 flag值
*/
private void popupAnim(final View view, float start, final float end, int duration, final int
keyword, final boolean isToDisplay) {
ValueAnimator va = ValueAnimator.ofFloat(start, end).setDuration(duration);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
switch (keyword) {
case KEYWORD_LOCATION_CLICK:
view.setPivotX(view.getMeasuredWidth());
view.setPivotY(0);
view.setScaleX(value);
view.setScaleY(value);
view.setAlpha(value);
break;
case KEYWORD_LOCATION_TOP:
view.setPivotX(0);
view.setPivotY(0);
view.setScaleY(value);
break;
default:
break;
}
}
});
va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAniming = false;
if(isToDisplay) {//当前为弹出popupWindow
isDisplay = true;
onPopupStateListener.onDisplay(isDisplay);
}else{//当前为消失popupWindow
try {
if (isDisplay) {
windowManager.removeViewImmediate(rootView);
}
} catch (Exception e) {
e.printStackTrace();
}
isDisplay = false;
onPopupStateListener.onDismiss(isDisplay);
}
}
});
va.start();
}
}
现在看看实现效果。
点击按钮+顶部弹出PopupWindow界面+模糊背景效果
接下来的是最难的一个地方!!!
并不是!!!骗你的!!!哈哈,实际上前面的代码也已经写的很清楚了,我们这个顶部弹出的这个界面是个什么东西呢?没错!!!就是一个ImageView!!!鹅已!!!
OK,玩笑开完。要注意一点就是ImageView应避免设置background而是应该设置src,因为设置background可能会因为图片比例导致图片拉伸失真,当然QQ顶部弹下来的肯定不是一个ImageView,这里也只是做一个效果,实际应用中自然可以根据需求去拓展。
最后定义一个接口OnPopupStateListener 用于将PopupWindow状态告知给TitleBar,然后TitleBar按键根据回调状态给按钮“+”设置属性动画。
/**
* popupWindow显示和消失状态变化接口
*/
public interface OnPopupStateListener {
/**
* popupWindow状态变化
* @param isDisplay popupWindow当前状态 true:显示 false:消失
*/
// void onChange(boolean isDisplay);
/**
* popupWindow为显示状态
*/
void onDisplay(boolean isDisplay);
/**
* popupWindow为消失状态
*/
void onDismiss(boolean isDisplay);
}
TitleBar 接口实现以及按钮动画
private void initPopupWindow(final Activity context) {
ImageView iv_popup_top = new ImageView(context);
LayoutParams params = new LayoutParams(LayoutParams
.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
iv_popup_top.setLayoutParams(params);
blurPopupWindow = new BlurPopupWindow(context, iv_popup_top,
KEYWORD_LOCATION_TOP);
blurPopupWindow.setOnPopupStateListener(new BlurPopupWindow.OnPopupStateListener() {
@Override
public void onDisplay(boolean isDisplay) {
TitleBar.this.isDisplay = isDisplay;
}
@Override
public void onDismiss(boolean isDisplay) {
TitleBar.this.isDisplay = isDisplay;
dismissAnim();
}
});
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_add:
if (onBarClicklistener != null) {
if (isDisplay) {
dismissPopupWindow();
} else {
displayPopupWindow(v);
}
onBarClicklistener.onBarClick(R.id.btn_add);
}
break;
case R.id.btn_back:
if (onBarClicklistener != null) {
onBarClicklistener.onBarClick(R.id.btn_back);
}
break;
}
}
public void displayPopupWindow(View v) {
displayAnim();
blurPopupWindow.displayPopupWindow(v, KEYWORD_LOCATION_TOP);
}
public void dismissPopupWindow() {
dismissAnim();
blurPopupWindow.dismissPopupWindow(KEYWORD_LOCATION_TOP);
}
/**
* Add按钮逆时针转90度
*/
private void displayAnim() {
ObjectAnimator.ofFloat(btn_add, "rotation", 0.f, -90.f).setDuration(500).start();
}
/**
* Add按钮瞬时间转90度
*/
private void dismissAnim() {
ObjectAnimator.ofFloat(btn_add, "rotation", 0.f, 90.f).setDuration(500).start();
}
贴上最终模拟器上运行的效果
最后附上完整demo地址:https://github.com/Horrarndoo/parallaxListView