关闭

DialogFragment源码分析及应用实战

标签: 对话框DialogFragment
569人阅读 评论(1) 收藏 举报
分类:
首先我们来看下要完成的效果:







从官文中我们发现DialogFragment继承自Fragment实现了对话框的OnCancelListener和OnDismissListener接口,最高可兼容到Api11

A fragment that displays a dialog window, floating on top of its activity's window. This fragment contains a Dialog object, which it displays as appropriate based on the fragment's state. 
他是一个对话框窗口,漂浮在活动窗口顶部,基于fragment片段 ,包括一个对话框对象,
Implementations should override this class and implement onCreateView(LayoutInflater, ViewGroup, Bundle) to supply the content of the dialog. Alternatively, they can override onCreateDialog(Bundle) to create an entirely custom dialog, such as an AlertDialog, with its own content.

使用时必须重写 onCreateView(LayoutInflater, ViewGroup, Bundle),来实现对话框内容,还可以通过  onCreateDialog(Bundle)来创建一个自定义的对话框如AlertDialog

DialogFragment needs to ensure that what is happening with the Fragment and Dialog states remains consistent

必须确保DialogFragment和Dialog的状态保持一致,可以通过 show(FragmentManager, String) 或show(FragmentTransaction, String)两种方式来实现DialogFragment的显示,通过FragmentManager和FragmentTransaction来跟踪它显示的状态

下面是DialogFragment的公有方法

Public methods

void

使Fragment及其dialog消失

void
这个方法由于版本的兼容性被废弃,使用的时候需要调用 FragmentTransaction.commitAllowingStateLoss()
void
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)

打印fragment的状态到流

Dialog getDialog()
boolean
返回setShowDialog(boolean false)时设置的值
int
获取fragment对话框主题
boolean
是否可点击,返回 setCancelable(boolean).时设置的值
void
onActivityCreated(Bundle savedInstanceState)
当DialogFragment依附的Activity被创建的时候调用,此时fragment的活动窗体被初始化
void
onAttach(Context context)
当DialogFragment首次被附上Acitivity的上下文时调用
void
当对话框消失的时候调用
void onCreate(Bundle savedInstanceState)

初始化DialogFragment的时候调用

Dialog
onCreateDialog(Bundle savedInstanceState)
当需要覆盖重写你自定义的对话框时调用
void onDestroyView()

移除你的对话框的时候调用

void
当你的DialogFragment不在依附于Activity窗体上时调用
void
对话框消失的时候调用
void
保存DialogFragment的状态在稍后启动即将创建新实例的时候调用
void
当DialogFragment对用户可见的时候调用
void
当DialogFragment不启用的时候调用
void
setCancelable(boolean cancelable)
控制已显示的对话框是否可以取消的时候调用
void
setShowsDialog(boolean showsDialog)
控制Fragment是否可以在对话框中显示
void setStyle(int style, int theme)

设置DialogFragment的风格和样式

void
show(FragmentManager manager, String tag)
显示对话框,将DialogFragment交给FragmentManager
int
show(FragmentTransaction transaction, String tag)
显示对话框,将DialogFragment添加到现有的事务,然后提交
DialogFragment源码分析:

public void show(FragmentManager manager, String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.add(this, tag);
    ft.commit();
}

public int show(FragmentTransaction transaction, String tag) {
    mDismissed = false;
    mShownByMe = true;
    transaction.add(this, tag);
    mViewDestroyed = false;
    mBackStackId = transaction.commit();
    return mBackStackId;
}

这两种显示方式都是通过tag的方式将DialogFragment以事务的形式提交,不同的是第二种方式是采用已经创建过的transaction,并且他返回了一个int类型的数值mBackStackId,mBackStackId是干什么用的呢?

mBackStackId:是做为将DialogFragment压入回退栈的编号,初始值是-1,
如果DialogFragment是用第二种方式show的话,他将被transaction默认压入回退栈,mBackStackId=transaction.commit(),此时他的回退栈编号大于0,他的具体使用在dismissInternal方法中后面会具体介绍


public void dismiss() {
    dismissInternal(false);
}


public void dismissAllowingStateLoss() {
    dismissInternal(true);
}

在源码中我们可以看到这两个方法都调用了 dismissInternal(boolean)方法,不同的是传入的boolean值一个为false一个为true,那么究竟这个boolean起到什么作用呢,我们
看看dismissInternal这个方法不就行了吗?

void dismissInternal(boolean allowStateLoss) {
   //如果对话框已经不可见就跳出方法体
    if (mDismissed) {
        return;
    }
   //设置对话框消失,不可见为true,可见为false
    mDismissed = true;
   //将对话框属性设置不可见
    mShownByMe = false;
   //如果DialogFragment中的Dialog对象不为空,就让其内的对话框消失
    if (mDialog != null) {
        mDialog.dismiss();
        mDialog = null;
    }
    //销毁View
    mViewDestroyed = true;
    //上面我们提到很重要的一个回退栈编号mBackStackId,他是用show(FragmentTransaction transaction, String tag)这个方法来压栈的,所以要取消对话框需要在这里面判断,已压栈的要弹出回退栈,这个回退栈是由Activity来管理的,如果是用show(FragmentManager manager, String tag)方式的话则不需要弹栈,只需要在FragmentTransaction中将其remove掉即可。
    if (mBackStackId >= 0) {
        getFragmentManager().popBackStack(mBackStackId,
                FragmentManager.POP_BACK_STACK_INCLUSIVE);
        mBackStackId = -1;
    } else {
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        ft.remove(this);
      //commitAllowingStateLoss()这个方法,查阅官文可以发现他是在我们Activity保存状态之后调用的,通常我们知道,如果在Activity的onSaveInstanceState调用之后再commit transaction会导致异常,Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState.这个时候我们就需要用commitAllowingStateLoss()方法了,这个方法最后会对Activity的状态进行检查,如果Activity已经保存状态,即调用onSaveInstanceState(),他就会忽略这个异常。   
        if (allowStateLoss) {
            ft.commitAllowingStateLoss();
        } else {
            ft.commit();
        }
    }
}

 DialogFragment其实就是一个容纳Dialog的Fragment,我们来看看DialogFragment的初始化方法:

public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
    if (!mShowsDialog) {
        return super.getLayoutInflater(savedInstanceState);
    }
    //对话框对象Dialog在页面初始化的时候被创建,通过onCreateDialog方法,这也就是说我们如果要自定义一个自己的对话框时,必须复写这个方法
    mDialog = onCreateDialog(savedInstanceState);
    switch (mStyle) {
        //不允许输入
        case STYLE_NO_INPUT:
            mDialog.getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
            // fall through...
        //设置无边框
        case STYLE_NO_FRAME:
        //设置无标题
        case STYLE_NO_TITLE:
            mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    }
    if (mDialog != null) {
        return (LayoutInflater)mDialog.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }
    return (LayoutInflater) mHost.getContext().getSystemService(
            Context.LAYOUT_INFLATER_SERVICE);
}

另外需要我们注意的地方就是setStyle方法,根据官文给出的实例,他只能在DialogFragment的onCreateView()方法之前调用,否则无效.还有
如果覆写了onCreateDialog()方法后,就不能覆写onCreateView()方法了

public void setStyle(int style, int theme) {
    mStyle = style;
    if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
        mTheme = com.android.internal.R.style.Theme_DeviceDefault_Dialog_NoFrame;
    }
    if (theme != 0) {
        mTheme = theme;
    }
}

下面我们来使用下,先看下效果图:




我结合了cardView,让DialogFragment样式能够稍微好看点,接下来我们用两种不同的方式创建DialogFragment,并用不同的方式show出来。

第一种方式:(直接继承DialogFragment)

首先需要引入cardView依赖:

compile 'com.android.support:appcompat-v7:23.2.0'
compile 'com.android.support:cardview-v7:23.2.0'

public class DialogFragmentView extends DialogFragment {
    private CardView cardView;
    private int number;
    private int screenWidth,screenHeigh;

    static DialogFragmentView newInstance(int number)
    {
        DialogFragmentView dialogFragmentView = new DialogFragmentView();
        Bundle bundle = new Bundle();
        bundle.putInt("number", number);
        dialogFragmentView.setArguments(bundle);
        return dialogFragmentView;
    }

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
    }

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        number = getArguments().getInt("number");
        //TODO 必须放在onCreateView之前否则不起作用,如果设置了,点击外部将不会消失
        setStyle(STYLE_NO_FRAME, 0);

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        View view = inflater.inflate(R.layout.activity_dialogfragment, container);
        cardView = (CardView) view.findViewById(R.id.cardView);
        //TODO  如果你是用了 AppCompat v7 支持库:
        //TODO  那么你可以直接给 CardView 加上 android:foreground="?attr/selectableItemBackground" 这个属性会在 Lollipop 上自动加上 Ripple 效果,在旧版本则是一个变深/变亮的效果
        //TODO 设置cardView的弧度
        cardView.setRadius(20);
        //TODO 设置cardView的阴影和Z轴位移
        cardView.setCardElevation(20);
        return view;
    }

}

然后在Acitivity中引用:

public class Main_DialogFragment extends Activity {
    Button button;
    private int mStackLevel=0;
    public void showDialog()
    {
        mStackLevel++;
        FragmentTransaction ft=getFragmentManager().beginTransaction();
        Fragment prev=getFragmentManager().findFragmentByTag("dialog");
        if(prev!=null)
        {
            ft.remove(prev);
        }
        DialogFragment dialog=DialogFragmentView.newInstance(mStackLevel);
        dialog.show(ft, "dialog");

    }
}

以上是我们创建并显示DialogFragment的第一种方式,接下来我们看下第二种自定义的方式,布局不变,只是创建方式和引用方式我都用了不同的方法:

第二种方式(自定义AlertDialog,重写onCreateDialog()方法):

public class CustomFragmentDialog extends DialogFragment {
    private CardView cardView;
    public static CustomFragmentDialog newInstance(int title)
    {
        CustomFragmentDialog customFragmentDialog=new CustomFragmentDialog();
        Bundle bundle=new Bundle();
        bundle.putInt("title", title);
        customFragmentDialog.setArguments(bundle);
        return customFragmentDialog;
    }

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
    }
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState)
    {
        int title=getArguments().getInt("title");
        //TODO 采用系统默认主题
        Dialog dialog=new Dialog(getActivity(),R.style.dialog);
        View view = LayoutInflater.from(getActivity()).inflate(R.layout.activity_dialogfragment, null);
        cardView = (CardView) view.findViewById(R.id.cardView);
        //TODO  如果你是用了 AppCompat v7 支持库:
        //TODO  那么你可以直接给 CardView 加上 android:foreground="?attr/selectableItemBackground" 这个属性会在 Lollipop 上自动加上 Ripple 效果,在旧版本则是一个变深/变亮的效果
        //TODO 设置cardView的弧度
        cardView.setRadius(20);
        //TODO 设置cardView的阴影和Z轴位移
        cardView.setCardElevation(20);
        dialog.getWindow().getAttributes().windowAnimations = R.style.CustomDialog;
        dialog.setContentView(view);
        return dialog;
    }
}

然后我们在Activity中用另一种方式show

public void showCustomDialog()
{
    DialogFragment dialog=CustomFragmentDialog.newInstance(2);
    dialog.show(getSupportFragmentManager(),"dialog");
}

DialogFragment嵌套ViewPager:

如何实现我们文章开篇那种DialogFragement中嵌套Viewpager,使我们的对话框更加灵活呢?很简单,我在代码中都做了注释,不过需要注意的是,
如果在DialogFragment中嵌套ViewPager的话,必须使用getChildFragmentManager(),而不能使用getSupportFragmentManager(),否则会报错 java.lang.IllegalStateException:No view found for id () for fragment。在Activity中show()的时候也必须用getChildFragmentManager()。

public class DialogFragmentView extends DialogFragment {
    private CardView cardView;
    private int number;
    private int screenWidth,screenHeigh;
    private List<android.support.v4.app.Fragment> fragmentList=new ArrayList<android.support.v4.app.Fragment>();
    //TODO 装载底部小圆点的布局
    private RelativeLayout bottom_viewpager_dots;
    private ViewPager viewPager;
    //TODO 底部圆点按钮
    private Button bt_dot;
    private Button pre_selected_dot;
    static DialogFragmentView newInstance(int number)
    {
        DialogFragmentView dialogFragmentView = new DialogFragmentView();
        Bundle bundle = new Bundle();
        bundle.putInt("number", number);
        dialogFragmentView.setArguments(bundle);
        return dialogFragmentView;
    }
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        number = getArguments().getInt("number");
        //TODO 必须放在onCreateView之前否则不起作用,如果设置了,点击外部将不会消失
        setStyle(STYLE_NO_FRAME, 0);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        View view = inflater.inflate(R.layout.activity_dialogfragment, container);
        cardView = (CardView) view.findViewById(R.id.cardView);
        bottom_viewpager_dots= (RelativeLayout) view.findViewById(R.id.bottom_viewpager_dots);
        viewPager= (ViewPager) view.findViewById(R.id.viewpager);
        //TODO  如果你是用了 AppCompat v7 支持库:
        //TODO  那么你可以直接给 CardView 加上 android:foreground="?attr/selectableItemBackground" 这个属性会在 Lollipop 上自动加上 Ripple 效果,在旧版本则是一个变深/变亮的效果
        //TODO 设置cardView的弧度
        cardView.setRadius(20);
        //TODO 设置cardView的阴影和Z轴位移
        cardView.setCardElevation(20);
        //TODO 将viewPager滑动切换要显示的fragment装载进集合
        initFragments();
        //TODO 设置viewPager的适配器
        initAdapter();
        //TODO 初始化底部圆点
        initBottomDots();
        //TODO 设置当viewPager滑动时底部圆点的选中变化
        setBottomDotsAdapter();
        return view;
    }

    //TODO 初始化底部小圆点
    private void initBottomDots()
    {
        //TODO 一共有多少页面就有多少圆点
        for(int i=0;i<fragmentList.size();i++)
        {
            bt_dot=new Button(getActivity());
            //TODO 设置圆点的大小和居中显示
            RelativeLayout.LayoutParams relative_Params=new RelativeLayout.LayoutParams(px2dip(getActivity(),26),px2dip(getActivity(),26));
            if(i>0)
            {
                //Todo 设置圆点之间的间隔距离
                relative_Params.setMargins(Dp2Px(getActivity(),19),0,0,0);
                bt_dot.setBackgroundResource(R.drawable.tip_normal);
            }else//TODO 如果是初加载,没有滑动之前,默认选中项是第一个圆点(第一个界面)
            {
                bt_dot.setBackgroundResource(R.drawable.tip_select);
            }
            bt_dot.setLayoutParams(relative_Params);
            bottom_viewpager_dots.addView(bt_dot);
            //TODO 在fragment中嵌套viewpager,那么其宿主Activity必须继承FragmentActivity或者AppCompatActivity
            new ViewPagerPagerAdapter(getChildFragmentManager(),fragmentList).notifyDataSetChanged();
        }
    }

   //TODO 设置当viewPager滑动时底部圆点的选中变化

    private void setBottomDotsAdapter()
    {
        //TODO 设置ViewPager滑动监听
        viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
            {}
            
            @Override
            public void onPageSelected(int position)
            {
                if(pre_selected_dot!=null)
                {
                    pre_selected_dot.setBackgroundResource(R.drawable.tip_normal);
                }else //TODO 即将滑动,改变初始默认选中的圆点状态(即第一个界面),使其由选中状态改为未选中状态
                {
                    bottom_viewpager_dots.getChildAt(0).setBackgroundResource(R.drawable.tip_normal);
                }
                Button current_dot=(Button)bottom_viewpager_dots.getChildAt(position);
                current_dot.setBackgroundResource(R.drawable.tip_select);
                pre_selected_dot=current_dot;
            }

            @Override
            public void onPageScrollStateChanged(int state)
            {}
        });
    }
  
   //TODO 将viewPager滑动切换要显示的fragment装载进集合

    private void initFragments()
    {
        fragmentList.add(new Fragment_One());
        fragmentList.add(new Fragment_Two());
    }

    //TODO 设置viewPager适配器,必须是getChildFragmentManager(),DialogFragment继承自v4包

    private void initAdapter()
    {
        ViewPagerPagerAdapter pagerAdapter=new ViewPagerPagerAdapter(getChildFragmentManager(),fragmentList);
        viewPager.setAdapter(pagerAdapter);
    }

    public int Dp2Px(Context context, float dp)
    {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dp * scale + 0.5f);
    }

    public static int px2dip(Context context, float pxValue)
    {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }
}

//TODO 设置viewPager切换页面的适配器
public class ViewPagerPagerAdapter extends FragmentPagerAdapter {

    private List<Fragment> fragments;
    public ViewPagerPagerAdapter(FragmentManager fm,List<Fragment> fragmentList)
    {
        super(fm);
        this.fragments=fragmentList;
    }

    @Override
    public Fragment getItem(int position)
    {
        return fragments.get(position);
    }

    @Override
    public int getCount()
    {
        return fragments.size();
    }
}

然后是我们ViewPager用来滑动的两个Fragment,我就不贴了,感兴趣的可以下载源码,末尾会给出链接。

DialogFragment添加动画:

如果单是一个DialogFragment的展示,相信大家都会觉得很生硬,如果加上动画,就牛粪插上花了!
可是呢,DialogFragment对动画的添加会有诸多限制,这里我写了常用的对话框入场动画,都是非常简单的,包括从左至右滑入,从底部滑入到屏幕中心,从屏幕中心放大等等出场方式,文末源码里面都有,这里展示下开篇的出场动画,虽然很丑。
设置动画的方式为:

//TODO 设置对话框弹出动画
getDialog().getWindow().getAttributes().windowAnimations = R.style.Top_Center_Dialog;

在style文件中定义:

<!--从屏幕顶部掉落的屏幕中间-->
  <style name="Top_Center_Dialog" parent="@android:style/Theme.Dialog">
      <item name="android:windowEnterAnimation">@anim/top_to_center_from</item>
      <item name="android:windowExitAnimation">@anim/scale_center_to</item>
  </style>

其次在anim文件夹下建立动画xml文件:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
   >
    <alpha android:fromAlpha="0"
           android:toAlpha="1"
           android:duration="2000"/>
    <!--第二个动画延迟300毫秒-->
    <rotate
        android:interpolator="@android:anim/bounce_interpolator"
        android:duration="1400"
        android:fromDegrees="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toDegrees="15"
        android:fillAfter="true"
        />
    <translate
        android:interpolator="@android:anim/bounce_interpolator"
        android:duration="1400"
        android:fromYDelta="-100%p"
        android:toYDelta="0%p"
        android:fillAfter="true"
        />

    <translate
        android:interpolator="@android:anim/bounce_interpolator"
        android:duration="800"
        android:startOffset="1400"
        android:fillAfter="true"
        android:fromYDelta="0%p"
        android:toYDelta="-250"

        />
    <set android:fillAfter="true"
        android:startOffset="800"
        android:interpolator="@android:anim/bounce_interpolator"
        >

    <translate

        android:interpolator="@android:anim/bounce_interpolator"
        android:duration="800"
        android:fillAfter="true"
        android:fromYDelta="0%"
        android:toYDelta="250"
        android:startOffset="800"
        />

        <rotate
            android:startOffset="1400"
            android:interpolator="@android:anim/bounce_interpolator"
            android:duration="800"
            android:fromDegrees="0"
            android:pivotX="50%"
            android:pivotY="50%"
            android:toDegrees="-15"
            android:fillAfter="true"
            />
        </set>
</set>

以上是对DialogFragment的解析和应用,东西不多,也不复杂,主要是为了实现一些片段式界面的效果,如果不想使用或者考虑到cardView的兼容性的话,可以使用drawable来自定义实现,程序员嘛,离不开交流和共享,欢迎大家到交流群里面分享一些惊天地泣鬼神的代码或者编程思想,贴下, 安卓代码艺术:128672634,如果你不仅仅是把安卓当作一份工作,而是饱含坚持和热爱!

4
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    文章分类