├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── paozhuanyinyu │ │ └── com │ │ └── freshmember │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── paozhuanyinyu │ │ │ └── freshmember │ │ │ ├── MainActivity.java │ │ │ ├── MyApplication.java │ │ │ ├── MyService.java │ │ │ ├── NotificationUtils.java │ │ │ └── SwitchView.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ └── notification.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── accessibility_service_config.xml │ └── test │ └── java │ └── paozhuanyinyu │ └── com │ └── freshmember │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image └── download_qrcode.png ├── screenshots ├── device-2017-09-13-160625.png ├── device-2017-09-13-160642.png └── device-2017-09-13-160658.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /app/build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 paozhuanyinyu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreshMember 2 | 3 | 4 | ## 简介 5 | 当你刚接手一个陌生项目的时候,对项目结构还不熟悉,在熟悉项目期间,肯定是经常对应这代码跑一下项目,但是一个项目里面可能有很多个Activity,但是你不熟悉,看起来很费劲。这个小工具就是来解决这个问题的,它可以将当前Activity的全限定类名显示在通知栏,你可以很方便的知道任何界面的类名。所以如果你研究其他app,这个工具也可以帮到你。 6 | 7 | 8 | ![image](./screenshots/device-2017-09-13-160658.png) 9 | 10 | 11 | ## 原理 12 | 这个工具的原理是AccessibilityService, 这个服务现在广泛用于非手机厂商应用商店自动安装应用,抢红包等场景,解决上面的问题更是不在话下。AccessibilityService可以监听AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED事件,跳转界面是会触发这个事件的,然后你可以通过API获取当前界面的全限定类名;然后我将类名放在通知里显示在通知栏上,这个通知是常驻通知栏的;然后我用了一些进程常驻的办法,让这个服务的常驻后台,但是也不能保证100%常驻,一般情况下你把测试机设置一下后台运行,锁屏不清理,安全管家不清理等,然后你不手动杀死,基本上可以做到常驻。 13 | 14 | ## apk扫码下载 15 | 16 | ![image](./image/download_qrcode.png) 17 | 18 | ## Thanks 19 | 20 | [ActivityLogHelper](https://github.com/yrickwong/ActivityLogHelper) 21 | 22 | ## License 23 | MIT License 24 | 25 | Copyright (c) 2017 paozhuanyinyu 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion "28.0.0" 6 | defaultConfig { 7 | applicationId "com.paozhuanyinyu.freshmember" 8 | minSdkVersion 16 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(include: ['*.jar'], dir: 'libs') 24 | implementation 'com.android.support:appcompat-v7:28.0.0' 25 | implementation 'com.google.code.gson:gson:2.8.5' 26 | implementation ('com.gyf.cactus:cactus-support:1.1.3-beta09'){ 27 | exclude group: 'com.google.code.gson', module: 'gson' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\Android_Studio_SDK\Android_Studio_SDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/paozhuanyinyu/com/freshmember/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package paozhuanyinyu.com.freshmember; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("paozhuanyinyu.com.freshmember", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/paozhuanyinyu/freshmember/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.paozhuanyinyu.freshmember; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.os.Bundle; 7 | import android.provider.Settings; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.text.TextUtils; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.widget.Toast; 13 | import paozhuanyinyu.com.freshmember.R; 14 | 15 | 16 | public class MainActivity extends AppCompatActivity { 17 | private static final int OPEN_SERVICE = 92; 18 | private SwitchView sb_switch; 19 | private SwitchView sb_switch_notification; 20 | private SharedPreferences sp; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.activity_main); 26 | initView(); 27 | } 28 | 29 | @Override 30 | protected void onResume() { 31 | super.onResume(); 32 | if(sp == null){ 33 | sp = getSharedPreferences("fresh_member", Context.MODE_PRIVATE); 34 | } 35 | if((isAccessibilitySettingsOn(this) && !sb_switch.isOpened()) || (!isAccessibilitySettingsOn(this) && sb_switch.isOpened())){ 36 | sb_switch.setOpened(isAccessibilitySettingsOn(this)); 37 | } 38 | if((isAccessibilitySettingsOn(this)&&sp.getBoolean("notification_switch",false) && !sb_switch_notification.isOpened()) || (!(isAccessibilitySettingsOn(this)&&sp.getBoolean("notification_switch",false)) && sb_switch_notification.isOpened())){ 39 | sb_switch_notification.setOpened(isAccessibilitySettingsOn(this)&&sp.getBoolean("notification_switch",false)); 40 | } 41 | 42 | } 43 | 44 | @Override 45 | public void onBackPressed() { 46 | Intent launcherIntent =new Intent(Intent.ACTION_MAIN); 47 | launcherIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 48 | launcherIntent.addCategory(Intent.CATEGORY_HOME); 49 | startActivity(launcherIntent); 50 | } 51 | private void initView() { 52 | sb_switch = (SwitchView) findViewById(R.id.sb_switch); 53 | sb_switch_notification = (SwitchView) findViewById(R.id.sb_switch_notification); 54 | 55 | 56 | sb_switch.setOnStateChangedListener(new SwitchView.OnStateChangedListener(){ 57 | 58 | @Override 59 | public void toggleToOn(View view) { 60 | sb_switch.toggleSwitch(true); 61 | if(!isAccessibilitySettingsOn(MainActivity.this)){ 62 | startActivityForResult(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS),OPEN_SERVICE); 63 | } 64 | } 65 | 66 | @Override 67 | public void toggleToOff(View view) { 68 | if(!isAccessibilitySettingsOn(MainActivity.this)){ 69 | sb_switch.toggleSwitch(false); 70 | }else{ 71 | Toast.makeText(MainActivity.this,"服务是打开状态",Toast.LENGTH_SHORT).show(); 72 | sb_switch.toggleSwitch(true); 73 | } 74 | } 75 | }); 76 | sp = getSharedPreferences("fresh_member", Context.MODE_PRIVATE); 77 | 78 | sb_switch_notification.setOnStateChangedListener(new SwitchView.OnStateChangedListener() { 79 | @Override 80 | public void toggleToOn(View view) { 81 | if(isAccessibilitySettingsOn(MainActivity.this)){ 82 | sb_switch_notification.toggleSwitch(true); 83 | sp.edit().putBoolean("notification_switch",true).commit(); 84 | NotificationUtils.showNotification(MainActivity.this,getComponentName().toShortString()); 85 | }else{ 86 | Toast.makeText(MainActivity.this,"请先打开服务",Toast.LENGTH_SHORT).show(); 87 | } 88 | 89 | } 90 | 91 | @Override 92 | public void toggleToOff(View view) { 93 | sb_switch_notification.toggleSwitch(false); 94 | sp.edit().putBoolean("notification_switch",false).commit(); 95 | NotificationUtils.hideNotification(MainActivity.this); 96 | } 97 | }); 98 | } 99 | private boolean isAccessibilitySettingsOn(Context mContext) { 100 | int accessibilityEnabled = 0; 101 | final String service = getPackageName() + "/" + getPackageName() + ".MyService"; 102 | boolean accessibilityFound = false; 103 | try { 104 | accessibilityEnabled = Settings.Secure.getInt( 105 | mContext.getApplicationContext().getContentResolver(), 106 | Settings.Secure.ACCESSIBILITY_ENABLED); 107 | Log.v("MainActivity","accessibilityEnabled = " + accessibilityEnabled); 108 | } catch (Settings.SettingNotFoundException e) { 109 | Log.e("MainActivity","Error finding setting, default accessibility to not found: " 110 | + e.getMessage()); 111 | } 112 | TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':'); 113 | 114 | if (accessibilityEnabled == 1) { 115 | Log.v("MainActivity","***ACCESSIBILIY IS ENABLED*** -----------------"); 116 | String settingValue = Settings.Secure.getString( 117 | mContext.getApplicationContext().getContentResolver(), 118 | Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 119 | if (settingValue != null) { 120 | TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; 121 | splitter.setString(settingValue); 122 | while (splitter.hasNext()) { 123 | String accessabilityService = splitter.next(); 124 | 125 | Log.v("MainActivity","-------------- > accessabilityService :: " + accessabilityService); 126 | if (accessabilityService.equalsIgnoreCase(service)) { 127 | Log.v("MainActivity","We've found the correct setting - accessibility is switched on!"); 128 | return true; 129 | } 130 | } 131 | } 132 | } else { 133 | Log.v("MainActivity","***ACCESSIBILIY IS DISABLED***"); 134 | } 135 | return accessibilityFound; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/com/paozhuanyinyu/freshmember/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.paozhuanyinyu.freshmember; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | import com.gyf.cactus.Cactus; 6 | import paozhuanyinyu.com.freshmember.BuildConfig; 7 | 8 | /** 9 | * Created by Administrator on 2017/9/8. 10 | */ 11 | 12 | public class MyApplication extends Application { 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | Log.d("MyApplication","Application onCreate"); 17 | Cactus.getInstance() 18 | .isDebug(BuildConfig.DEBUG) 19 | .register(this); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/paozhuanyinyu/freshmember/MyService.java: -------------------------------------------------------------------------------- 1 | package com.paozhuanyinyu.freshmember; 2 | 3 | import android.accessibilityservice.AccessibilityService; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.SharedPreferences; 8 | import android.content.pm.PackageManager; 9 | import android.util.Log; 10 | import android.view.accessibility.AccessibilityEvent; 11 | 12 | /** 13 | * Created by Administrator on 2017/9/6. 14 | */ 15 | 16 | public class MyService extends AccessibilityService{ 17 | private String currentActivityName; 18 | private SharedPreferences sp; 19 | public static final int NOTIFICATION_ID=0x11; 20 | @Override 21 | public void onCreate() { 22 | super.onCreate(); 23 | } 24 | @Override 25 | protected void onServiceConnected() { 26 | super.onServiceConnected(); 27 | sp = getSharedPreferences("fresh_member", Context.MODE_PRIVATE); 28 | Log.d("MyService","onServiceConnected"); 29 | } 30 | 31 | @Override 32 | public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) { 33 | Log.d("MyService","onAccessibilityEvent"); 34 | getCurrentActivityName(accessibilityEvent); 35 | } 36 | 37 | @Override 38 | public void onInterrupt() { 39 | Log.d("MyService","onInterrupt"); 40 | NotificationUtils.hideNotification(this); 41 | } 42 | 43 | @Override 44 | public boolean onUnbind(Intent intent) { 45 | Log.d("MyService","onUnbind"); 46 | NotificationUtils.hideNotification(this); 47 | return super.onUnbind(intent); 48 | } 49 | private void getCurrentActivityName(AccessibilityEvent event) { 50 | if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 51 | return; 52 | } 53 | try { 54 | String pkgName = event.getPackageName().toString(); 55 | String className = event.getClassName().toString(); 56 | ComponentName componentName = new ComponentName(pkgName, className); 57 | getPackageManager().getActivityInfo(componentName, 0); 58 | currentActivityName = componentName.flattenToShortString(); 59 | Log.d("MyService", "cur=" + currentActivityName); 60 | if(sp.getBoolean("notification_switch",false)){ 61 | NotificationUtils.updateNotification(this,currentActivityName); 62 | } 63 | } catch (PackageManager.NameNotFoundException e) { 64 | //只是窗口变化,并无activity调转 65 | Log.d("MyService", "e=" + e.getLocalizedMessage()); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/paozhuanyinyu/freshmember/NotificationUtils.java: -------------------------------------------------------------------------------- 1 | package com.paozhuanyinyu.freshmember; 2 | 3 | import android.app.NotificationManager; 4 | import android.app.PendingIntent; 5 | import android.content.BroadcastReceiver; 6 | import android.content.ClipboardManager; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.IntentFilter; 10 | import android.os.Build; 11 | import android.support.annotation.NonNull; 12 | import android.support.v4.app.NotificationCompat; 13 | import android.util.Log; 14 | import android.widget.RemoteViews; 15 | import android.widget.Toast; 16 | 17 | import java.lang.reflect.Method; 18 | 19 | import paozhuanyinyu.com.freshmember.R; 20 | 21 | /** 22 | * Created by kay on 2017/9/6. 23 | */ 24 | 25 | public class NotificationUtils { 26 | private static final String MY_ACTION = "com.notifications.intent.action.ButtonClick"; 27 | private static NotificationCompat.Builder mBuilder; 28 | private static NotificationManager mNotificationManager; 29 | //create a notification 30 | public static void showNotification(final Context context, final String contentText){ 31 | //创建一个NotificationManager的引用 32 | String ns = Context.NOTIFICATION_SERVICE; 33 | mNotificationManager = (NotificationManager)context.getSystemService(ns); 34 | // 定义Notification的各种属性 35 | int icon = R.mipmap.ic_launcher; //notification icon 36 | CharSequence tickerText = "the class name of current activity"; //状态栏显示的通知文本提示 37 | long when = System.currentTimeMillis(); //通知产生的时间,会在通知信息里显示 38 | //用上面的属性初始化 Nofification 39 | mBuilder = new NotificationCompat.Builder(context); 40 | mBuilder.setSmallIcon(icon); 41 | mBuilder.setTicker(tickerText); 42 | mBuilder.setWhen(when); 43 | /* 44 | * 添加声音 45 | * notification.defaults |=Notification.DEFAULT_SOUND; 46 | * 或者使用以下几种方式 47 | * notification.sound = Uri.parse("file:///sdcard/notification/ringer.mp3"); 48 | * notification.sound = Uri.withAppendedPath(Audio.Media.INTERNAL_CONTENT_URI, "6"); 49 | * 如果想要让声音持续重复直到用户对通知做出反应,则可以在notification的flags字段增加"FLAG_INSISTENT" 50 | * 如果notification的defaults字段包括了"DEFAULT_SOUND"属性,则这个属性将覆盖sound字段中定义的声音 51 | */ 52 | /* 53 | * 添加振动 54 | * notification.defaults |= Notification.DEFAULT_VIBRATE; 55 | * 或者可以定义自己的振动模式: 56 | * long[] vibrate = {0,100,200,300}; //0毫秒后开始振动,振动100毫秒后停止,再过200毫秒后再次振动300毫秒 57 | * notification.vibrate = vibrate; 58 | * long数组可以定义成想要的任何长度 59 | * 如果notification的defaults字段包括了"DEFAULT_VIBRATE",则这个属性将覆盖vibrate字段中定义的振动 60 | */ 61 | /* 62 | * 添加LED灯提醒 63 | * notification.defaults |= Notification.DEFAULT_LIGHTS; 64 | * 或者可以自己的LED提醒模式: 65 | * notification.ledARGB = 0xff00ff00; 66 | * notification.ledOnMS = 300; //亮的时间 67 | * notification.ledOffMS = 1000; //灭的时间 68 | * notification.flags |= Notification.FLAG_SHOW_LIGHTS; 69 | */ 70 | /* 71 | * 更多的特征属性 72 | * notification.flags |= FLAG_AUTO_CANCEL; //在通知栏上点击此通知后自动清除此通知 73 | * notification.flags |= FLAG_INSISTENT; //重复发出声音,直到用户响应此通知 74 | * notification.flags |= FLAG_ONGOING_EVENT; //将此通知放到通知栏的"Ongoing"即"正在运行"组中 75 | * notification.flags |= FLAG_NO_CLEAR; //表明在点击了通知栏中的"清除通知"后,此通知不清除, 76 | * //经常与FLAG_ONGOING_EVENT一起使用 77 | * notification.number = 1; //number字段表示此通知代表的当前事件数量,它将覆盖在状态栏图标的顶部 78 | * //如果要使用此字段,必须从1开始 79 | * notification.iconLevel = ; // 80 | */ 81 | 82 | //设置通知的事件消息 83 | // CharSequence contentTitle = "当前界面的全限定类名"; //通知栏标题 84 | Intent notificationIntent = new Intent(context,MainActivity.class); //点击该通知后要跳转的Activity 85 | PendingIntent contentIntent = PendingIntent.getActivity(context,0,notificationIntent,0); 86 | RemoteViews view_custom = getRemoteViews(context, contentText); 87 | 88 | PendingIntent pendButtonIntent = getPendingIntent(context, contentText); 89 | view_custom.setOnClickPendingIntent(R.id.btn, pendButtonIntent); 90 | 91 | mBuilder.setCustomContentView(view_custom); 92 | mBuilder.setContentIntent(contentIntent); 93 | mBuilder.setOngoing(true); 94 | //把Notification传递给 NotificationManager 95 | mNotificationManager.notify(0,mBuilder.build()); 96 | } 97 | 98 | @NonNull 99 | private static RemoteViews getRemoteViews(Context context, String contentText) { 100 | RemoteViews view_custom = new RemoteViews(context.getPackageName(), R.layout.notification); 101 | view_custom.setTextViewText(R.id.content,contentText); 102 | return view_custom; 103 | } 104 | 105 | private static PendingIntent getPendingIntent(Context context, String contentText) { 106 | final int id = (int)System.currentTimeMillis(); 107 | BroadcastReceiver onClickReceiver = new BroadcastReceiver() { 108 | 109 | @Override 110 | public void onReceive(Context context, Intent intent) { 111 | if (intent.getAction().equals(MY_ACTION + "_" + id)) { 112 | //获取剪贴板管理器: 113 | ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 114 | cm.setText(intent.getStringExtra("content")); 115 | Log.d("NotificationUtils","content: " + intent.getStringExtra("content")); 116 | collapseStatusBar(context); 117 | Toast.makeText(context,"已复制到粘贴板: " + intent.getStringExtra("content"),Toast.LENGTH_SHORT).show(); 118 | } 119 | } 120 | }; 121 | IntentFilter filter = new IntentFilter(); 122 | filter.addAction(MY_ACTION + "_" + id); 123 | context.registerReceiver(onClickReceiver, filter); 124 | Intent buttonIntent = new Intent(MY_ACTION + "_" + id); 125 | buttonIntent.putExtra("content",contentText); 126 | return PendingIntent.getBroadcast(context, id, buttonIntent, 0); 127 | } 128 | //hide the notification bar 129 | public static void collapseStatusBar(Context context) { 130 | try { 131 | Object statusBarManager = context.getSystemService("statusbar"); 132 | Method collapse; 133 | 134 | if (Build.VERSION.SDK_INT <= 16) { 135 | collapse = statusBarManager.getClass().getMethod("collapse"); 136 | } else { 137 | collapse = statusBarManager.getClass().getMethod("collapsePanels"); 138 | } 139 | collapse.invoke(statusBarManager); 140 | } catch (Exception localException) { 141 | localException.printStackTrace(); 142 | } 143 | } 144 | //clear the notification 145 | public static void hideNotification(Context context){ 146 | if(mNotificationManager!=null){ 147 | mNotificationManager.cancel(0); 148 | } 149 | } 150 | //update the content of the notification 151 | public static void updateNotification(Context context,String contentText){ 152 | if(mBuilder==null||mNotificationManager==null){ 153 | showNotification(context,contentText); 154 | }else{ 155 | RemoteViews remoteViews = getRemoteViews(context,contentText); 156 | PendingIntent pendButtonIntent = getPendingIntent(context, contentText); 157 | remoteViews.setOnClickPendingIntent(R.id.btn, pendButtonIntent); 158 | mBuilder.setCustomContentView(remoteViews); 159 | mNotificationManager.notify(0,mBuilder.build()); 160 | } 161 | 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/paozhuanyinyu/freshmember/SwitchView.java: -------------------------------------------------------------------------------- 1 | package com.paozhuanyinyu.freshmember; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.Paint.Style; 7 | import android.graphics.Path; 8 | import android.graphics.RadialGradient; 9 | import android.graphics.RectF; 10 | import android.graphics.Shader; 11 | import android.os.Parcel; 12 | import android.os.Parcelable; 13 | import android.util.AttributeSet; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.view.animation.AccelerateInterpolator; 17 | 18 | /** 19 | * For details, please see http://blog.csdn.net/bfbx5173/article/details/45191147 20 | * 21 | * @author else 22 | */ 23 | public class SwitchView extends View { 24 | 25 | private final Paint paint = new Paint(); 26 | private final Path sPath = new Path(); 27 | private final Path bPath = new Path(); 28 | private final RectF bRectF = new RectF(); 29 | private float sAnim, bAnim; 30 | private RadialGradient shadowGradient; 31 | private final AccelerateInterpolator aInterpolator = new AccelerateInterpolator(2); 32 | 33 | /** 34 | * state switch on 35 | */ 36 | public static final int STATE_SWITCH_ON = 4; 37 | /** 38 | * state prepare to off 39 | */ 40 | public static final int STATE_SWITCH_ON2 = 3; 41 | /** 42 | * state prepare to on 43 | */ 44 | public static final int STATE_SWITCH_OFF2 = 2; 45 | /** 46 | * state prepare to off 47 | */ 48 | public static final int STATE_SWITCH_OFF = 1; 49 | /** 50 | * current state 51 | */ 52 | private int state = STATE_SWITCH_OFF; 53 | /** 54 | * last state 55 | */ 56 | private int lastState = state; 57 | 58 | private boolean isOpened = false; 59 | 60 | private int mWidth, mHeight; 61 | private float sWidth, sHeight; 62 | private float sLeft, sTop, sRight, sBottom; 63 | private float sCenterX, sCenterY; 64 | private float sScale; 65 | 66 | private float bOffset; 67 | private float bRadius, bStrokeWidth; 68 | private float bWidth; 69 | private float bLeft, bTop, bRight, bBottom; 70 | private float bOnLeftX, bOn2LeftX, bOff2LeftX, bOffLeftX; 71 | 72 | private float shadowHeight; 73 | 74 | public SwitchView(Context context) { 75 | this(context, null); 76 | } 77 | 78 | public SwitchView(Context context, AttributeSet attrs) { 79 | super(context, attrs); 80 | setLayerType(LAYER_TYPE_SOFTWARE, null); 81 | } 82 | 83 | @Override 84 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 85 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 86 | int heightSize = (int) (widthSize * 0.65f); 87 | setMeasuredDimension(widthSize, heightSize); 88 | } 89 | 90 | @Override 91 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 92 | super.onSizeChanged(w, h, oldw, oldh); 93 | mWidth = w; 94 | mHeight = h; 95 | 96 | sLeft = sTop = 0; 97 | sRight = mWidth; 98 | sBottom = mHeight * 0.91f; 99 | sWidth = sRight - sLeft; 100 | sHeight = sBottom - sTop; 101 | sCenterX = (sRight + sLeft) / 2; 102 | sCenterY = (sBottom + sTop) / 2; 103 | 104 | shadowHeight = mHeight - sBottom; 105 | 106 | bLeft = bTop = 0; 107 | bRight = bBottom = sBottom; 108 | bWidth = bRight - bLeft; 109 | final float halfHeightOfS = (sBottom - sTop) / 2; 110 | bRadius = halfHeightOfS * 0.95f; 111 | bOffset = bRadius * 0.2f; 112 | bStrokeWidth = (halfHeightOfS - bRadius) * 2; 113 | 114 | bOnLeftX = sWidth - bWidth; 115 | bOn2LeftX = bOnLeftX - bOffset; 116 | bOffLeftX = 0; 117 | bOff2LeftX = 0; 118 | 119 | sScale = 1 - bStrokeWidth / sHeight; 120 | 121 | RectF sRectF = new RectF(sLeft, sTop, sBottom, sBottom); 122 | sPath.arcTo(sRectF, 90, 180); 123 | sRectF.left = sRight - sBottom; 124 | sRectF.right = sRight; 125 | sPath.arcTo(sRectF, 270, 180); 126 | sPath.close(); 127 | 128 | bRectF.left = bLeft; 129 | bRectF.right = bRight; 130 | bRectF.top = bTop + bStrokeWidth / 2; 131 | bRectF.bottom = bBottom - bStrokeWidth / 2; 132 | 133 | shadowGradient = new RadialGradient(bWidth / 2, bWidth / 2, bWidth / 2, 0xff000000, 0x00000000, Shader.TileMode.CLAMP); 134 | } 135 | 136 | private void calcBPath(float percent) { 137 | bPath.reset(); 138 | bRectF.left = bLeft + bStrokeWidth / 2; 139 | bRectF.right = bRight - bStrokeWidth / 2; 140 | bPath.arcTo(bRectF, 90, 180); 141 | bRectF.left = bLeft + percent * bOffset + bStrokeWidth / 2; 142 | bRectF.right = bRight + percent * bOffset - bStrokeWidth / 2; 143 | bPath.arcTo(bRectF, 270, 180); 144 | bPath.close(); 145 | } 146 | 147 | private float calcBTranslate(float percent) { 148 | float result = 0; 149 | int wich = state - lastState; 150 | switch (wich) { 151 | case 1: 152 | // off -> off2 153 | if (state == STATE_SWITCH_OFF2) { 154 | result = bOff2LeftX - (bOff2LeftX - bOffLeftX) * percent; 155 | } 156 | // on2 -> on 157 | else if (state == STATE_SWITCH_ON) { 158 | result = bOnLeftX - (bOnLeftX - bOn2LeftX) * percent; 159 | } 160 | break; 161 | case 2: 162 | // off2 -> on 163 | if (state == STATE_SWITCH_ON) { 164 | result = bOnLeftX - (bOnLeftX - bOff2LeftX) * percent; 165 | } 166 | // off -> on2 167 | else if (state == STATE_SWITCH_ON) { 168 | result = bOn2LeftX - (bOn2LeftX - bOffLeftX) * percent; 169 | } 170 | break; 171 | case 3: // off -> on 172 | result = bOnLeftX - (bOnLeftX - bOffLeftX) * percent; 173 | break; 174 | case -1: 175 | // on -> on2 176 | if (state == STATE_SWITCH_ON2) { 177 | result = bOn2LeftX + (bOnLeftX - bOn2LeftX) * percent; 178 | } 179 | // off2 -> off 180 | else if (state == STATE_SWITCH_OFF) { 181 | result = bOffLeftX + (bOff2LeftX - bOffLeftX) * percent; 182 | } 183 | break; 184 | case -2: 185 | // on2 -> off 186 | if (state == STATE_SWITCH_OFF) { 187 | result = bOffLeftX + (bOn2LeftX - bOffLeftX) * percent; 188 | } 189 | // on -> off2 190 | else if (state == STATE_SWITCH_OFF2) { 191 | result = bOff2LeftX + (bOnLeftX - bOff2LeftX) * percent; 192 | } 193 | break; 194 | case -3: // on -> off 195 | result = bOffLeftX + (bOnLeftX - bOffLeftX) * percent; 196 | break; 197 | } 198 | 199 | return result - bOffLeftX; 200 | } 201 | 202 | @Override 203 | protected void onDraw(Canvas canvas) { 204 | super.onDraw(canvas); 205 | paint.setAntiAlias(true); 206 | final boolean isOn = (state == STATE_SWITCH_ON || state == STATE_SWITCH_ON2); 207 | // draw background 208 | paint.setStyle(Style.FILL); 209 | paint.setColor(isOn ? 0xff4bd763 : 0xffe3e3e3); 210 | canvas.drawPath(sPath, paint); 211 | 212 | sAnim = sAnim - 0.1f > 0 ? sAnim - 0.1f : 0; 213 | bAnim = bAnim - 0.1f > 0 ? bAnim - 0.1f : 0; 214 | 215 | final float dsAnim = aInterpolator.getInterpolation(sAnim); 216 | final float dbAnim = aInterpolator.getInterpolation(bAnim); 217 | 218 | // draw background animation 219 | final float scale = sScale * (isOn ? dsAnim : 1 - dsAnim); 220 | final float scaleOffset = (bOnLeftX + bRadius - sCenterX) * (isOn ? 1 - dsAnim : dsAnim); 221 | canvas.save(); 222 | canvas.scale(scale, scale, sCenterX + scaleOffset, sCenterY); 223 | paint.setColor(0xffffffff); 224 | canvas.drawPath(sPath, paint); 225 | canvas.restore(); 226 | 227 | // draw center bar 228 | canvas.save(); 229 | canvas.translate(calcBTranslate(dbAnim), shadowHeight); 230 | final boolean isState2 = (state == STATE_SWITCH_ON2 || state == STATE_SWITCH_OFF2); 231 | calcBPath(isState2 ? 1 - dbAnim : dbAnim); 232 | 233 | // draw shadow 234 | paint.setStyle(Style.FILL); 235 | paint.setColor(0xff333333); 236 | paint.setShader(shadowGradient); 237 | canvas.drawPath(bPath, paint); 238 | paint.setShader(null); 239 | canvas.translate(0, -shadowHeight); 240 | 241 | canvas.scale(0.98f, 0.98f, bWidth / 2, bWidth / 2); 242 | paint.setStyle(Style.FILL); 243 | paint.setColor(0xffffffff); 244 | canvas.drawPath(bPath, paint); 245 | 246 | paint.setStyle(Style.STROKE); 247 | paint.setStrokeWidth(bStrokeWidth * 0.5f); 248 | 249 | paint.setColor(isOn ? 0xff4ada60 : 0xffbfbfbf); 250 | canvas.drawPath(bPath, paint); 251 | 252 | canvas.restore(); 253 | 254 | paint.reset(); 255 | if (sAnim > 0 || bAnim > 0) invalidate(); 256 | } 257 | 258 | @Override 259 | public boolean onTouchEvent(MotionEvent event) { 260 | if ((state == STATE_SWITCH_ON || state == STATE_SWITCH_OFF) && (sAnim * bAnim == 0)) { 261 | switch (event.getAction()) { 262 | case MotionEvent.ACTION_DOWN: 263 | return true; 264 | case MotionEvent.ACTION_UP: 265 | lastState = state; 266 | if (state == STATE_SWITCH_OFF) { 267 | refreshState(STATE_SWITCH_OFF2); 268 | } else if (state == STATE_SWITCH_ON) { 269 | refreshState(STATE_SWITCH_ON2); 270 | } 271 | bAnim = 1; 272 | invalidate(); 273 | 274 | if (state == STATE_SWITCH_OFF2) { 275 | listener.toggleToOn(this); 276 | } else if (state == STATE_SWITCH_ON2) { 277 | listener.toggleToOff(this); 278 | } 279 | break; 280 | } 281 | } 282 | return super.onTouchEvent(event); 283 | } 284 | 285 | private void refreshState(int newState) { 286 | if (!isOpened && newState == STATE_SWITCH_ON) { 287 | isOpened = true; 288 | } else if (isOpened && newState == STATE_SWITCH_OFF) { 289 | isOpened = false; 290 | } 291 | lastState = state; 292 | state = newState; 293 | postInvalidate(); 294 | } 295 | 296 | /** 297 | * @return the state of switch view 298 | */ 299 | public boolean isOpened() { 300 | return isOpened; 301 | } 302 | 303 | /** 304 | * if set true , the state change to on; 305 | * if set false, the state change to off 306 | * 307 | * @param isOpened 308 | */ 309 | public void setOpened(boolean isOpened) { 310 | refreshState(isOpened ? STATE_SWITCH_ON : STATE_SWITCH_OFF); 311 | } 312 | 313 | /** 314 | * if set true , the state change to on; 315 | * if set false, the state change to off 316 | *
change state with animation 317 | * 318 | * @param isOpened 319 | */ 320 | public void toggleSwitch(final boolean isOpened) { 321 | this.isOpened = isOpened; 322 | postDelayed(new Runnable() { 323 | @Override 324 | public void run() { 325 | toggleSwitch(isOpened ? STATE_SWITCH_ON : STATE_SWITCH_OFF); 326 | } 327 | }, 300); 328 | } 329 | 330 | private synchronized void toggleSwitch(int wich) { 331 | if (wich == STATE_SWITCH_ON || wich == STATE_SWITCH_OFF) { 332 | if ((wich == STATE_SWITCH_ON && (lastState == STATE_SWITCH_OFF || lastState == STATE_SWITCH_OFF2)) 333 | || (wich == STATE_SWITCH_OFF && (lastState == STATE_SWITCH_ON || lastState == STATE_SWITCH_ON2))) { 334 | sAnim = 1; 335 | } 336 | bAnim = 1; 337 | refreshState(wich); 338 | } 339 | } 340 | 341 | public interface OnStateChangedListener { 342 | void toggleToOn(View view); 343 | 344 | void toggleToOff(View view); 345 | } 346 | 347 | private OnStateChangedListener listener = new OnStateChangedListener() { 348 | @Override 349 | public void toggleToOn(View view) { 350 | toggleSwitch(STATE_SWITCH_ON); 351 | } 352 | 353 | @Override 354 | public void toggleToOff(View view) { 355 | toggleSwitch(STATE_SWITCH_OFF); 356 | } 357 | }; 358 | 359 | public void setOnStateChangedListener(OnStateChangedListener listener) { 360 | if (listener == null) throw new IllegalArgumentException("empty listener"); 361 | this.listener = listener; 362 | } 363 | 364 | @Override 365 | public Parcelable onSaveInstanceState() { 366 | Parcelable superState = super.onSaveInstanceState(); 367 | SavedState ss = new SavedState(superState); 368 | ss.isOpened = isOpened; 369 | return ss; 370 | } 371 | 372 | @Override 373 | public void onRestoreInstanceState(Parcelable state) { 374 | SavedState ss = (SavedState) state; 375 | super.onRestoreInstanceState(ss.getSuperState()); 376 | this.isOpened = ss.isOpened; 377 | this.state = this.isOpened ? STATE_SWITCH_ON : STATE_SWITCH_OFF; 378 | } 379 | 380 | static final class SavedState extends BaseSavedState { 381 | private boolean isOpened; 382 | 383 | SavedState(Parcelable superState) { 384 | super(superState); 385 | } 386 | 387 | private SavedState(Parcel in) { 388 | super(in); 389 | isOpened = 1 == in.readInt(); 390 | } 391 | 392 | @Override 393 | public void writeToParcel(Parcel out, int flags) { 394 | super.writeToParcel(out, flags); 395 | out.writeInt(isOpened ? 1 : 0); 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 22 | 23 | 30 | 31 | 32 | 37 | 43 | 44 | 49 | 50 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 26 |