專案需求討論-仿ios底部彈框實現及分析

青蛙要fly發表於2017-10-09

hi,在專案開發中,有時候需要仿照ios的底部彈框做效果,比如我們在iPhone上面關閉定位的時候,就會彈出ios特有的底部彈框:

彈框佈局:

我們可以來看下這個彈框有哪些顯示:

  1. 標題(一個標題)
  2. 選項(N個選項,此處圖片只有關閉這一個選項)
  3. 底部一個取消按鈕(一個取消按鈕)

所以我們先考慮這個彈框的佈局就需要:


因為中間的選單是一個列表,所以根據這個圖我們可以想到我們所要寫的彈框的佈局大致為:

<LinearLayout>
    <LinearLayout>
        <TextView/>  <!--標題-->
        <RecyclerView/>  <!--選單列表(或者ListView)-->
    </LinearLayout>
    <Button/> <!--取消按鈕-->
</LinearLayout>複製程式碼

我們已經規劃好了彈框的佈局,現在我們要開始實現彈框了。


實現彈框:

因為後來谷歌推薦使用的是DialogFragment,所以我們此處彈框也是使用DialogFragment。

我們一步步來看如何使用DialogFragment來實現我們想要的彈框:

我們按照上面的佈局寫了具體的彈框佈局程式碼
fragment_ios_dialog.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tool="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@android:color/transparent"
    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/circle_bg"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="20dp"
            android:text="標題內容" />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#bbbbbb" />

        <ListView
            android:id="@+id/lv_menu"
            android:scrollbars="none"
            android:layout_width="match_parent"
            android:layout_height="200dp" />

    </LinearLayout>

    <Button
        android:id="@+id/btn_cancle"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="20dp"
        android:background="@drawable/circle_bg"
        android:text="取消"
        />
</LinearLayout>複製程式碼

在這裡,我們先假設中間的選單ListView的高度寫成50dp,主要是先來看效果,實際使用的時候可以寫成wrap_content,根據傳入的item數量決定高度。

再繼承DialogFragment來實現我們的IOSDialogFragment:
IOSDialogFragment.java:

public class IOSDialogFragment extends DialogFragment {

    private View rootView;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        rootView = inflater.inflate(R.layout.fragment_ios_dialog, container, false);
        return rootView;
    }
}複製程式碼

我們就是單純的引入我們寫的佈局,不做其他處理,我們執行後發現介面效果如下圖所示:

  1. 標題內容的上方有一塊區域
  2. 我們彈框佈局的底部的背景色預設是灰色

我們針對這二個先做處理:

  1. 其實我們上方的一塊區域是彈框的標題,
    我們在IOSDialogFragment中新增:
    @Override
    public void onStart() {
     super.onStart();
     getDialog().setTitle("我是標題");
    }複製程式碼
    我們再看下彈框的效果:

    我們可以看到標題頭了。所以我們要去掉上面一塊區域,只需要把彈框預設的標題頭給去掉即可,只需要新增:
    getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);複製程式碼
  2. 我們可以改變DecorView的背景色,設定它的背景色為透明即可:
    View decorView = getDialog().getWindow().getDecorView();
    decorView.setBackground(new ColorDrawable(Color.TRANSPARENT));複製程式碼
    (PS:Window -> DecorView -> FrameLayout -> FrameLayout -> 我們的自定義View) 這個邏輯大家應該都知道的,所以我們只需要改變底部的DecorView的背景色即可。

經過上面二步的修改,我們可以看到了效果變成了這樣:

那接下去如何讓彈框變成在底部呢??????
我們知道最後我們的View是在window下面的,我們只需要讓window的Grivaty屬性是Bottom,這樣,裡面的元素都是居於底部即可。

Window window = getDialog().getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.BOTTOM;
window.setAttributes(layoutParams);複製程式碼

我們再看下效果:

的確是在底部了,但是這時候可能有人會有疑問,我們這個自定義View的佈局fragment_ios_dialog.xml裡面,明明layout_widthmatch_parent,可是左右二邊是間隙的,


這時候比如我想要按照自己的專案要求調整二邊的間隙豈不是單純的在自己的fragment_ios_dialog.xml就無法實現了。

我們就來看看到底是為什麼二邊有間隙,然後再來看如何自己處理:
我們知道我們的View都是被包含在window裡面,雖然我們的自己的View的寬度已經設定成了match_parent,但是我們並沒有對window設定寬度為最大。所以我們先來改變window的寬度。

改變window的寬度

Window window = getDialog().getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.BOTTOM;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);複製程式碼

我們在前面修改彈框位置的程式碼處,多新增一句:

layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;複製程式碼


我們發現,果然二邊的間隙變小了很多。但是還是有間隙,既然我們都已經把window的寬度變為match_parent,還是沒填充,說明應該是有padding值。那我們馬上就想到了,難道是DecorView裡面有padding值。畢竟我們的View也是被包含在DecorView裡面。廢話不多說,我們馬上實驗:

decorView.setPadding(0,0,0,0);複製程式碼

然後我們再看效果,果不其然:

PS:這裡還有另外一種方法,不寫這句decorView.setPadding(0,0,0,0);而是直接設定window的背景顏色,window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));原始碼中其實也給DecorView設定了padding值。所以效果相同。


彈框從下而上顯示:

我們看過ios的彈框效果,是從底部從下而上升起,然後消失的時候也是從上而下消失。所以消失的時候我們不能單純的讓DialogFragment執行dismiss(),而是先讓彈框執行下移的動畫效果,然後再dismiss()

既然談到了上下的移動,大家肯定馬上想到了用TranslateAnimation動畫來做,我們就一步步來看如何用這個來實現:

  • 彈框出現動畫:

    Animation slide = new TranslateAnimation(
          Animation.RELATIVE_TO_SELF, 0.0f,
          Animation.RELATIVE_TO_SELF, 0.0f, 
          Animation.RELATIVE_TO_SELF, 1.0f, 
          Animation.RELATIVE_TO_SELF, 0.0f
    );
    slide.setDuration(400);
    slide.setFillAfter(true);
    slide.setFillEnabled(true);
    view.startAnimation(slide);複製程式碼

    我們來看TranslateAnimation,這裡我們傳了八個引數,一般大家用到的是隻傳四個引數:
    TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)
    也就是從座標(fromXDelta,fromYDelta)(toXDelta,toYDelta)
    我們可以點進去這個建構函式檢視:

    public TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) {
          mFromXValue = fromXDelta;
          mToXValue = toXDelta;
          mFromYValue = fromYDelta;
          mToYValue = toYDelta;
    
          mFromXType = ABSOLUTE;
          mToXType = ABSOLUTE;
          mFromYType = ABSOLUTE;
          mToYType = ABSOLUTE;
      }複製程式碼

    之所以我們以前用的只傳了四個引數,是因為他給我們把另外四個引數以及賦了預設值,也就是ABSOLUTE。我們繼續看有哪幾種可以選擇:

      /**
       * The specified dimension is an absolute number of pixels.
       */
      public static final int ABSOLUTE = 0;
    
      /**
       * The specified dimension holds a float and should be multiplied by the
       * height or width of the object being animated.
       */
      public static final int RELATIVE_TO_SELF = 1;
    
      /**
       * The specified dimension holds a float and should be multiplied by the
       * height or width of the parent of the object being animated.
       */
      public static final int RELATIVE_TO_PARENT = 2;複製程式碼

    通過字面意思我們也能理解:
    ABSOLUTE是絕對座標,RELATIVE_TO_SELF是相對於自身,RELATIVE_TO_PARENT是相對於父View。
    而我們只需要我們的彈框顯示的位置,讓的起始位置如下圖所示:



    剛開始超過螢幕,並且高度為彈框自身的高度,然後再回到原始位置,所以我們就用:

    Animation slide = new TranslateAnimation(
          Animation.RELATIVE_TO_SELF, 0.0f,
          Animation.RELATIVE_TO_SELF, 0.0f, 
          Animation.RELATIVE_TO_SELF, 1.0f, 
          Animation.RELATIVE_TO_SELF, 0.0f
    );複製程式碼

    從原來的位置,增加了自身高度的距離為起始點,開始移動,然後再回到原來的位置。

  • 消失動畫:
    只要跟上面反過來就可以了。同時這裡我們要額外增加監聽動畫結束事件,因為我們讓彈框往下移動結束後,要讓這個彈框dismiss掉:

    Animation slide = new TranslateAnimation(
          Animation.RELATIVE_TO_SELF, 0.0f,
          Animation.RELATIVE_TO_SELF, 0.0f, 
          Animation.RELATIVE_TO_SELF, 0.0f, 
          Animation.RELATIVE_TO_SELF, 1.0f
    );
    slide.setAnimationListener(new Animation.AnimationListener() {
      @Override
      public void onAnimationStart(Animation animation) {}
    
      @Override
      public void onAnimationEnd(Animation animation) {
          IOSDialogFragment.this.dismiss();
      }
    
      @Override
      public void onAnimationRepeat(Animation animation) {}
    });複製程式碼

所以我們的動畫的程式碼總結下就是:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
    getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
    rootView = inflater.inflate(R.layout.fragment_ios_dialog, container, false);
    slideToUp(rootView);
    return rootView;
}


public void slideToUp(View view){
    Animation slide = new TranslateAnimation(
        Animation.RELATIVE_TO_SELF, 0.0f,
        Animation.RELATIVE_TO_SELF, 0.0f, 
        Animation.RELATIVE_TO_SELF,1.0f, Animation.RELATIVE_TO_SELF, 0.0f);

        slide.setDuration(400);
        slide.setFillEnabled(true);
        slide.setFillAfter(true);
        view.startAnimation(slide);
    }

    public void slideToDown(View view){
        Animation slide = new TranslateAnimation(
            Animation.RELATIVE_TO_SELF, 0.0f,
            Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF,0.0f, Animation.RELATIVE_TO_SELF, 1.0f);

        slide.setDuration(400);
        slide.setFillEnabled(true);
        slide.setFillAfter(true);
        view.startAnimation(slide);

        slide.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                IOSDialogFragment.this.dismiss();//彈框消失
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }複製程式碼

彈框的點選事件:

相關的點選事件就很簡單了。只需要在onViewCreated中,通過findViewByid獲取View例項,然後設定點選事件即可。

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    TextView titleView = (TextView) view.findViewById(R.id.tv_title);
    titleView.setText("標題內容");

    ListView listView = (ListView) view.findViewById(R.id.lv_menu);
    listView.setAdapter(new ArrayAdapter(getActivity(),R.layout.menu_item,R.id.item_text,items));
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

                //點選執行相關的事件
                ......
                ......


        }
    });

    Button cancel = (Button) view.findViewById(R.id.cancel);
    cancel.setText(mCancel);
    cancel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //執行相關事件
            ........
        }
    });
}複製程式碼

具體的結束事件:

比如上面的cancel點選事件執行的肯定是彈框向下移動的動畫。所以我們可以自己寫個方法:

private boolean isAnimation = false;//用來判斷是否多次點選。防止多次執行
private void dialogfinish(){
    if (isAnimation) {
        return;
    }
    isAnimation = true;
    slideToDown(rootView);
}

public void slideToDown(View view){
      .....
      .....
    slide.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {}

        @Override
        public void onAnimationEnd(Animation animation) {
            isAnimation = false;//用來判斷是否多次點選。防止多次執行
            IOSDialogFragment.this.dismiss();//彈框消失
        }

        @Override
        public void onAnimationRepeat(Animation animation) {}
        });
    }
}複製程式碼

又或者不想再加新的方法,也可以直接複寫dismiss方法:

private boolean isAnimation = false;//用來判斷是否多次點選。防止多次執行

@Override
public void dismiss() {
    if (isAnimation) {
        return;
    }
    isAnimation = true;
    slideToDown(rootView)
}

//然後再更換IOSDialogFragment.this.dismiss() -> IOSDialogFragment.super.dismiss()

public void slideToDown(View view){
      .....
      .....
    slide.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {}

        @Override
        public void onAnimationEnd(Animation animation) {
            isAnimation = false;//用來判斷是否多次點選。防止多次執行
            IOSDialogFragment.super.dismiss();//彈框消失
        }

        @Override
        public void onAnimationRepeat(Animation animation) {}
        });
    }
}複製程式碼

點選空白讓彈框消失問題:

當點選上方一些空白處,我們會發現我們的彈框會直接消失,而不會像我們上面點選<取消>按鈕點選事件那樣,彈框先往下移動再消失,因為DialogFragment預設點選彈框外的時候,會直接dismiss,而不走我們的方法:

我們可以這麼解決,直接對DecorView設定onTouchListener:

window.getDecorView().setOnTouchListener(new View.OnTouchListener() {
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            //彈框消失的動畫執行相關程式碼
            ....
            ....

        }
        return true;
    }
});複製程式碼

這樣就會執行我們自己寫的彈框消失的相關事件的了。

最後結語

希望大家不要噴我,哈哈。如果哪裡寫錯了。可以下面評論回覆,謝謝大家了。O(∩_∩)O~

最後附上Demo

相關文章