Android开发技巧——使用Dialog实现仿QQ的ActionSheet菜单

最近看到有人用Dialog来实现QQ的仿ActionSheet的自定义菜单,对于自己没实现过的一些控件,看着也想实现一下。于是动手了一下,发现也不难,和大家分享一下。

本文原创,转载请注明出处:http://blog.csdn.net/maosidiaoxian/article/details/46119197

在这里我也是用Dialog来实现,代码不多,这里说一下实现的过程。

菜单的布局文件

首先我们写先一下菜单的布局文件,很简单,一个ListView菜单再加一个取消的Button。

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

    <ListView
        android:id="@+id/menu_items"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:listSelector="@android:color/transparent"/>

    <Button
        android:id="@+id/menu_cancel"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        android:text="取消"/>
</LinearLayout>

在这里我们先是写一下最基本的布局文件,因为我急着想知道实现上的可行性,所以背景那些暂未修改。

继承Dialog实现自己的菜单

我们的对话框有几个特点,一是弹出的位置在底部,二是没有对话框的那些windowFrame层也没有标题和contentOverlay层,并且背景透明。

所以我们要先写一个Dialog的Style,继承自系统主题:

    <style name="ActionSheetDialog" parent="android:Theme.Dialog">
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowFrame">@null</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowNoTitle">true</item>
    </style>

接下来我们需要写一个类继承Dialog,来实现自己的弹出菜单。在构造方法中调用super(Context context, int theme)方法。并且我们尝试设置gravity,来使它显示在底部。

public class ActionSheet extends Dialog {
    private Button mCancel;
    private ListView mMenuItems;
    private ArrayAdapter<String> mAdapter;

    public ActionSheet(Context context) {
        super(context, R.style.ActionSheetDialog);
        getWindow().setGravity(Gravity.BOTTOM);
        initView(context);
    }

    private void initView(Context context) {
        View rootView = View.inflate(context, R.layout.dialog_action_sheet, null);
        mCancel = (Button) rootView.findViewById(R.id.menu_cancel);
        mMenuItems = (ListView) rootView.findViewById(R.id.menu_items);
        mAdapter = new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1);
        mMenuItems.setAdapter(mAdapter);
        this.setContentView(rootView);
    }

    public ActionSheet addMenuItem(String items) {
        mAdapter.add(items);
        return this;
    }

    public void toggle() {
        if (isShowing()) {
            dismiss();
        } else {
            show();
        }
    }

    @Override
    public void show() {
        mAdapter.notifyDataSetChanged();
        super.show();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            dismiss();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}

在该类当中,我们需要拦截MENU键,处理按下MENU时菜单消失。

写一个Activity来验证可行性

然后写我们的Activity,来显示我们的Dialog,看是否如我们所想。

public class MainActivity extends ActionBarActivity {
    private ActionSheet mActionSheet;

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

    /**
     * 创建MENU
     */
    public boolean onCreateOptionsMenu(Menu menu) {
        menu.add("menu").setVisible(false);// 必须创建一项,设为false之后ActionBar上不会出现菜单按钮。
        return super.onCreateOptionsMenu(menu);
    }

    /**
     * 拦截MENU事件,显示自己的菜单
     */
    @Override
    public boolean onMenuOpened(int featureId, Menu menu) {
        if (mActionSheet == null) {
            mActionSheet = new ActionSheet(this);
            mActionSheet.addMenuItem("Test1").addMenuItem("Test2");
        }
        mActionSheet.show();
        return true;
    }

}

需要注意的是,要显示我们自己的菜单,只重写Activity的onKeyDown在那里显示是实现不了的。需要继承自onCreateOptionsMenu方法并添加一项菜单,然后才可以在onMenuOpened当中显示。

网上传的方法是说在onCreateOptionsMenu添加一项,然后在onMenuOpened中弹出我们的菜单并返回true。但是这样写有一个问题,就是在ActionBar的右边还是会有一个菜单键。在各种尝试中,我发现了一个很简单的解决此问题的方法。就是在onCreateOptionsMenu中添加了一项菜单之后,设为不可见。接下来在onMenuOpened弹出菜单之后,返回true和false都可以,都不会显示系统原来的菜单了。

修改我们的菜单

上面的代码跑起来,主要的效果确实如我们所想,所以接下来我们就需要对菜单的外观进行大的修改,来让它更像是QQ的菜单。

背景

首先,是菜单背景。菜单的背景共有四种,分别是在四个角中,仅上面圆角,仅下面圆角,都为圆角,都不为圆角。其次,背景都是半透明的。所以我们先在colors.xml中定义背景的颜色,包括正常状态时的颜色及按下去状态的颜色。

    <color name="menu_item_normal">#c9ffffff</color>
    <color name="menu_item_pressed">#d5dadada</color>

接着在drawable里编写这四个背景的selector。

menu_iten_top.xml,仅上面是圆角的背景。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <corners android:topLeftRadius="@dimen/list_corner"
                     android:topRightRadius="@dimen/list_corner"/>
            <solid android:color="@color/menu_item_pressed"/>
        </shape>
    </item>
    <item>
        <shape>
            <corners android:topLeftRadius="@dimen/list_corner"
                     android:topRightRadius="@dimen/list_corner"/>
            <solid android:color="@color/menu_item_normal"/>
        </shape>
    </item>
</selector>

menu_item_middle.xml,都不为圆角:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <solid android:color="@color/menu_item_pressed"/>
        </shape>
    </item>
    <item>
        <shape >
            <solid android:color="@color/menu_item_normal"/>
        </shape>
    </item>
</selector>

menu_item_bottom.xml,仅下面是圆角:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <corners android:bottomLeftRadius="@dimen/list_corner"
                     android:bottomRightRadius="@dimen/list_corner"/>
            <solid android:color="@color/menu_item_pressed"/>
        </shape>
    </item>
    <item>
        <shape>
            <corners android:bottomLeftRadius="@dimen/list_corner"
                     android:bottomRightRadius="@dimen/list_corner"/>
            <solid android:color="@color/menu_item_normal"/>
        </shape>
    </item>
</selector>

menu_item_single.xml,均为圆角:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <corners android:radius="@dimen/list_corner"/>
            <solid android:color="@color/menu_item_pressed"/>
        </shape>
    </item>
    <item>
        <shape>
            <corners android:radius="@dimen/list_corner"/>
            <solid android:color="@color/menu_item_normal"/>
        </shape>
    </item>
</selector>

其中取消按扭使用的是均为圆角的背景,所以回到菜单的布局文件中,对其修改。并且把ListView的listSelector设为透明,添加分割线,改完如下:

  <ListView
        android:id="@+id/menu_items"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="#c9dddddd"
        android:dividerHeight="1px"
        android:listSelector="@android:color/transparent"/>

    <Button
        android:id="@+id/menu_cancel"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        android:background="@drawable/menu_item_single"
        android:text="取消"
        android:textColor="@color/menu_text"/>

接着修改ListView的每一项的背景,我们需要重写我们的Adapter,设置背景。在此之前,先定ListView的item的布局文件:

menu_item.xml

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@android:id/text1"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:textSize="18sp"
          android:textColor="@color/menu_text"
          android:gravity="center"
          android:minHeight="45dp" />

定义了文字颜色为蓝色:

    <color name="menu_text">#f12162ff</color>

同时设置取消按钮的文字也是这个颜色。

重写Adapter,代码如下:

mAdapter = new ArrayAdapter<String>(context, R.layout.menu_item) {
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = super.getView(position, convertView, parent);
        setBackground(position, view);
        return view;
    }

    private void setBackground(int position, View view) {
        int count = getCount();
        if (count == 1) {
            view.setBackgroundResource(R.drawable.menu_item_single);
        } else if (position == 0) {
            view.setBackgroundResource(R.drawable.menu_item_top);
        } else if (position == count - 1) {
            view.setBackgroundResource(R.drawable.menu_item_bottom);
        } else {
            view.setBackgroundResource(R.drawable.menu_item_middle);
        }
    }
};

写完之后,给Activity的下面加点文字,看看背景透明度是否如我们的所想。这下子看起来很像了。但还是觉得有所欠缺,没错,我们还缺少动画。

动画

编写两个动画,一个是显示时弹出的,一个是消失的。

弹出动画:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
           android:fromYDelta="100%"
           android:toYDelta="0"
           android:duration="350">
</translate>

消失动画:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
           android:fromYDelta="0%"
           android:toYDelta="100%"
           android:duration="350">
</translate>

然后回到ActionSheet类,把我们的rootView重构为成员变量 ,因为我们的动画要加在它身上。同时,需要定义几个成员变量,分别是显示和消失的动画以及表示正在消失的boolean变量。

    private View mRootView;

    private Animation mShowAnim;
    private Animation mDismissAnim;

    private boolean isDismissing;

然后是初始化动画变量,重写show和dismiss方法,加入播放动画的代码。注意,对父类的dismiss调用是在弹出动画结束之后才调用的,所以加入一个isDismissing表示这段过程,并添加一个私有方法dismissMe来调用父类的dismiss方法。代码如下:

    private void initAnim(Context context) {
        mShowAnim = AnimationUtils.loadAnimation(context, R.anim.translate_up);
        mDismissAnim = AnimationUtils.loadAnimation(context, R.anim.translate_down);
        mDismissAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                dismissMe();
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

这是初始化动画的代码,该方法在initView中调用。然后是显示和隐藏菜单的代码:

    @Override
    public void show() {
        mAdapter.notifyDataSetChanged();
        super.show();
        mRootView.startAnimation(mShowAnim);
    }

    @Override
    public void dismiss() {
        if(isDismissing) {
            return;
        }
        isDismissing = true;
        mRootView.startAnimation(mDismissAnim);
    }

    private void dismissMe() {
        super.dismiss();
        isDismissing = false;
    }

加上动画之后,更逼真了些吧。但我们还漏了一个很重要的东西 。事件!

事件

首先,在ActionSheet里面定义一个接口:

    interface MenuListener {
        void onItemSelected(int position, String item);

        void onCancel();
    }

添加MenuListener变量。

    private MenuListener mMenuListener;

    public MenuListener getMenuListener() {
        return mMenuListener;
    }

    public void setMenuListener(MenuListener menuListener) {
        mMenuListener = menuListener;
    }

各种事件回调:

        //取消按钮的事件
        mCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cancel();
            }
        });
        // 菜单的事件
        mMenuItems.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (mMenuListener != null) {
                    mMenuListener.onItemSelected(position, mAdapter.getItem(position));
                    dismiss();
                }
            }
        });
        // 对话框取消的回调
        setOnCancelListener(new OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                if(mMenuListener != null) {
                    mMenuListener.onCancel();
                }
            }
        });

这下就基本完成了。

运行

然后再对我们的Activity略加修改,加入事件回调。

            mActionSheet.setMenuListener(new ActionSheet.MenuListener() {
                @Override
                public void onItemSelected(int position, String item) {
                    Toast.makeText(MainActivity.this, item, Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onCancel() {
                    Toast.makeText(MainActivity.this, "onCancel", Toast.LENGTH_SHORT).show();
                }
            });

运行,效果如下(由于我是通过Android Studio屏幕录制先录成MP4再在线转换为GIF的,GIF有些大,所以我就不贴图了):http://v.youku.com/v_show/id_XOTY2MTM0ODM2.html

这篇博客由于主要是写实现的过程,所以有点长。实际上代码并不复杂,ActionSheet的全部代码加注释才170行。

项目地址(包括运行效果的录制视频):http://zdz.la/2pz0Ys

下一篇将写一下如何把它写成一个可复用的控件。

参考博客:http://blog.csdn.net/bbld_/article/details/39124097

上一篇:Java与算法之(13) - 二叉搜索树


下一篇:javaweb常见问题解决