用 DialogFragment 模仿国际微博评论框 返回首页

发表于 2017-08-11

啥也不说了,先上图

国际微博评论框 模仿评论框
International Weibo comment box.gif Imitation of international Weibo comment box.gif

分析

  1. 从图中,可以看出,微博详情页底下的一个评论条其实并不做输入框使用,而是充当一个 Button 的作用,唤起真正的输入框。

  2. 真正的输入框,从效果来看,像是 Dialog,这里选用了自定义布局的 DialogFragment。

  3. 输入框 Dismiss 或者 Cancel 时,若评论并未提交,则将其显示到底部的评论条中,单行显示,溢出省略。

  4. 再次进入输入框时,若评论条内容不为空,需将文本内容填充到输入框中。

  5. 评论框中无字符输入时,提交按钮为灰色且不可触发,反之为正常的提交按钮。

分析完了那就开工吧

1. 底部评论条

这个就比较简单了,一个 LinearLayout 包裹住头像控件和文本控件且置于屏幕底部。这里想要提出的一点:

Tip 1: 直接选用 TextView 而不是 EditText

在一开始的时候,我的第一直觉是认为那是一个 EditText,所以直接用的这个控件,设置为不可编辑,一切都看似那么的合乎情理。直到调试的时候,我发现在首次进入页面的时候,第一次点击评论条,并不能触发点击事件,需要第二次点击时才有效,而后就不会再出现问题。我试图去找过是什么原因,但未果,也不排除是否是机型或者什么其他原因,但倾向于是控件的事件分发哪里被截断了。总之,如果你能找到原因,可以一起交流交流。

之后转念一想,反正 TextView 也是可以设置 Hint 的,那为啥不直接用 TextView 呢,简单省事。

下面是布局代码:

<!-- activity_main.xml -->

<?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"
    android:background="@color/background_my_material_grey"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_alignParentBottom="true"
        android:background="@color/pureWhite"
        android:elevation="8dp">

        <de.hdodenhof.circleimageview.CircleImageView
            android:layout_width="35dp"
            android:layout_height="35dp"
            android:layout_margin="10dp"
            android:layout_gravity="center_vertical"
            android:src="@drawable/img_avatar"/>

        <TextView
            android:id="@+id/tv_comment_fake_button"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:hint="添加评论..."
            android:textColorHint="@color/textHint"
            android:textColor="@color/primary_text"
            android:gravity="center_vertical"
            android:maxLines="1"
            android:ellipsize="end"/>
    </LinearLayout>
</RelativeLayout>

2. DialogFragment 评论框

评论框布局也是比较简单的,一个无背景的输入框,一个提交按钮,以及左侧几个附加的功能按钮,比如选图,和 @ 功能,这个就直接上布局文件了:

<!-- dialog_fragment_comment_layout.xml -->

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="130dp"
              android:layout_gravity="bottom"
              android:background="@color/background_my_material_light">

    <EditText
        android:id="@+id/edit_comment"
        android:layout_width="match_parent"
        android:layout_height="75dp"
        android:background="@null"
        android:gravity="top"
        android:layout_margin="10dp"
        android:hint="添加评论..."
        android:textColorHint="@color/textHint"
        android:textSize="17sp"/>

    <ImageView
        android:id="@+id/image_btn_photo"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_below="@id/edit_comment"
        android:layout_marginStart="10dp"
        android:layout_marginBottom="10dp"
        android:src="@drawable/ic_photo"
        android:tint="@color/iconCoverDark"
        android:tintMode="multiply"/>

    <ImageView
        android:id="@+id/image_btn_at"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_below="@id/edit_comment"
        android:layout_toEndOf="@id/image_btn_photo"
        android:layout_marginStart="10dp"
        android:padding="2dp"
        android:src="@drawable/ic_at"
        android:tint="@color/iconCoverDark"
        android:tintMode="multiply"/>

    <ImageView
        android:id="@+id/image_btn_comment_send"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_below="@id/edit_comment"
        android:layout_alignParentEnd="true"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="10dp"
        android:src="@drawable/ic_send"
        android:tint="@color/iconCover"
        android:tintMode="multiply"/>
</RelativeLayout>

3. DialogFragment 逻辑代码

关于 DialogFragment 用法,网上搜一下就有很多了。首先必须要实现的是 onCreateView() 或者是 onCreateDialog() 方法,这里比较核心的应该就是布局的位置了,因为 DialogFragment 默认布局是常见的 Dialog 样式,这里需要手动设置:

// CommentDialogFragment.java

public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{

    private Dialog mDialog;
    private EditText commentEditText;
    private ImageView photoButton;
    private ImageView atButton;
    private ImageView sendButton;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        // 自定义 style BottomDialog
        mDialog = new Dialog(getActivity(), R.style.BottomDialog);

        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);

        // 外部点击设置为可以取消
        mDialog.setCanceledOnTouchOutside(true);

        Window window = mDialog.getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();

        // 布局属性位于整个窗口底部
        layoutParams.gravity = Gravity.BOTTOM;

        // 布局属性宽度填充满整个窗口宽度,默认是有 margin 值的
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        window.setAttributes(layoutParams);

        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);

        photoButton.setOnClickListener(this);
        atButton.setOnClickListener(this);
        sendButton.setOnClickListener(this);

        return mDialog;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.image_btn_photo:
                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_at:
                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_comment_send:
                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
                dismiss();
                break;
            default:
                break;
        }
    }
}

自定义的 BottomDialog style:

<!-- styles.xml -->

<resources>

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        ...
    </style>

    <style name="BottomDialog" parent="@style/AppTheme">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:windowIsFloating">true</item>
    </style>
</resources>

4. MainActivity 逻辑代码

此时只需要为评论条添加监听器,响应 DialogFragment 的呼出即可:

// MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private TextView commentFakeButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        commentFakeButton = (TextView) findViewById(R.id.tv_comment_fake_button);
        commentFakeButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.tv_comment_fake_button:
                CommentDialogFragment commentDialogFragment = new CommentDialogFragment();
                commentDialogFragment.show(getFragmentManager(), "CommentDialogFragment");
                break;
            default:
                break;
        }
    }
}

此时,运行程序,大体上是没问题了,接下来就需要实现它的一些小 Feature,一个是评论文本内容填充,一个是评论框文本输入监听,控制评论提交按钮的状态。

5. 评论文本内容填充

这里第一反应可能是普通 Activity 与 Fragment 间的通信,之后在网上看文章时,学习到回调这种用法,也是第一次比较深入地去理解回调这门艺术,不得不说,回调真是个好东西。接下来我们就来看看怎么用回调来实现这一个 Feature。

首先需要定义一个接口:

// DialogFragmentDataCallback.java

public interface DialogFragmentDataCallback {

    String getCommentText();

    void setCommentText(String commentTextTemp);
}

代码很简单,这里比较好的理解方式是,一个类实现这个接口,那么就必须重写这两个方法,而 get 方法,可以看作是提供给外部类调用,用来获取这个类的一些数据,而 set 方法,可看作是外部类用来回调操作此类的一个暴露的方法。那接下来就让 MainActivity 实现这个接口:

// MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener, DialogFragmentDataCallback{

    private TextView commentFakeButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        commentFakeButton = (TextView) findViewById(R.id.tv_comment_fake_button);
        commentFakeButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.tv_comment_fake_button:
                CommentDialogFragment commentDialogFragment = new CommentDialogFragment();
                commentDialogFragment.show(getFragmentManager(), "CommentDialogFragment");
                break;
            default:
                break;
        }
    }

    @Override
    public String getCommentText() {
        // 提供给外部类获取本类中数据的方法,这里就是用于获取评论条的文本,将其返回给外部类就可以
        return commentFakeButton.getText().toString();
    }

    @Override
    public void setCommentText(String commentTextTemp) {
        // 提供给外部类用来回调设置评论条文本内容的方法
        commentFakeButton.setText(commentTextTemp);
    }
}

接着在 CommentDialogFragment 中实现文本的填充:

// CommentDialogFragment.java

public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{

    private Dialog mDialog;
    private EditText commentEditText;
    private ImageView photoButton;
    private ImageView atButton;
    private ImageView sendButton;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        mDialog = new Dialog(getActivity(), R.style.BottomDialog);
        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
        mDialog.setCanceledOnTouchOutside(true);

        Window window = mDialog.getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.gravity = Gravity.BOTTOM;
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        window.setAttributes(layoutParams);

        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);

        // 填充文本方法
        fillEditText();

        photoButton.setOnClickListener(this);
        atButton.setOnClickListener(this);
        sendButton.setOnClickListener(this);

        return mDialog;
    }

    private void fillEditText() {

        // 获取 MainActivity 实例并转型为 DialogFragmentDataCallback 接口
        dataCallback = (DialogFragmentDataCallback) getActivity();
        // 回调获取评论条文本内容并填充至输入框中
        commentEditText.setText(dataCallback.getCommentText());
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.image_btn_photo:
                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_at:
                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_comment_send:
                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();

                // 提交成功后 (根据具体需求一般为网络请求发送评论) 清空评论框,重写 onDismiss,使评论框文本内容填充回评论条,这里提交后就设为空
                commentEditText.setText("");
                dismiss();
                break;
            default:
                break;
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog) {

        // 重写 onDismiss,将评论框文本填充回评论条
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onDismiss(dialog);
    }

    @Override
    public void onCancel(DialogInterface dialog) {

        // 重写 onCancel,将评论框文本填充回评论条,一般是点击外部及返回键所触发
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onCancel(dialog);
    }
}

6. 评论框文本输入监听,控制评论提交按钮的状态

这个算是本文比较有趣的一部分,也是自己在实现这个项目效果中碰到坑最多的地方了,让我细细说来。首先是 EditText 的输入监听,使用的是 TextWatcher,当输入的文本字符大于 0 时,将 sendButton 禁用,设置颜色为灰色。

Tip 2: 在刚进入输入框,未执行动作前,TextWatcher 是无法监听此时状态的

因为刚开始进入输入框时,在还未执行输入动作之前,TextWatcher 无法监听此时的状态,所有方法都得不到调用。所以,在填充文本时,就应该要做一个判断,因为 ImageView 充当 Button 默认设置的是可点击的,所以在文本为空时,需要将发送按钮禁用。

Tip 3: ImageView 充当 Button,clickable 属性设置无效,应使用 enable 属性

在一开始时,我是试图用 clickable 属性来控制这个 ImageView 假 Button 的点击事件,但却发现这个属性完全失效不起作用。不是说 ImageView 默认是不可点击的吗?那我先在 xml 里设置 clickable 属性为 false 也是没用的,后来经大佬提点,才知道是监听器的原因。为 ImageView 设置 OnClickListener 时,会自动将其属性 clickable 设置为 true,这点请看源码:

// View.java

/**
 * Register a callback to be invoked when this view is clicked. If this view is not
 * clickable, it becomes clickable.
 *
 * @param l The callback that will run
 *
 * @see #setClickable(boolean)
 */
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

既然如此,那就在代码中设置其 enable 属性,将其完全禁用:

// CommentDialogFragment.java

public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{

    private Dialog mDialog;
    private EditText commentEditText;
    private ImageView photoButton;
    private ImageView atButton;
    private ImageView sendButton;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        mDialog = new Dialog(getActivity(), R.style.BottomDialog);
        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
        mDialog.setCanceledOnTouchOutside(true);

        Window window = mDialog.getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.gravity = Gravity.BOTTOM;
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        window.setAttributes(layoutParams);

        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);

        fillEditText();

        // 为 EditText 设置监听器
        commentEditText.addTextChangedListener(mTextWatcher);

        photoButton.setOnClickListener(this);
        atButton.setOnClickListener(this);
        sendButton.setOnClickListener(this);

        return mDialog;
    }

    private void fillEditText() {
        dataCallback = (DialogFragmentDataCallback) getActivity();
        commentEditText.setText(dataCallback.getCommentText());

        // 若评论条文本内容为空,禁用按钮
        if (dataCallback.getCommentText().length() == 0) {
            sendButton.setEnabled(false);
            sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
        }
    }

    // TextWatcher,主要重写 afterTextChanged 方法
    private TextWatcher mTextWatcher = new TextWatcher() {

        private CharSequence temp;

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            temp = s;
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable s) {
            // 输入动作触发后,将进行监听,若文本被清空,禁用按钮,反之,则恢复正常
            if (temp.length() > 0) {
                sendButton.setEnabled(true);
                sendButton.setClickable(true);
                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));
            } else {
                sendButton.setEnabled(false);
                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
            }
        }
    };

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.image_btn_photo:
                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_at:
                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_comment_send:
                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
                commentEditText.setText("");
                dismiss();
                break;
            default:
                break;
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onDismiss(dialog);
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onCancel(dialog);
    }
}

7. 在弹出评论框时呼出软键盘

关于软键盘的呼出问题,也是一个比较头疼的问题,自己也碰到了比较多坑,网上的解决方案比较多,也比较多没有用 :) 。文末有推荐几篇比较优秀的文章可供参考。

Tip 4: 在给 EditText 获取焦点 (requestFocus() 方法) 之前,需设置 setFocusable 和 setFocusableInTouchMode 为 true

高中毕业,买了第一部手机,Meizu MX4,今天我才发现真的是买对了。且不说 Flyme 基于 bug,就 MX4 这款变态的占屏比,真可谓是 Android 开发人员必备的调试机。

haixiu.jpg

第一个要提出的点是:在填充评论条文本内容到评论框后,需要调用 EditText 的 setSelection 方法设置光标位置,不然魅族默认没有光标,也不会有软键盘弹出,我不知道是不是只有我的手机会这样,反正用另一部华为手机就没有这个问题。(这里有点奇怪的是,为什么国际微博评论框弹出时,光标是置于文本头的,感觉这里很不人性化)

buxing.jpg

第二个是软键盘的弹出,之前就是怎么都弹不出来,之后在一篇文章中看到说设置延时是很关键的一步,我试过之后,发现确实如此,可是所有文章都未提到为什么一定要设置延时,唯一说到一点的是说软键盘的加载需要一点时间,当然,我仍然不知道这里为何,这个理由完全不是需要延时的原因啊。延时时间是我手动调试过去的,但发现偶尔还是弹不出来,这里实在是不解,而且不同手机又怎么保证延时时间是正确的?如果你知道这个问题的答案,可以一起交流一下。下面是 CommentDialogFragment 完整的代码:

// CommentDialogFragment.java

public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{

    private Dialog mDialog;
    private EditText commentEditText;
    private ImageView photoButton;
    private ImageView atButton;
    private ImageView sendButton;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        mDialog = new Dialog(getActivity(), R.style.BottomDialog);
        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
        mDialog.setCanceledOnTouchOutside(true);

        Window window = mDialog.getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.gravity = Gravity.BOTTOM;
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        window.setAttributes(layoutParams);

        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);

        fillEditText();

        // 呼出软键盘方法
        setSoftKeyboard();

        commentEditText.addTextChangedListener(mTextWatcher);
        photoButton.setOnClickListener(this);
        atButton.setOnClickListener(this);
        sendButton.setOnClickListener(this);

        return mDialog;
    }

    private void fillEditText() {
        dataCallback = (DialogFragmentDataCallback) getActivity();
        commentEditText.setText(dataCallback.getCommentText());

        // Write this line for meizu 特别的爱,给特别的你
        commentEditText.setSelection(dataCallback.getCommentText().length());

        if (dataCallback.getCommentText().length() == 0) {
            sendButton.setEnabled(false);
            sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
        }
    }

    // !!! 2017-12-22 updated
    @Deprecated
    private void setSoftKeyboard() {

        // 为 EditText 获取焦点
        commentEditText.setFocusable(true);
        commentEditText.setFocusableInTouchMode(true);
        commentEditText.requestFocus();

        // TODO: 17-8-11 为何这里要延时才能弹出软键盘, 延时时长又如何判断?目前是手动调试
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
                inputMethodManager.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
            }
        }, 110);
    }

    private TextWatcher mTextWatcher = new TextWatcher() {

        private CharSequence temp;

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            temp = s;
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable s) {
            if (temp.length() > 0) {
                sendButton.setEnabled(true);
                sendButton.setClickable(true);
                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));
            } else {
                sendButton.setEnabled(false);
                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
            }
        }
    };

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.image_btn_photo:
                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_at:
                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_comment_send:
                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
                commentEditText.setText("");
                dismiss();
                break;
            default:
                break;
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onDismiss(dialog);
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onCancel(dialog);
    }
}

!!! 2017/12/22 更新

关于软键盘弹出问题,已解决,在此感谢 limuyang2!

在之前把文章投稿到一位前辈公众号时,评论里就有人提出应该是绘制问题,那时候没有去深究了。这次又把官方文档看了一遍,包括 Dialog 和 DialogFragment。继承自 Dialog,FragmentDialog 生命周期类似,当我们使用自定义 Dialog 时,重写的是 onCreateDialog() 方法,此时就不需要重写 onCreateView() 方法,且 onCreateDialog() 会在 onCreateView() 之前,onCreate() 之后调用,此时大致的生命周期各方法调用顺序如下:

onAttach(Activity) ->
onCreate(Bundle) ->
onCreateDialog(Bundle) ->
onCreateView(LayoutInflater, ViewGroup, Bundle) ->
onActivityCreated(Bundle) ->
onViewStateRestored(Bundle) ->
onStart() ->
onResume()

此时,我们调起键盘是在 onCreateDialog() 方法中,此时,DialogFragment 肯定是没有绘制完的,即使是在 onStart() 也只是刚开始绘制,具体的结束时间这里并不知道。这也是为什么设置延时能调起键盘的原因,延时时间,对于性能好一点的手机,可以短一些,但是肯定是需要照顾一下一些性能差的手机,那么,必定需要将延时时间设置在一个相对大的值,这样的结果就是,在性能好的手机上,会出现键盘弹出「迟钝」的现象。作为一个精致男孩,这肯定是不能忍的。那就必须设置监听器,监听全局视图的绘制。代码如下:

private void setSoftKeyboard() {
    commentEditText.setFocusable(true);
    commentEditText.setFocusableInTouchMode(true);
    commentEditText.requestFocus();

    // 为 commentEditText 设置监听器,在 DialogFragment 绘制完后立即呼出软键盘,呼出成功后即注销
    commentEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
            if (inputMethodManager.showSoftInput(commentEditText, 0)) {
                commentEditText.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        }
    });
}

在键盘呼出成功后,需要注销监听器,不然会一直被触发,造成的后果就是,无论按下返回键还是软键盘中的收起键,键盘收起时又立马被唤起。这里还有一点是,之前调起键盘使用的是 toggleSoftInput() 方法,这个方法的意思是切换软键盘,如果当前没有调起软键盘,就调起,如果调起了软键盘,就收起。如果这里仍旧使用这个方法,你会发现,在第一次唤起软键盘之后,就一直收起和唤出不停地切换,效果还是很惊艳的,你可以试试 :p 。至于原因,前面已经讲过了,addOnGlobalLayoutListener() 监听的是视图的变化,软键盘的呼出和收起都会引起视图的变化从而触发监听器。

在搜索过程中还发现,软键盘还是有点复杂的,尤其是涉及到如何监听软键盘的状态,看到的方法都是测量 View 的高度,判断软键盘的大概高度,然后推算是不是软键盘弹出。以后有时间可以再深入研究一下,看看能不能写篇就关于软键盘部分的文章 :p

这是更新之后的 CommentDialogFragment 完整代码:

// CommentDialogFragment.java

public class CommentDialogFragment extends DialogFragment implements View.OnClickListener{

    private Dialog mDialog;
    private EditText commentEditText;
    private ImageView photoButton;
    private ImageView atButton;
    private ImageView sendButton;
    private InputMethodManager inputMethodManager;
    private DialogFragmentDataCallback dataCallback;

    @Override
    public void onAttach(Context context) {
        if (!(getActivity() instanceof DialogFragmentDataCallback)) {
            throw new IllegalStateException("DialogFragment 所在的 activity 必须实现 DialogFragmentDataCallback 接口");
        }
        super.onAttach(context);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        mDialog = new Dialog(getActivity(), R.style.BottomDialog);
        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mDialog.setContentView(R.layout.dialog_fragment_comment_layout);
        mDialog.setCanceledOnTouchOutside(true);

        Window window = mDialog.getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.gravity = Gravity.BOTTOM;
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        window.setAttributes(layoutParams);

        commentEditText = (EditText) mDialog.findViewById(R.id.edit_comment);
        photoButton = (ImageView) mDialog.findViewById(R.id.image_btn_photo);
        atButton = (ImageView) mDialog.findViewById(R.id.image_btn_at);
        sendButton = (ImageView) mDialog.findViewById(R.id.image_btn_comment_send);

        fillEditText();
        setSoftKeyboard();

        commentEditText.addTextChangedListener(mTextWatcher);
        photoButton.setOnClickListener(this);
        atButton.setOnClickListener(this);

        sendButton.setOnClickListener(this);

        return mDialog;
    }

    private void fillEditText() {
        dataCallback = (DialogFragmentDataCallback) getActivity();
        commentEditText.setText(dataCallback.getCommentText());
        commentEditText.setSelection(dataCallback.getCommentText().length());
        if (dataCallback.getCommentText().length() == 0) {
            sendButton.setEnabled(false);
            sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
        }
    }

    private void setSoftKeyboard() {
        commentEditText.setFocusable(true);
        commentEditText.setFocusableInTouchMode(true);
        commentEditText.requestFocus();

        // 为 commentEditText 设置监听器,在 DialogFragment 绘制完后立即呼出软键盘,呼出成功后即注销
        commentEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
                if (inputMethodManager.showSoftInput(commentEditText, 0)) {
                    commentEditText.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            }
        });
    }

    private TextWatcher mTextWatcher = new TextWatcher() {

        private CharSequence temp;

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            temp = s;
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable s) {
            if (temp.length() > 0) {
                sendButton.setEnabled(true);
                sendButton.setClickable(true);
                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.colorAccent));
            } else {
                sendButton.setEnabled(false);
                sendButton.setColorFilter(ContextCompat.getColor(getActivity(), R.color.iconCover));
            }
        }
    };

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.image_btn_photo:
                Toast.makeText(getActivity(), "Pick photo Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_at:
                Toast.makeText(getActivity(), "Pick people you want to at Activity", Toast.LENGTH_SHORT).show();
                break;
            case R.id.image_btn_comment_send:
                Toast.makeText(getActivity(), commentEditText.getText().toString(), Toast.LENGTH_SHORT).show();
                commentEditText.setText("");
                dismiss();
                break;
            default:
                break;
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onDismiss(dialog);
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        dataCallback.setCommentText(commentEditText.getText().toString());
        super.onCancel(dialog);
    }
}

写在最后

本文项目完整代码已提交到 GitHub 上,如有需要,请自取。PureComment 项目链接

本文到这里差不多就结束了,感谢阅读。关于字数限制和输入字符数量提示,这个在 TextWatcher 里监听就可以实现了,因为自己项目中评论并没有字数限制,所以这里没有写这个部分,不过相信讲到这里,自己去实现也是很简单的啦!可参考下面给出的参考文档。

最后,如果你有更好的实现方法或者什么建议,欢迎联系,一起讨论交流。Thank you and have a nice day!

扩展阅读,参考文档

Android 官方推荐 : DialogFragment 创建对话框

DialogFragment 实现底部弹窗

DialogFragment 生命周期简介

知乎:回调函数(callback)是什么?

onTextChanged 参数解释及实现 EditText 字数监听

Android 软键盘隐藏寻找最优解

DialogFragment - Android developer

Fragment - Android developer

ViewTreeObserver - Android developer