简单定制Android控件(3) - 打造通用的PopupWindow(四)

上一篇文章(链接点我),我们实现了一个从底部滑上来的popup,这一次,我们将会实现一个更好玩的,而且也是更常见的——仿微信朋友圈点赞的popup


在文章之前,请让我说明一下这次github代码的更新。

这一次我们的BasePopup多了两个方法,分别是:

    public Animation getExitAnimation(){
        return null;
    }
    public Animator getExitAnimator(){
        return null;
    }


它们在BasePopupWindow的108行和111行,详情请看github

这两行代码很明显,就是用来dismiss时用的,至于为什么不把它弄成抽象,是因为popup默认退出无需动画,也就是这个并非必须实现的,所以我们添加这个方法但不要求子类必须实现。


这两行代码对应的地方就是我们BasePopupWindow的dismiss()方法,通过给animation设置一个animationListener,在onAnimationEnd里面执行popup的dismiss方法,就做到了在退出前播放动画,然后消掉popup。



OK,介绍结束,下面开始我们的朋友圈popup实现:

本次实现的效果如下(添加了退出动画)




首先是毫无疑问的xml文件:

popup_comment.xml:

<?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="wrap_content"
    android:layout_height="wrap_content"
    tools:background="@android:color/holo_red_light">

    <LinearLayout
        android:id="@+id/comment_popup_contianer"
        android:layout_width="180dp"
        android:orientation="horizontal"
        android:layout_height="40dp"
        android:background="@drawable/bg_comment_popup"
        android:weightSum="2"
        android:clipChildren="false"
        >
        <RelativeLayout
            android:id="@+id/item_like"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:gravity="center"
            >
            <TextView
                android:id="@+id/tv_like"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="赞  "
                android:layout_centerInParent="true"
                android:textColor="@color/comment_popup_tx_bg"
                android:textSize="12sp"
                android:drawablePadding="5dp"
                />
            <ImageView
                android:id="@+id/iv_like"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:layout_toLeftOf="@id/tv_like"
                android:layout_marginRight="5dp"
                android:src="@drawable/ic_like"
                />
        </RelativeLayout>
        <View
            android:id="@+id/v_line"
            android:layout_width="0.5dp"
            android:layout_gravity="center_vertical"
            android:layout_height="15dp"
            android:background="#6c6c6c"
            />
        <RelativeLayout
            android:id="@+id/item_comment"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            >
            <TextView
                android:layout_centerVertical="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="评论"
                android:drawableLeft="@drawable/ic_comment"
                android:textColor="@color/comment_popup_tx_bg"
                android:textSize="12sp"
                android:drawablePadding="5dp"
                android:layout_centerInParent="true"
                />
        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

其中背景是手动绘制的,xml配置如下:

bg_comment_popup.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <solid android:color="@color/comment_popup_bg"/>
    <corners android:radius="4dp"/>


</shape>

色值如下:

comment_popup_bg: #292929

comment_popup_tx_bg: #c7c7c7


xml写完后,我们就开始我们的java,这一次我们依然继承我们的BasePopupWindow,不过与之前不同,这次我们需要针对性的修改一下。


首先是我们的构造器,因为我们的popup与之前的差别在于,我们这次的popup并不需要全屏,而是包裹内容,所以我们的构造器就需要这么写


 public CommentPopup(Activity context) {
        this(context, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

 public CommentPopup(Activity context, int w, int h) {
        super(context, w, h);

        viewLocation = new int[2];
        mHandler=new Handler();

        mLikeAnimaView = (ImageView) mPopupView.findViewById(R.id.iv_like);
        mLikeText = (TextView) mPopupView.findViewById(R.id.tv_like);

        mLikeClikcLayout = (RelativeLayout) mPopupView.findViewById(R.id.item_like);
        mCommentClickLayout = (RelativeLayout) mPopupView.findViewById(R.id.item_comment);

        mLikeClikcLayout.setOnClickListener(this);
        mCommentClickLayout.setOnClickListener(this);

        buildAnima();
    }

对外而言,我们仍然只需要调用new CommentPopup((Activity)context);

但对内而言,我们就给定popup的大小为wrap_content,当然,我们的xml最外层父控件也需要wrap_content,然后主体我们限定一个宽高,这样这个popup就不是全屏的了。


接下来就是定义我们的动画,这个动画用于我们点赞后,那个心心放大,关于这个动画,我们需要注意到xml的心心所在的父view需要包含一个参数:clipChildren="false",有了这个参数,我们的动画才能顺利的突破父view的区域,否则就会被裁剪掉。


代码如下:

    private void buildAnima() {
        ScaleAnimation mScaleAnimation = new ScaleAnimation(1f, 2f, 1f, 2f, Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        mScaleAnimation.setDuration(200);
        mScaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mScaleAnimation.setFillAfter(false);

        AlphaAnimation mAlphaAnimation = new AlphaAnimation(1, .2f);
        mAlphaAnimation.setDuration(400);
        mAlphaAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mAlphaAnimation.setFillAfter(false);

        mAnimationSet=new AnimationSet(false);
        mAnimationSet.setDuration(400);
        mAnimationSet.addAnimation(mScaleAnimation);
        mAnimationSet.addAnimation(mAlphaAnimation);
        mAnimationSet.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        dismiss();
                    }
                }, 150);

            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

动画代码没啥好解释的。。。


接下来就是普通的实现我们父类的方法

    @Override
    public Animation getAnimation() {
        return getScaleAnimation(0.0f, 1.0f, 1.0f, 1.0f, Animation.RELATIVE_TO_SELF, 1.0f,
                Animation.RELATIVE_TO_SELF, 0.0f);
    }

    @Override
    public Animator getAnimator() {
        return null;
    }

    @Override
    public Animation getExitAnimation() {
        return getScaleAnimation(1.0f, 0.0f, 1.0f, 1.0f, Animation.RELATIVE_TO_SELF, 1.0f,
                Animation.RELATIVE_TO_SELF, 0.0f);
    }

    @Override
    public View getPopupView() {
        return LayoutInflater.from(mContext).inflate(R.layout.popup_comment, null);
    }

    @Override
    public View getAnimaView() {
        return mPopupView.findViewById(R.id.comment_popup_contianer);
    }

如果有看前面几篇文章的朋友,对这个应该理解不难,值得注意的是,scaleAnimation()我们的x方向参照点是popup的最右边,所以我们设置为RELATIVE_TO_SELF,1.0F,从0.0~1.0代表着x方向的宽度百分比。


上面这一些弄完后,就是我们的重头戏了。


大家注意到,朋友圈是一个listview,每个listview都有一个评论按钮,那么我们怎样才能正确的将popup展示到我们点击的评论按钮上呢。


于是今天我们不再用showPopup(),而是用showPopup(View v)

showPopup(View v)在我们的父类上已经写了,其根本依然是调用popupWindow.showAtLocation,但是除了第一个参数可以是view外,另外还有一些参数


简单的说,popupWindow.showAtLocation(View parent, int gravity, int x, int y)一共有四个参数


第一个不用说,就是参照的view,这个view可以是任何的view,比如说顶级的decorview,事实上我们的showPopup()这个无参方法用的view就是顶级decorview,其id是android.R.id.content。


第二个参数有一定的误导性,很容易理解为popup的内容的gravity,但事实上并非如此,第二个参数实质上是指参考屏幕的哪个位置。

这一个我们可以通过源码来看看:

showAtLocation的源码:

    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;
        }

        TransitionManager.endTransitions(mDecorView);

        unregisterForScrollChanged();

        mIsShowing = true;
        mIsDropdown = false;

        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);

        // Only override the default if some gravity was specified.
        if (gravity != Gravity.NO_GRAVITY) {
            p.gravity = gravity;
        }

        p.x = x;
        p.y = y;

        invokePopup(p);
    }

可以看到,我们的gravity最终是赋值给p,而p是属于windowManager,那么我们很有理由相信,p的作用是给windowManager来addView进而凭空生成一个view的,事实也是如此,我们点进invokePopup

    private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);

        setLayoutDirectionFromAnchor();

        mWindowManager.addView(decorView, p);

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

很明显看到,我们的gravity最终是用到mWindowManager.addView里面,所以我们的第二个参数实质上是参照屏幕的位置


那么第三个和第四个参数就很明朗了,就是偏移值嘛。


OK,既然我们知道了位置,那么久接下来为了让我们的popup正确的显示在我们的评论按钮,我们就需要重写我们basepopupwindow的showPopup(View v),代码如下:

 @Override
    public void showPopupWindow(View v) {
        try {
            //得到v的位置
            v.getLocationOnScreen(viewLocation);
            //展示位置:
            //参照点为屏幕的右上角,偏移值为:x方向距离参照view的一定倍数距离
            //垂直方向自身减去popup自身高度的一半(确保在中间)
            mPopupWindow.showAtLocation(v, Gravity.RIGHT | Gravity.TOP, (int) (v.getWidth() * 1.8),
                    viewLocation[1] - DimensUtils.dipToPx(mContext,15f));
            if (getAnimation() != null && getAnimaView() != null) {
                getAnimaView().startAnimation(getAnimation());
            }
        } catch (Exception e) {
            Log.w("error","error");
        }
    }
在展示前,我们先取得view(也就是评论的那个imageView)在屏幕的位置,然后通过showAtLocation,偏移量在Y上面就是我们view所在的位置减去我们popup的高度的一半(或许会有5dp的出入,暂时不清楚原因)。

然后就是跟父类代码一样执行就可以了。


接下来就是为了解耦,我们的点赞/评论的点击事件通过接口暴露给外面进行回调操作,这样我们的朋友圈点赞就完成了。


全部代码如下:

/**
 * Created by 大灯泡 on 2016/1/16.
 * 微信朋友圈评论弹窗
 */
public class CommentPopup extends BasePopupWindow implements View.OnClickListener {

    private ImageView mLikeAnimaView;
    private TextView mLikeText;

    private RelativeLayout mLikeClikcLayout;
    private RelativeLayout mCommentClickLayout;

    private int[] viewLocation;

    private OnCommentPopupClickListener mOnCommentPopupClickListener;

    private Handler mHandler;
    public CommentPopup(Activity context) {
        this(context, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    public CommentPopup(Activity context, int w, int h) {
        super(context, w, h);

        viewLocation = new int[2];
        mHandler=new Handler();

        mLikeAnimaView = (ImageView) mPopupView.findViewById(R.id.iv_like);
        mLikeText = (TextView) mPopupView.findViewById(R.id.tv_like);

        mLikeClikcLayout = (RelativeLayout) mPopupView.findViewById(R.id.item_like);
        mCommentClickLayout = (RelativeLayout) mPopupView.findViewById(R.id.item_comment);

        mLikeClikcLayout.setOnClickListener(this);
        mCommentClickLayout.setOnClickListener(this);

        buildAnima();
    }

    private AnimationSet mAnimationSet;

    private void buildAnima() {
        ScaleAnimation mScaleAnimation = new ScaleAnimation(1f, 2f, 1f, 2f, Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        mScaleAnimation.setDuration(200);
        mScaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mScaleAnimation.setFillAfter(false);

        AlphaAnimation mAlphaAnimation = new AlphaAnimation(1, .2f);
        mAlphaAnimation.setDuration(400);
        mAlphaAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mAlphaAnimation.setFillAfter(false);

        mAnimationSet=new AnimationSet(false);
        mAnimationSet.setDuration(400);
        mAnimationSet.addAnimation(mScaleAnimation);
        mAnimationSet.addAnimation(mAlphaAnimation);
        mAnimationSet.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        dismiss();
                    }
                }, 150);

            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

    @Override
    public void showPopupWindow(View v) {
        try {
            //得到v的位置
            v.getLocationOnScreen(viewLocation);
            //展示位置:
            //参照点为view的右上角,偏移值为:x方向距离参照view的一定倍数距离
            //垂直方向自身减去popup自身高度的一半(确保在中间)
            mPopupWindow.showAtLocation(v, Gravity.RIGHT | Gravity.TOP, (int) (v.getWidth() * 1.8),
                    viewLocation[1] - DimensUtils.dipToPx(mContext,15f));

            if (getAnimation() != null && getAnimaView() != null) {
                getAnimaView().startAnimation(getAnimation());
            }
        } catch (Exception e) {
            Log.w("error","error");
        }
    }

    @Override
    public Animation getAnimation() {
        return getScaleAnimation(0.0f, 1.0f, 1.0f, 1.0f, Animation.RELATIVE_TO_SELF, 1.0f,
                Animation.RELATIVE_TO_SELF, 0.0f);
    }

    @Override
    public Animator getAnimator() {
        return null;
    }

    @Override
    public Animation getExitAnimation() {
        return getScaleAnimation(1.0f, 0.0f, 1.0f, 1.0f, Animation.RELATIVE_TO_SELF, 1.0f,
                Animation.RELATIVE_TO_SELF, 0.0f);
    }

    @Override
    public View getPopupView() {
        return LayoutInflater.from(mContext).inflate(R.layout.popup_comment, null);
    }

    @Override
    public View getAnimaView() {
        return mPopupView.findViewById(R.id.comment_popup_contianer);
    }
    //=============================================================Getter/Setter

    public OnCommentPopupClickListener getOnCommentPopupClickListener() {
        return mOnCommentPopupClickListener;
    }

    public void setOnCommentPopupClickListener(OnCommentPopupClickListener onCommentPopupClickListener) {
        mOnCommentPopupClickListener = onCommentPopupClickListener;
    }

    //=============================================================clickEvent
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.item_like:
                if (mOnCommentPopupClickListener != null) {
                    mOnCommentPopupClickListener.onLikeClick(v, mLikeText);
                    mLikeAnimaView.clearAnimation();
                    mLikeAnimaView.startAnimation(mAnimationSet);
                }
                break;
            case R.id.item_comment:
                if (mOnCommentPopupClickListener != null) {
                    mOnCommentPopupClickListener.onCommentClick(v);
                    dismiss();
                }
                break;
        }
    }

    //=============================================================InterFace
    public interface OnCommentPopupClickListener {
        void onLikeClick(View v, TextView likeText);

        void onCommentClick(View v);
    }
    //=============================================================abortMethods


    @Override
    public View getInputView() {
        return null;
    }

    @Override
    public View getDismissView() {
        return null;
    }
}


下一篇,我们将实现一个含有输入框的popup。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值