之前在框架封装中提到了有关Notification的处理,觉得很有必要详细整理一下封装的思路。
Notification是一个Android中经常使用的组件了,但是原生API提供的功能并不能够很好的满足商业应用开发时的需求,初始化也比较繁琐,个人认为在开发时有必要在现有API的基础上做一下封装,目的有二,其一是集合应用中针对Notification所需的定制功能,其二是提升项目的代码整洁性和模块化程度。
本文只贴出我认为重要的代码:完整源码以及可演示的demo请查看 QuickDevFramework。
// source code from NotificationWrapperPRivate static int defaultIconRes = R.drawable.ic_notify_default;public static void setDefaultIconRes(@DrawableRes int resId) { defaultIconRes = resId;}public static int getDefaultIconRes() { return defaultIconRes;}//source code from SimpleNotificationBuilderprivate Context mContext;private NotificationCompat.Builder mBuilder;/** * 构造方法 * <p>此处将Notification的icon设定为只用NotificationWrapper中所配置的默认icon</p> * */public SimpleNotificationBuilder(Context context, @NonNull String contentTitle, @NonNull String contentText) { this(context, NotificationWrapper.getDefaultIconRes(), contentTitle, contentText);}/** * 构造方法 * <p>icon,contentTitle,contentText为构建一个Notification的必须参数, * 如果不传递这三个参数,代码不会报错,但通知不会显示</p> * */public SimpleNotificationBuilder(Context context, @DrawableRes int icon, @NonNull String contentTitle, @NonNull String contentText) { this.mContext = context; mBuilder = new NotificationCompat.Builder(context); mBuilder.setSmallIcon(icon) .setContentTitle(contentTitle) .setContentText(contentText);}1. Notification常见样式的封装
出去上面只包含最基本元素的Notification外,Notification有多种样式,常见的有如下三种,BigText、BigPicture、Inbox,分别是大文本,大图,多行这三种样式分别对应NotificationCompat下属的BigTextStyle, BigPictureStyle和InboxStyle,在配置好Style之后通过NotificationCompat.Builder.setStyle()方法设置样式生效。可以看到上述三种样式其实也是比较简单的,完全可以在封装中简化配置过程,将样式配置归一化处理
//source code from simpleNotificationBuilderpublic SimpleNotificationBuilder setBigText(String title, String contentText, String summaryText) { NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); bigTextStyle.setBigContentTitle(title); bigTextStyle.bigText(contentText); if (!TextUtils.isEmpty(summaryText)) { bigTextStyle.setSummaryText(summaryText); } setStyle(bigTextStyle); return this;}public SimpleNotificationBuilder setBigPicture(String title, Bitmap picture, String summaryText) { NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); bigPictureStyle.setBigContentTitle(title); bigPictureStyle.bigPicture(picture); if (!TextUtils.isEmpty(summaryText)) { bigPictureStyle.setSummaryText(summaryText); } setStyle(bigPictureStyle); return this;}public SimpleNotificationBuilder setInboxMessages(String title, String summaryText, List<String> lines) { NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); inboxStyle.setBigContentTitle(title); if (!TextUtils.isEmpty(summaryText)) { inboxStyle.setSummaryText(summaryText); } for (String line : lines) { inboxStyle.addLine(line); } setStyle(inboxStyle); return this;}2. Notification点击操作的封装
Notification点击产生的操作时通过PendingIntent来设定的,PendingIntent可以看做是Intent的一个包装,使用它的目的就是为了在App未启动的状态下,也可以通过PendingIntent里面的Context执行Intent,从而达到点击通知时无论应用是否启动都能做出操作的目的。直接使用PendingIntent来打开App中的页面是可以的,但是所能做到的事情就是直接通过Intent唤起一个组件而已,如果想退回上一级页面目前网上很多博客都在说TaskStackBuilder、ParentStack,但这个方法只是形式上的指定“这个页面的上一级页面是什么”,两个页面可以完全没有关系,在实际操作中也不是先启动上一级页面再进入目标页面,而是在当前页面退出后如果存在Parent就打开Parent,与我们在进行商业应用开发中所需的逻辑相去甚远。仔细把玩一下目前的主流应用可以看到Notification的处理有如下逻辑:收到一条通知后,如果应用处于启动状态(无论是处于前台还是后台),点击通知时直接打开目标页面,不破坏当前的Activity栈;如果应用处于未启动状态,则先启动应用,然后打开目标页面。如果想要实现这样的功能单靠PendingIntent就不行了,这个功能大概有两个重点:1. 如何判断应用是否启动,2. 在什么时机和地方去判断应用是否启动并执行相应操作。这两个问题需要结合在一起看,因为判断的时机不同判断的方法也可能不同。使用PendingIntent可以做到唤起Activity之类的组件,那么在什么地方和时机去执行判断操作这个问题就找到了突破口:让PendingIntent启动一个Activity或者BroadcastReceiver,在其中进行判断,第一个原因的考虑,我选择了使用BroadcastReceiver,下面叙述。对于应用是否启动的判断,思路有如下:判断进程是否存在获取应用当前Activity的数量判断是否存在启动的前台页面。根据这个思路我找到了判断进程是否存在的方法/** * 判断应用是否已经启动 * * @param context 一个context * @param packageName 要判断应用的包名 * @return boolean */public static boolean isProcessRunning(Context context, String packageName) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> processInfo = activityManager.getRunningAppProcesses(); for (int i = 0; i < processInfo.size(); i++) { if (processInfo.get(i).processName.equals(packageName)) { return true; } } return false;}使用这个方法可以检查一个进程是否正在运行,而且不需要多余的权限,是比较好的手段,但存在一个问题就是如果检查函数本身就处于被检查的进程中,那么自己检查自己肯定是运行状态。这时候,要么检查activity数量,要么将检查组件置于不同进程中。出于从简处理的目的我自己的实现方案没有选择使用不同进程,而是采取通过ActivityLifeCycle来维护一个Activity计数。以此来判断应用是否正在运行,此时可以明显看到使用BroadcastReceiver比Activity要合适。这个检测方法顺便也解决了判断应用前后台的问题 :)public atract class Baseapplication extends Application { /** * 用于判断一个app是否处于前台 */ private static int mForegroundCount = 0; /** * 应用内Activity的数量,如果数量为0,则可以判断当前应用未启动 * <p>如果有什么一像素Activity等东西存在请另改值</p> * */ private static int mActivityCount = 0; @Override public void onCreate() { super.onCreate(); this.registerActivityLifecycleCallbacks(new FrameworkActivityLifeCycleCallback()); } …… /** * 检查应用主进程是否正在运行 * */ public static boolean isAppRunning() { Context mContext = getAppContext(); String packageName = mContext.getPackageName(); ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> processInfo = activityManager.getRunningAppProcesses(); for(int i = 0; i < processInfo.size(); i++){ if(processInfo.get(i).processName.equals(packageName)){ //如果有没被销毁的Activity,则App至少处于后台正在运行,否则App应处于未运行状态 return mActivityCount > 0; } } return false; } /** * 检查App是否处于前台 * */ public static boolean isAppForeground() { return isAppRunning() && mForegroundCount > 0; } private class FrameworkActivityLifeCycleCallback implements ActivityLifecycleCallbacks { @Override public void onActivityCreated(Activity activity, Bundle bundle) { mActivityCount++; } @Override public void onActivityStarted(Activity activity) { mForegroundCount++; } @Override public void onActivityResumed(Activity activity) {} @Override public void onActivityPaused(Activity activity) {} @Override public void onActivityStopped(Activity activity) { mForegroundCount--; } @Override public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {} @Override public void onActivityDestroyed(Activity activity) { mActivityCount--; } }}负责跳转的Intent封装Intent broadcastIntent = new Intent(mContext, NotificationReceiver.class);Bundle bundle = new Bundle();if (targetActivityIntent.getExtras() != null) { bundle.putAll(targetActivityIntent.getExtras());}bundle.putString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME, targetActivityIntent.getComponent().getClassName());broadcastIntent.putExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA, bundle);PendingIntent mPendingIntent = PendingIntent.getBroadcast(mContext, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);负责判断逻辑处理的BroadcastReceiverpublic abspublic class NotificationReceiver extends BroadcastReceiver { private static final String TAG = NotificationReceiver.class.getSimpleName(); @Override public void onReceive(Context context, Intent intent) { Bundle notificationExtra = intent.getBundleExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA); if (notificationExtra == null) { return; } if (BaseApplication.isAppRunning()) { Intent targetIntent = new Intent(); String targetKey = notificationExtra.getString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME); if (TextUtils.isEmpty(targetKey)) { targetKey = NotificationResumeActivity.class.getName(); } try { Class<?> targetActivityClass = Class.forName(targetKey); targetIntent.setClass(context, targetActivityClass); targetIntent.putExtras(notificationExtra); } catch (ClassNotFoundException e) { targetIntent.setClass(context, NotificationResumeActivity.class); } targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(targetIntent); } else { Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage( BaseApplication.getAppContext().getPackageName()); launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); launchIntent.putExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA, notificationExtra); context.startActivity(launchIntent); } }}在应用启动/初始化Activity中的处理@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); notificationExtra = intent.getBundleExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA); isHandleNotification = notificationExtra != null;}@Overrideprotected void onDestroy() { super.onDestroy(); handleNotification();}protected void handleNotification() { if (!isHandleNotification) { return; } String targetKey = notificationExtra.getString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME); if (TextUtils.isEmpty(targetKey)) { Logger.e(TAG, "handleNotification: target key is null !"); return; } try { Class<?> destActivityClass = Class.forName(targetKey); Intent destIntent = new Intent(this, destActivityClass); destIntent.putExtras(notificationExtra); startActivity(destIntent); } catch (ClassNotFoundException e) { e.printStackTrace(); Logger.e(TAG, "handleNotification: reflect to get activity class failed !"); }}这样就在同一进程的方案下实现了Notification所需的跳转功能。3. 扩展预备
上述内容只是对最为常见的一些Notification构建、样式、操作的基础封装,在未来的使用中必然还会有更多的需求,因此自定义的Builder以及一些独特的功能应该有预留的扩展方法,至少不至于在当前Notification构建满足不了需求的时候就只能copy代码...我在项目中编写的SimpleNotificationBuilder代理了常用的NotificationCompat.Builder所含方法,但并没有全部代理,主要是考虑到潜在的SDK更新后源码变更的可能性,因此配置了一个接口用于对类中的NotificationCompat.Builder进行编辑。另一方面可能某些自己构建的Notification也会用到根据应用启动状态判断打开方式的功能,因此将打开BroadcastReceiver的Intent包装功能单独做了一个方法以供使用:/** * 将Notification的Intent转换成用广播传递的Intent * <p> * 主要用于自定义Notification时处理点击打开Activity的事件,使用此方法 * 将会在应用启动时直接打开目标Activity,应用未启动时先启动应用再打开Activity * </p> * */public static Intent getBroadcastIntent(Context context, Intent targetActivityIntent) { Intent broadcastIntent = new Intent(context, NotificationReceiver.class); Bundle bundle = new Bundle(); bundle.putAll(targetActivityIntent.getExtras()); bundle.putString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME, targetActivityIntent.getComponent().getClassName()); broadcastIntent.putExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA, bundle); return broadcastIntent;}
新闻热点
疑难解答