1.锁屏页
实现思路
自定义锁屏页大多使用悬浮窗和弹出Activity的方式实现,这里我参考了QQ音乐和网易云音乐的实现方式,在锁屏页面弹出Activity。首先,动态注册一个广播接收器用于监听手机熄屏和亮屏的广播,在接收到熄屏广播后启动Activity,在这里启动Activity时,还涉及到一个权限问题,由于一些手机厂商都有自己的自定义权限,这里我选择所有的权限让用户手动去开启(权限要动态校验)。到这一步已经差不多实现了,最后就是一些细节问题,滑动解锁以及沉浸式实现
监听亮屏和熄屏广播的代码网上都有,这里就不多说了,重点是权限适配的问题,我几乎找遍了全网,整理出下面这个工具类,直接上代码
权限适配
/**
* 锁屏显示权限工具类
*
* @author Sago
* @since 2024/12/7
*/
public class LockScreenPermissionUtil {
private static final String TAG = "LockScreenPermissionUtil";
private static final int HW_OP_CODE_POPUP_BACKGROUND_WINDOW = 100000;
private static final int XM_OP_CODE_POPUP_BACKGROUND_WINDOW = 10021;
private static Context sContext;
private static boolean isInit = false;
public static void init(Context context) {
if (!isInit) {
sContext = context.getApplicationContext();
isInit = true;
}
}
/**
* 是否有后台弹出页面权限
*/
public static boolean hasPopupBackgroundPermission() {
if (isHuawei()) {
return checkHwPermission();
}
if (isXiaoMi()) {
return checkXmPermission();
}
if (isVivo()) {
return checkVivoPermission();
}
if (isOppo() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(sContext);
}
return true;
}
/**
* 是否有悬浮窗权限
*/
public static boolean haveSystemAlertWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(sContext);
}
return true;
}
public static boolean isHuawei() {
return checkManufacturer("huawei");
}
public static boolean isXiaoMi() {
return checkManufacturer("xiaomi");
}
public static boolean isOppo() {
return checkManufacturer("oppo");
}
public static boolean isVivo() {
return checkManufacturer("vivo");
}
private static boolean checkManufacturer(String manufacturer) {
return manufacturer.equalsIgnoreCase(Build.MANUFACTURER);
}
/**
* 华为后台弹窗权限
*
* @return
*/
private static boolean checkHwPermission() {
Context context = sContext;
try {
Class<?> c = Class.forName("com.huawei.android.app.AppOpsManagerEx");
Method m = c.getDeclaredMethod(
"checkHwOpNoThrow",
AppOpsManager.class,
int.class,
int.class,
String.class
);
Object result = m.invoke(
c.newInstance(),
context.getSystemService(Context.APP_OPS_SERVICE),
HW_OP_CODE_POPUP_BACKGROUND_WINDOW,
Binder.getCallingUid(),
context.getPackageName()
);
Log.d(TAG, "LockScreenPermissionUtil checkHwPermission result: " + (AppOpsManager.MODE_ALLOWED == (int) result));
return AppOpsManager.MODE_ALLOWED == (int) result;
} catch (Exception e) {
// ignore
}
return false;
}
private static boolean checkXmPermission() {
// Context context = sContext;
// AppOpsManager ops = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
// try {
// Method method = ops.getClass().getMethod(
// "checkOpNoThrow",
// int.class,
// int.class,
// String.class
// );
// Object result = method.invoke(
// ops,
// XM_OP_CODE_POPUP_BACKGROUND_WINDOW,
// Process.myUid(),
// context.getPackageName()
// );
// Log.d(TAG, "LockScreenPermissionUtil checkXmPermission result: " + (AppOpsManager.MODE_ALLOWED == (int) result));
// return AppOpsManager.MODE_ALLOWED == (int) result;
// } catch (Exception e) {
// // ignore
// }
// return false;
if (canStartFromBackground(sContext) && canShowOnLockScreen(sContext)) {
return true;
} else {
return false;
}
}
/**
* 小米后台启动页面权限
*
* @param context 上下文
* @return MIUI是否可以从后台启动
*/
public static boolean canStartFromBackground(Context context) {
AppOpsManager ops = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
try {
int op = 10021;
// >= 23
// ops.checkOpNoThrow(op, uid, packageName)
Method method = ops.getClass().getMethod("checkOpNoThrow", int.class, int.class, String.class);
Integer result = (Integer) method.invoke(ops, op, android.os.Process.myUid(), context.getPackageName());
return result == AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
/**
* 小米锁屏显示权限
*
* @param context 上下文
* @return MIUI是否可以锁屏显示
*/
public static boolean canShowOnLockScreen(Context context) {
AppOpsManager ops = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
try {
int op = 10020;
// >= 23
// ops.checkOpNoThrow(op, uid, packageName)
Method method = ops.getClass().getMethod("checkOpNoThrow", int.class, int.class, String.class);
Integer result = (Integer) method.invoke(ops, op, android.os.Process.myUid(), context.getPackageName());
return result == AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
/**
* vivo后台弹出页面权限
*
* @return
*/
private static boolean checkVivoPermission() {
// Context context = sContext;
// Uri uri = Uri.parse("content://com.vivo.permissionmanager.provider.permission/start_bg_activity");
// String selection = "pkgname = ?";
// String[] selectionArgs = {context.getPackageName()};
// int result = 1;
// try {
// context.getContentResolver().query(uri, null, selection, selectionArgs, null).close();
// } catch (Exception exception) {
// // ignore
// }
// Log.d(TAG, "LockScreenPermissionUtil checkVivoPermission result: " + (AppOpsManager.MODE_ALLOWED == result));
// return result == AppOpsManager.MODE_ALLOWED;
if (getvivoBgStartActivityPermissionStatus(sContext) == 0 && getVivoLockStatus(sContext) == 0) {
return true;
} else {
return false;
}
}
/**
* 判断vivo后台弹出界面 1未开启 0开启
*
* @param context
* @return
*/
public static int getvivoBgStartActivityPermissionStatus(Context context) {
String packageName = context.getPackageName();
Uri uri2 = Uri.parse("content://com.vivo.permissionmanager.provider.permission/start_bg_activity");
String selection = "pkgname = ?";
String[] selectionArgs = new String[]{packageName};
try {
Cursor cursor = context
.getContentResolver()
.query(uri2, null, selection, selectionArgs, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int currentmode = cursor.getInt(cursor.getColumnIndex("currentstate"));
cursor.close();
return currentmode;
} else {
cursor.close();
return 1;
}
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return 1;
}
/**
* 判断vivo锁屏显示 1未开启 0开启
* @param context
* @return
*/
public static int getVivoLockStatus(Context context) {
String packageName = context.getPackageName();
Uri uri2 = Uri.parse("content://com.vivo.permissionmanager.provider.permission/control_locked_screen_action");
String selection = "pkgname = ?";
String[] selectionArgs = new String[]{packageName};
try {
Cursor cursor = context
.getContentResolver()
.query(uri2, null, selection, selectionArgs, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int currentmode = cursor.getInt(cursor.getColumnIndex("currentstate"));
cursor.close();
return currentmode;
} else {
cursor.close();
return 1;
}
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return 1;
}
/**
* 启动应用详情页面
*/
private void startCustomSystemAlertWindow() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + sContext.getPackageName()));
sContext.startActivity(intent);
}
}
滑动解锁
使用方法:new这个CircleProgressDrawable对象,与ImageView绑定即可
/**
* 进度条
*
* @author Sago
* @since 2024/9/4
*/
public class CircleProgressDrawable extends Drawable {
private static final float DEFAULT_SHADOW_DRAW_PERCENT = 0.08F;
private static final String DEFAULT_SHADOW_START_COLOR = "#70000000";
private int bgColor = Color.parseColor("#1F000000");
private int pColor = Color.RED;
private float bgWidth = 8F;
private float pWidth = 8F;
private float shadowPercent = DEFAULT_SHADOW_DRAW_PERCENT;
private int width = 0;
private int height = 0;
private int centerX = 0;
private int centerY = 0;
private float radius = 0F;
private float drawPercent = 0F;
private Paint mBgPaint;
private Paint mPaint;
private Paint mShadowPaint;
private Shader mShadowShader;
private int progress = 0;
private int total = 0;
//声明构造函数
public CircleProgressDrawable(int bgColor, int pColor, float bgWidth, float pWidth) {
this.bgColor = bgColor;
this.pColor = pColor;
this.bgWidth = bgWidth;
this.pWidth = pWidth;
initPaints();
}
private void initPaints() {
mBgPaint = new Paint();
mBgPaint.setColor(bgColor);
mBgPaint.setStyle(Paint.Style.STROKE);
mBgPaint.setStrokeWidth(bgWidth);
mPaint = new Paint();
mPaint.setColor(pColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(pWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mShadowPaint = new Paint();
mShadowPaint.setStrokeWidth(pWidth);
mShadowPaint.setStyle(Paint.Style.STROKE);
mShadowPaint.setStrokeCap(Paint.Cap.SQUARE);
mShadowPaint.setAntiAlias(true);
mShadowShader = new SweepGradient(
0F, 0F,
new int[]{Color.parseColor(DEFAULT_SHADOW_START_COLOR), Color.TRANSPARENT},
new float[]{0F, shadowPercent}
);
}
public void setProgress(int progress) {
this.progress = progress;
invalidateSelf();
}
public void setTotal(int total) {
this.total = total;
invalidateSelf();
}
public void setShadowPercent(@FloatRange(from = 0.0, to = 1.0) float shadowPercent) {
this.shadowPercent = shadowPercent;
mShadowShader = new SweepGradient(
0F, 0F,
new int[]{Color.parseColor(DEFAULT_SHADOW_START_COLOR), Color.TRANSPARENT},
new float[]{0F, shadowPercent}
);
invalidateSelf();
}
@Override
public void draw(Canvas canvas) {
width = getBounds().width();
height = getBounds().height();
centerX = (getBounds().left + getBounds().right) / 2;
centerY = (getBounds().top + getBounds().bottom) / 2;
radius = width / 2F - Math.max(bgWidth, pWidth) / 2F;
canvas.save();
canvas.translate(centerX, centerY);
canvas.rotate(90F);
mBgPaint.setShader(null); // Ensure no shader is set for background
canvas.drawCircle(0F, 0F, radius, mBgPaint);
drawPercent = (progress % Math.max(total, 1)) / (float) Math.max(total, 1);
if (progress == 0) {
canvas.drawPoint(radius, 0F, mPaint);
} else if (progress < total) {
canvas.drawArc(
-radius, -radius, radius, radius,
0F, 360F * drawPercent,
false, mPaint
);
} else if (progress == total) {
canvas.drawCircle(0F, 0F, radius, mPaint);
} else {
canvas.drawCircle(0F, 0F, radius, mPaint);
canvas.save();
canvas.rotate(360F * drawPercent);
mShadowPaint.setShader(mShadowShader);
canvas.drawArc(
-radius, -radius, radius, radius,
0F, 360F * shadowPercent,
false, mShadowPaint
);
canvas.restore();
canvas.drawArc(
-radius, -radius, radius, radius,
0F, 360F * drawPercent,
false, mPaint
);
}
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
滑动解锁xml代码
<TextView
android:id="@+id/text_slider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="50dp"
android:text="@string/text_slider"
android:textColor="#ffffff"
android:textSize="14sp" />
<ImageView
android:id="@+id/slider_icon"
android:layout_width="7.01dp"
android:layout_height="12.02dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="53.5dp"
android:layout_toEndOf="@id/text_slider"
android:layout_marginStart="8.98dp"
android:src="@drawable/slider_icon" />
沉浸式
在Activity中实现这个方法即可
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//沉浸式状态栏
if (hasFocus) {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
2.通知栏
版本兼容
安卓8.0以上需要创建通道
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//安卓8.0以上需要创建通道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "运动数据", NotificationManager.IMPORTANCE_DEFAULT);
channel.setSound(null, null);
notificationManager.createNotificationChannel(channel);
}
发送自定义通知(notificationLayout为RemoteViews对象,每次更新需要重新new一个,否则可能会出现一些显示异常的问题)
//构建通知
Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCustomBigContentView(notificationLayout)
.setCustomContentView(notificationLayout)
.setSmallIcon(R.drawable.xxx)
.setContentTitle("xxx")
.setOngoing(true)
.build();
//更新通知
notificationManager.notify(1, notification);
暗色主题适配
这里我是注册了一个Service监听暗色模式的切换,并且定义了两套主题以适应暗色和亮色模式
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
Log.d("ThemeMode", "Dark mode is enabled");
} else if (currentNightMode == Configuration.UI_MODE_NIGHT_NO) {
Log.d("ThemeMode", "Light mode is enabled");
}
}
注意,在首次发送通知前需要获取一次当前系统模式,避免颜色设置主题出错
/**
* 检查当前系统是否为暗色模式
* @param context 上下文
* @return 是否为暗色模式
*/
public static boolean isDarkMode(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10 及以上版本,使用 UiModeManager
return isDarkModeUsingUiMode(context);
} else {
// Android 9 及以下版本,使用 Configuration
return isDarkModeUsingConfiguration(context);
}
}
/**
* 使用 UiModeManager 获取暗色模式状态(适用于 Android 10 及以上)
* @param context 上下文
* @return 是否为暗色模式
*/
private static boolean isDarkModeUsingUiMode(Context context) {
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
int nightMode = uiModeManager.getNightMode();
return nightMode == UiModeManager.MODE_NIGHT_YES; // 暗色模式
}
/**
* 使用 Configuration 获取暗色模式状态(适用于 Android 9 及以下)
* @param context 上下文
* @return 是否为暗色模式
*/
private static boolean isDarkModeUsingConfiguration(Context context) {
int nightModeFlags = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return nightModeFlags == Configuration.UI_MODE_NIGHT_YES; // 暗色模式
}
通知清除
在Service中实现下面方法,即可实现应用退出后台自动改清除通知
@Override
public void onTaskRemoved(Intent rootIntent) {
NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}