1. 程式人生 > >仿微信相簿資料夾選擇的互動

仿微信相簿資料夾選擇的互動

前言

這是按照微信相簿中資料夾選擇的展示動畫做出來的PopupWindow。之前自己做過相簿部分,發現微信相簿中這個資料夾顯示和消失的動畫很自然,很舒服,而之前做的不是那麼自然,於是就做了這麼個東西。

效果

資料夾選擇效果

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部分。