仿微信图库文件夹选择的交互

前言

这是按照微信图库中文件夹选择的展示动画做出来的PopupWindow。之前自己做过图库部分,发现微信图库中这个文件夹显示和消失的动画很自然,很舒服,而之前做的不是那么自然,于是就做了这么个东西。

效果

文件夹选择效果

做这个交互用到的主要是popupwindow和动画相关的东西。这里先说PopupWindow的部分。
上代码
自定义的PopupWindow,重写了显示和消失的方法,加入了自定义的动画。

public class GalleryDirPopupWindow extends PopupWindow{
    private View mConvertView;
    private ListView lv_dirs;
    private View view;
    private BaseAdapter mAdapter;
    private List<PhotoDir> mPhotoDirs;
    //动画相关
    AnimatorSet animatorSet ;
    ObjectAnimator mPlaceViewShowAnimation;
    ObjectAnimator mPlaceViewDismissAnimation;
    ObjectAnimator mListViewShowAnimation;
    ObjectAnimator mListViewDismissAnimation;

    //是否正在消失,为了解决快速的两次点击出现的问题
    boolean isDismissing = false;

    private Context mContext;

    /**
     *
     * @param context
     * @param photoDirs
     * @param height    之所以要设置高度而不用match_parent,是因为安卓7.0的window的属性有变化,
     *             设置为match_parent之后直接全屏,showasdrapdown的位移失效,所以必须设置精确的高度
     */
    public GalleryDirPopupWindow(Context context,List<PhotoDir> photoDirs,int height) {
        this.mPhotoDirs = photoDirs;
        this.mConvertView = LayoutInflater.from(context).inflate(R.layout.gallery_list_dir, null);
        this.mContext = context;
        // 设置SelectPicPopupWindow的View
        this.setContentView(mConvertView);
        // 设置SelectPicPopupWindow弹出窗体的宽
        this.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
        // 设置SelectPicPopupWindow弹出窗体的高
        this.setHeight(height);
        // 设置SelectPicPopupWindow弹出窗体可点击
        this.setFocusable(true);
        // 加上它之后,setOutsideTouchable()才会生效;并且PopupWindow才会对手机的返回按钮有响应
        this.setBackgroundDrawable(new BitmapDrawable());
        // 设置popWindow的显示和消失动画
        //this.setAnimationStyle(R.style.pop_anim_style);
        this.setOutsideTouchable(true);
        initViews();
        initShowAnimation();
    }

    public void initViews() {
        lv_dirs = (ListView) mConvertView.findViewById(R.id.lv_dirs);
        view = (View)mConvertView.findViewById(R.id.view);
        // mMenuView添加OnTouchListener监听判断获取触屏位置如果在选择框外面则销毁弹出框
        mConvertView.setOnTouchListener(new View.OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                int height = lv_dirs.getTop();
                int y = (int) event.getY();
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    if (y < height) {
                        dismiss();
                    }
                }
                return true;
            }
        });
        mAdapter =new GalleryDirsAdapter(mContext,mPhotoDirs);
        lv_dirs.setAdapter(mAdapter);
    }

    public void updatePopWindow() {
        mAdapter.notifyDataSetChanged();
    }


    @Override
    public void dismiss() {
        if(isDismissing)
            return ;
        isDismissing = true;
        startDismissAnimator();
    }
    //这种带Gravity参数的方法是API 19新引入的,所以为了适配应该去掉这个gravity,因为这里用不到
    @Override
    public void showAsDropDown(View anchor,int x,int y,int gravity) {
        super.showAsDropDown(anchor,x,y,gravity);
        startShowAnimator();
    }

    @Override
    public void showAtLocation(View parent, int gravity, int x, int y) {
        super.showAtLocation(parent, gravity, x, y);
        startShowAnimator();
    }

    /**
     * 初始化动画
     */
    private void initShowAnimation(){
        if(mPlaceViewShowAnimation==null){
            //占位控件的出场动画和退场动画
            mPlaceViewShowAnimation = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
            mPlaceViewShowAnimation.setInterpolator(new AccelerateInterpolator  ());
            mPlaceViewShowAnimation.setDuration(300);
            mPlaceViewDismissAnimation = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
            mPlaceViewDismissAnimation.setInterpolator(new AccelerateInterpolator  ());
            mPlaceViewDismissAnimation.setDuration(300);
            mPlaceViewDismissAnimation.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    mConvertView.setVisibility(View.GONE);
                    GalleryDirPopupWindow.super.dismiss();
                    isDismissing = false;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    mConvertView.setVisibility(View.GONE);
                    GalleryDirPopupWindow.super.dismiss();
                    isDismissing = false;
                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            measureView(lv_dirs);
            //ListView的出场动画和退场动画
            mListViewShowAnimation = ObjectAnimator.ofFloat(lv_dirs, "translationY", lv_dirs.getMeasuredHeight(), 0f);
            mListViewShowAnimation.setDuration(300);
            mListViewShowAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
            mListViewDismissAnimation =ObjectAnimator.ofFloat(lv_dirs, "translationY", 0f, lv_dirs.getMeasuredHeight());
            mListViewDismissAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
            mListViewDismissAnimation.setDuration(200);
        }
    }
    /**
     * 目前该方法只支持预计算宽高设置为准确值或wrap_content的情况,
     * 不支持match_parent的情况,因为view的父view还未预计算出宽高
     * @param v 要预计算的view
     */
    private void measureView(View v) {
        ViewGroup.LayoutParams lp = v.getLayoutParams();
        if (lp == null) {
            return;
        }
        int width;
        int height;
        if (lp.width > 0) {
            // xml文件中设置了该view的准确宽度值,例如android:layout_width="150dp"
            width = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
        } else {
            // xml文件中使用wrap_content设定该view宽度,例如android:layout_width="wrap_content"
            width = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        }

        if (lp.height > 0) {
            // xml文件中设置了该view的准确高度值,例如android:layout_height="50dp"
            height = View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY);
        } else {
            // xml文件中使用wrap_content设定该view高度,例如android:layout_height="wrap_content"
            height = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        }

        v.measure(width, height);
    }
    /**
     * 启动动画,设置Visibility为VISIABLE
     */
    private void startShowAnimator(){
        if(animatorSet!=null && animatorSet.isRunning()){
            animatorSet.cancel();
        }
        animatorSet = new AnimatorSet();
        animatorSet.play(mListViewShowAnimation).with(mPlaceViewShowAnimation);
        animatorSet.start();
        mConvertView.setVisibility(View.VISIBLE);
    }

    /**
     * 退场动画
     */
    private void startDismissAnimator(){
        if(animatorSet!=null && animatorSet.isRunning()){
            animatorSet.cancel();
        }
        animatorSet = new AnimatorSet();
        animatorSet.play(mListViewDismissAnimation).with(mPlaceViewDismissAnimation);
        animatorSet.start();
    }
    class GalleryDirsAdapter extends BaseAdapter {
        private List<PhotoDir> mPhotoDirs;
        private Context mContext;
        public GalleryDirsAdapter(Context context,List<PhotoDir> photoDirs){
            mContext = context;
            mPhotoDirs = photoDirs;
        }
        @Override
        public int getCount() {
            return mPhotoDirs.size();
        }

        @Override
        public Object getItem(int position) {
            return mPhotoDirs.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            final PhotoDir item = mPhotoDirs.get(position);
            ImgViewHolder holder = null;
            if(convertView==null) {
                convertView = LayoutInflater.from(mContext).inflate(R.layout.gallery_list_dir_item, null);
                holder = new ImgViewHolder();
                holder.tv_dir_name = (TextView) convertView.findViewById(R.id.tv_dir_name);
                holder.iv_dir_image=(ImageView)convertView.findViewById(R.id.iv_dir_image);
                holder.tv_dir_count = (TextView) convertView.findViewById(R.id.tv_dir_count);
                convertView.setTag(holder);
            } else {
                holder=(ImgViewHolder) convertView.getTag();
                resetHolder(holder);
            }
            convertView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(position==0){
                        ((GalleryActivity)mContext).resetPhotos(PhotoFactory.getInstance().getPhotos(),position);
                    }else{
                        ((GalleryActivity)mContext).resetPhotos(item.getPhotos(),position);
                    }
                    dismiss();
                }
            });
            holder.tv_dir_name.setText(item.getName());
            Glide.with(mContext).load(item.getFirstPhotoPath()).override(200,200).into(holder.iv_dir_image);
            return convertView;
        }
        class ImgViewHolder{
            public TextView tv_dir_name;
            public ImageView iv_dir_image;
            public TextView tv_dir_count;
        }

        private void resetHolder(ImgViewHolder holder) {
            holder.tv_dir_name.setText("");
            holder.tv_dir_count.setText("");
        }
    }
}

popupwindow布局文件

<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#b0000000"
        />
    <ListView
        android:id="@+id/lv_dirs"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:layout_alignParentBottom="true"
        android:paddingTop="5dp"
        android:divider="#EEE3D9"
        android:dividerHeight="1px"
        android:background="#ffffff"
        />

</RelativeLayout>

外部调用

//设置的高度是屏幕高度减去状态栏高度,减去下方自定义的bottombar的高度,再减去toolbar的高度,实际跟gridview的高度一样
dirPopupWindow = new GalleryDirPopupWindow(this,photoDirs,gv_photos.getHeight());
        dirPopupWindow.showAsDropDown(findViewById(R.id.layout_bottom),0,-(dirPopupWindow.getHeight()+PixelUtil.dp2px(48)));
//        dirPopupWindow.showAtLocation(gv_photos,Gravity.NO_GRAVITY,0,toolbar.getHeight()+mStatusBarHeight);//toolbar的高度和通知栏高度

看完代码先说Popupwindow部分,popupwindow在使用的时候要注意的地方有两个,一个是一开始的参数的设置,这个网上介绍很多,不多说。第二个是showAsDropDown方法和showAtLocation方法的使用。

  • showAsDropDown
    这个方法的参数有四个,分别是anchor(popupwindow会显示在该view之下),xoff(x轴上的位移,正值往右偏,负值往左偏),yoff(y轴上的位移,正值往下偏,负值往上偏,跟屏幕的坐标系有关系),gravity(相对于anchor指定对齐)。默认的原点是anchor的左下角。
    注意:在android 7.0的系统上,当Popupwindow的宽高是match_parent的时候,则设为match_parent的轴边的偏移失效,全屏显示。所以在使用的时候应该为了兼容7.0的系统而计算Popupwindow的宽高。

  • showAtLocation
    这个方法的参数基本上可以参照showAsDropDown,区别是坐标原点是屏幕的坐标原点,即屏幕左上角,gravity也是相对于屏幕的。7.0上位移失效的问题同样存在,所以仍然建议计算宽高。

动画

为了实现多个子view同时开始动画,选用属性动画。动画方面比较简单,就是针对各个view编写动画,然后通过AnimatorSet来实现同时播放动画。
在使用过程中要注意的地方有两个,一个是view的隐藏,在隐藏view时,需要把隐藏的动作放到动画播放之后,不然会直接隐藏,而不展现动画效果。还有一个是设置动画的参数值,一定要确保参数有效,在上面代码中,listview的动画用到了listview的高度,所以必须保证在设置动画的参数之前,将listview的高度计算出来。

最后

这个图库还没有完善,要笔记的东西也有一些,这里只是先把一部分写出来,源码链接还是给出来,想看的同学可以看看,项目名称是Rxjava2Demo,里面的gallery部分。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值