项目需求讨论-仿ios底部弹框实现及分析

hi,在项目开发中,有时候需要仿照ios的底部弹框做效果,比如我们在iPhone上面关闭定位的时候,就会弹出ios特有的底部弹框:

屏幕快照 2017-10-09 08.20.30 PM.png

弹框布局:

我们可以来看下这个弹框有哪些显示:

  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);
  1. 我们可以改变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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,287评论 25 708
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,241评论 4 61
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,523评论 0 17
  • 无关于阴晴圆缺无关于风华雪月无关于海誓山盟无关于生命终结 没有附着没有着落没有可以关于的任何 心里还是不爽缠绕缠绕...
    郭安安阅读 636评论 6 14
  • 刚开始写文 阅读量上不去 虽然阅读量这东西 并不重要 然而 这样的写作方式 实在有趣 随性而行 文章好坏与否 不在...
    戒指花歌阅读 116评论 0 0