一、改造目的

前几天我照着欧阳燊先生编著的《Android App开发入门与项目实战》一书的第10章 “自定义控件”的内容做了一个自定义月度选择器,不过书中的源码是用的Activity,使得每次调用这个选择器都是全屏显示,而我希望是弹窗式的,网上搜索了一下,发现Activity是可以改成弹窗式的,这样就不用再写个Dialog或者PopupWindow了。

文章链接: Android 应用开发学习-自定义月份选择器

android 禁止弹出toast_android 禁止弹出toast

(改造前,选择器占据了整个屏幕)

二、照着别人的教程做

我在网上搜了一下,找到了一篇《android Activity实现从底部弹出或滑出选择菜单或窗口》的文章,这篇文章很多网站都有,我就贴一个有最终效果图的链接:

不过最终从底部弹出的效果我没有做出来,这里先记录一下我改造自定义月份选择器的大致过程。

首先是对已有的MonthPickerActivity.java和activity_month_picker.xml两个文件进行修改(原始文件代码见改造目的中提供的链接)。

activity_month_picker.xml文件,给根布局的LinearLayout添加一个id:

android:id="@+id/pop_layout"

这个id的作用是对月份选择器的窗口添加监听器的,点击选择器窗口内,会用Toast给出"提示:点击窗口外部关闭窗口!"。

另外还需要重写onTouchEvent方法,实现点击选择器之外的区域,销毁选择器的Activity。这些修改在MonthPickerActivity.java文件中进行,我只给出添加的一些代码,完整代码后面附上。

private LinearLayout layout;  // 声明一个线性布局

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_month_picker);
        
        layout = findViewById(R.id.pop_layout);
        
        //添加选择窗口范围监听可以优先获取触点,点击窗口内区域给出提示
        layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MonthPickerActivity.this, "提示:点击窗口外部关闭窗口!", Toast.LENGTH_SHORT).show();
            }
        });
    }


    // 实现onTouchEvent触屏函数,点击选择器之外的区域时销毁本Activity
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        finish();
        return true;
    }

修改了上面的两个文件后,接着按照《android Activity实现从底部弹出或滑出选择菜单或窗口》一文,添加其它的文件,原文的第四步道第六步的顺序有问题,应该反过来。调整后的顺序如下:

第四步、在res文件夹下创建anim文件夹,在anim文件夹下分别创建push_bottom_in.xml文件和push_bottom_out.xml文件

push_bottom_in.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- 上下滑入式 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="200"
        android:fromYDelta="100%p"
        android:toYDelta="0"
        />

</set>

push_bottom_out.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- 上下滑入式 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="200"
        android:fromYDelta="0"
        android:toYDelta="50%p"
        />

</set>

第五步、打开res/values文件夹下的styles.xml文件,没有就自己创建,在该文件内添加以下内容:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- style下的name自己设定的,不必一样,下面的style会用到 -->
    <style name="AnimBottom" parent="@android:style/Animation">
        <!-- 这两个文件就是第四步创建的 -->
        <item name="android:windowEnterAnimation">@anim/push_bottom_in</item>
        <item name="android:windowExitAnimation">@anim/push_bottom_out</item>
    </style>


    <!-- 这个style必须要继承自Theme.Dialog -->
    <style name="DialogStyleBottom" parent="android:style/Theme.Dialog">
        <!-- Dialog的动画类型 -->
        <item name="android:windowAnimationStyle">@style/AnimBottom</item>
        <!-- Dialog的windowFrame框为无 -->
        <item name="android:windowFrame">@null</item>
        <!-- 是否浮现在activity之上 -->
        <item name="android:windowIsFloating">true</item>
        <!-- 是否半透明 -->
        <item name="android:windowIsTranslucent">true</item>
        <!-- 是否显示title -->
        <item name="android:windowNoTitle">true</item>
        <!-- 背景透明 -->
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

</resources>

第六部,在AndroidManifest.xml文件中找到这个自定义月份选择器的对应的<Activity> ,在其下添加以下代码:

android:theme="@style/DialogStyleBottom"

这个style就是第五步创建的。

三、问题与解决

弄完这些后,进行调试,程序正常运行,但是在打开月份选择器(调用MonthPickerActivity.java)时,出错了,错误提示:

You need to use a Theme.AppCompat theme (or descendant) with this activity.

网上搜了一下,与使用的Theme有关,我将AndroidManifest.xml中添加的那段代码去掉后,再运行,一切正常,这就有些懵了。我有会同把原文看了一遍,发现了一处不同,原文中的第二步创建的类时继承的Activity类,而我的MonthPickerActivity.java默认继承的AppCpmpatActivity类。

我将AppCpmpatActivity改成Activity后在测试,这次月份选择器打开了,不过这风格是怎么回事!样式和之前不同就算了,怎么还显示不全呀,心里万马奔腾。

android 禁止弹出toast_android_02

我先尝试对activity_month_picker.xml文件进行调试,将布局器或者按钮的宽度指定为一个固定值(android:layout_width="240dp"),然后再运行,这下宽度正常了。

android 禁止弹出toast_android 禁止弹出toast_03

不过这个样式还是看着不习惯,我又一阵搜索,找到了下面这篇文章

android datepicker自定义样式,DatePickerDialog 自定义样式及使用全解

受这篇文章的启发,我尝试在activity_month_picker.xml文件中给我自定义的月份选择器设置主题,代码如下:

<com.bahamutj.easyinventory.widget.MonthPicker
        android:id="@+id/mp_month"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:calendarViewShown="false"
        android:theme="@style/Theme.AppCompat.Light"
        android:datePickerMode="spinner"
        android:gravity="center" />

上述代码中的android:theme="@style/Theme.AppCompat.Light"就是添加的内容,至于选哪个主题,我也不确定,当时看了一下软件提供的选项,决定这个应该可以就试着选了它。测试一下,OK,但从底部弹出实现不了,我把背景样式也从上面两个圆角的样式改成了四个角都是圆角的样式了,最终效果如下:

android 禁止弹出toast_android 禁止弹出toast_04

至此,我的月份选择器改造完成。感觉安卓版本的升级会导致一些以前适用的方法在新版本里会出问题,网上的一些资料很多都是转载,转载者自己可能都没弄清楚,弄不好就要采坑。

四、java和xml文件代码

前面把style的代码贴出来了,这里再把改造后的月份选择器的代码也贴出来吧:

activity_month_picker.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pop_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:layout_alignParentBottom="true"
    android:orientation="vertical"
    android:background="@drawable/shape_round_bg_white"
    android:padding="10dp">

    <!-- 自定义的月份选择器,需要使用全路径 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:text="@string/selectMonth"
        android:textColor="@color/black"
        android:textSize="17sp" />

    <com.bahamutj.easyinventory.widget.MonthPicker
        android:id="@+id/mp_month"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:calendarViewShown="false"
        android:theme="@style/Theme.AppCompat.Light"
        android:datePickerMode="spinner"
        android:gravity="center" />

    <Button
        android:id="@+id/btn_confirm"
        android:layout_width="240dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dp"
        android:layout_gravity="center_horizontal"
        android:gravity="center_horizontal"
        android:text="@string/confirm"
        android:textColor="@color/black"
        android:backgroundTint="@color/blue_367"
        android:textSize="17sp" />

</LinearLayout>

MonthPickerActivity.java

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.bahamutj.easyinventory.R;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class MonthPickerActivity extends Activity implements View.OnClickListener {

    private final static String TAG = "MonthPickerActivity";
    private MonthPicker mp_month; // 声明一个月份选择器对象
    private LinearLayout layout;  // 声明一个线性布局

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_month_picker);

        // 获取传入的月份(格式为 yyyy-MM 的字符串)
        Bundle bundle = getIntent().getExtras();
        String month = bundle.getString("month");
        String[] ym = month.split("-");
        int year = Integer.parseInt(ym[0]);
        int monthOfYear = Integer.parseInt(ym[1]) -1;  // 因为1月份对应的monthOfYear是0,因此要减1

        // 从布局文件中获取名叫mp_month的月份选择器
        mp_month = findViewById(R.id.mp_month);

        // 设置日期选择器的初始日期
        mp_month.init(year, monthOfYear, 1, null);
        findViewById(R.id.btn_confirm).setOnClickListener(this);

        layout = findViewById(R.id.pop_layout);
        layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MonthPickerActivity.this, "提示:点击窗口外部关闭窗口!", Toast.LENGTH_SHORT).show();
            }
        });
    }

    // 实现onTouchEvent触屏函数,点击屏幕时销毁本Activity
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        finish();
        return true;
    }


    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_confirm) {
            String ym = String.format(Locale.CHINESE, "%d-%d",mp_month.getYear(),mp_month.getMonth() + 1);
            // 将yyyy-M转换yyyy-MM
            // 创建解析格式和格式化格式
            @SuppressLint("SimpleDateFormat") SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-M");
            @SuppressLint("SimpleDateFormat") SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM");
            // 解析输入的日期字符串
            Date date = null;
            try {
                date = inputFormat.parse(ym);
            } catch (ParseException e) {
                e.printStackTrace();
            }

            // 格式化日期对象为输出的日期字符串
            assert date != null;
            String month = outputFormat.format(date);
            //Log.d(TAG, "选择的月份是:" + month);
            // 回传数据 参见Android App 开发入门与项目实战 P91
            Intent intent = new Intent();
            Bundle bundle = new Bundle();
            bundle.putString("month", month);
            intent.putExtras(bundle);
            setResult(Activity.RESULT_OK, intent);
            finish();
        }
    }

}

还有一个MonthPicker.java文件没有改动,源代码见我的上一篇日志《  Android 应用开发学习-自定义月份选择器》