Android自定义控件:打造自己的QQ空间主页

前面已经实现过仿QQ的List抽屉效果以及仿QQ未读消息拖拽效果,具体请见:
Android自定义控件:类QQ抽屉效果
Android自定义控件:类QQ未读消息拖拽效果
趁热打铁,这次我们实现QQ空间的主页全效果,先贴上我们最终的完成效果图:
这里写图片描述
可以看到,我们实现了如下效果:
1. 下拉拖拽视差效果
2. 透明状态栏+TitleBar
3. 状态栏+TitleBar颜色动态渐变
4. 下拉加载更多
5. 点击按钮∨弹出PopupWindow list选项+模糊背景效果
6. 点击按钮+顶部弹出PopupWindow界面+模糊背景效果

下拉拖拽视差效果

第一步先实现拖拽视差效果,也就是下拉的时候,有一种阻滞感,然后手抬起的时候,会稍微回弹一下。
在实现效果之前,我们先看一下实现原理,我们看一下下面这张图:
这里写图片描述
实际上呢,一整个视差效果界面,其实就是一个ListView。我们给listView设置了一个headView,然后设置headView 布局的scaleType为centerCrop,取src图片的中部也就是图中绿色部分,这部分是初始显示区域。headView的src图片上下部分实际上是处于界面之外没有显示出来,也就是图中的棕色部分。
下面贴上头布局代码:

list_item_head.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="330dp"
        android:scaleType="centerCrop"
        android:id="@+id/iv_head"
        android:src="@mipmap/parallax_img"/>

</LinearLayout>

然后在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代码

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tb_title"
    android:layout_width="match_parent"
    android:layout_height="90dp">

    <Button
        android:id="@+id/btn_back"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:background="@mipmap/back"/>

    <RelativeLayout
        android:id="@+id/rl_title"
        android:layout_width="0dp"
        android:layout_height="80dp"
        android:layout_marginTop="43dp"
        android:layout_weight="1">

        <ImageView
            android:id="@+id/iv_title"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_toLeftOf="@+id/tv_title"
            android:layout_marginRight="5dp"
            android:layout_marginTop="2dp"
            android:src="@mipmap/refesh"
            android:visibility="invisible"/>

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="80dp"
            android:background="@android:color/transparent"
            android:text="Title"
            android:layout_centerInParent="true"
            android:textColor="@android:color/white"
            android:textSize="20sp"/>
    </RelativeLayout>


    <Button
        android:id="@+id/btn_add"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_gravity="center"
        android:layout_marginRight="20dp"
        android:layout_marginTop="10dp"
        android:background="@mipmap/add_white"/>
</LinearLayout>

最后将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

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值