2024年已经过半了,我作为聋人独立开发者,我经常会时不时反思:自己这半年到底进步了多少?在这篇文章里,我分享一个用Java和Kotlin研发实现首页壁纸的手势切换功能的案例。如果你有一定开发经验,相信这篇文章对你会非常有所帮助。
一、项目背景
本文详细介绍如何在安卓车机应用的首页实现通过左右手势切换壁纸的功能。
1.1 项目需求分析
本项目是通过左右滑动手势切换首页壁纸,为车机应用用户提供灵活、便捷的壁纸定制体验。这功能提升了应用的互动性,增强了用户对应用的操作感。
二、项目开发
2.1 添加项目依赖项
引入UI库以及Glide库,用于加载壁纸资源。
代码语言:java复制dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'
}
2.2 创建 GestureDetector 实例
为了实现手势检测,使用 GestureDetector
来处理用户的滑动操作。以下是 GestureDetector
的配置:
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.d(TAG, "onFling:");
try {
float diffX = e2.getX() - e1.getX();
float diffY = e2.getY() - e1.getY();
Log.d(TAG, "x: " diffX ", y: " diffY);
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight();
} else {
onSwipeLeft();
}
return true;
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
return false;
}
Kotlin版本
代码语言:java复制override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
Log.d(TAG, "onFling:")
try {
val diffX = e2.x - e1.x
val diffY = e2.y - e1.y
Log.d(TAG, "x: $diffX, y: $diffY")
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight()
} else {
onSwipeLeft()
}
return true
}
}
} catch (exception: Exception) {
exception.printStackTrace()
}
return false
}
2.2.1 代码解析
onFling
方法用于处理快速滑动事件,通过比较e1
(手势开始位置)和e2
(手势结束位置)判断滑动方向。diffX
和diffY
分别表示水平和垂直的滑动距离。SWIPE_THRESHOLD
和SWIPE_VELOCITY_THRESHOLD
用于设定判断滑动的最小距离和速度阈值,避免误触。
2.3 创建壁纸显示页面
代码语言:java复制<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@ id/id_scene_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true">
2.3.1 代码解析
android:clickable="true"
和android:focusable="true"
是非常关键设置,缺少这两项导致手势事件无法正常触发。
2.4 实现手势切换逻辑
以下是处理左右滑动事件的完整代码段:
代码语言:java复制// 设置滑动的阈值
private static final int SWIPE_THRESHOLD = 50;
private static final int SWIPE_VELOCITY_THRESHOLD = 50;
private int currentWallpaperIndex = 0;
private final int[] mWallpapers = {
R.color.colorPrimary,
R.color.colorPrimaryDark,
R.color.colorAccent,
R.color.black_overlay,
R.color.black,
R.color.white
};
...
/**
* 处理向右滑动事件
*/
private void onSwipeRight() {
Log.d(TAG, "right: " currentWallpaperIndex);
if (currentWallpaperIndex > 0) {
currentWallpaperIndex--;
} else {
currentWallpaperIndex = mWallpapers.length - 1;
}
setWallpaper(mWallpapers[currentWallpaperIndex]);
}
/**
* 处理向左滑动事件
*/
private void onSwipeLeft() {
Log.d(TAG, "left: " currentWallpaperIndex);
if (currentWallpaperIndex < mWallpapers.length - 1) {
currentWallpaperIndex ;
} else {
currentWallpaperIndex = 0;
}
setWallpaper(mWallpapers[currentWallpaperIndex]);
}
Kotlin版本
代码语言:java复制
private const val SWIPE_THRESHOLD = 50
private const val SWIPE_VELOCITY_THRESHOLD = 50
private var currentWallpaperIndex = 0
private val mWallpapers = intArrayOf(
R.color.colorPrimary,
R.color.colorPrimaryDark,
R.color.colorAccent,
R.color.black_overlay,
R.color.black,
R.color.white
)
// ...
private fun onSwipeRight() {
Log.d(TAG, "right: $currentWallpaperIndex")
if (currentWallpaperIndex > 0) {
currentWallpaperIndex--
} else {
currentWallpaperIndex = mWallpapers.size - 1
}
setWallpaper(mWallpapers[currentWallpaperIndex])
}
private fun onSwipeLeft() {
Log.d(TAG, "left: $currentWallpaperIndex")
if (currentWallpaperIndex < mWallpapers.size - 1) {
currentWallpaperIndex
} else {
currentWallpaperIndex = 0
}
setWallpaper(mWallpapers[currentWallpaperIndex])
}
2.4.1 代码解析
onSwipeRight()
和onSwipeLeft()
分别处理右滑和左滑的逻辑,通过调整currentWallpaperIndex
实现壁纸的切换。currentWallpaperIndex
的更新逻辑包含边界检查,防止数组越界。
2.5 壁纸切换实现逻辑
代码语言:java复制/**
* 设置壁纸
*/
private void setWallpaper(int colorResourceId) {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(mContext);
int color = mContext.getResources().getColor(colorResourceId);
int screenWidth = 1920;
int screenHeight = 1080;
Bitmap colorBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(colorBitmap);
canvas.drawColor(color);
try {
wallpaperManager.setBitmap(colorBitmap);
} catch (IOException e) {
throw new RuntimeException("设置壁纸失败", e);
}
}
Kotlin版本
代码语言:java复制/**
* 设置壁纸
*/
private fun setWallpaper(colorResourceId: Int) {
val wallpaperManager = WallpaperManager.getInstance(mContext)
val color = mContext.resources.getColor(colorResourceId)
val screenWidth = 1920
val screenHeight = 1080
val colorBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(colorBitmap)
canvas.drawColor(color)
try {
wallpaperManager.setBitmap(colorBitmap)
} catch (e: IOException) {
throw RuntimeException("设置壁纸失败", e)
}
}
2.5.1 代码解析
WallpaperManager.getInstance(mContext)
用于获取当前设备的壁纸管理器。- 使用
Bitmap
创建一个指定颜色的纯色图像,通过Canvas
绘制设置为设备的壁纸。 - 异常处理部分确保在设置壁纸失败时抛出明确的错误信息,用于调试和问题定位。
2.6 完整代码
项目完整代码整合了手势检测、壁纸切换以及UI展示的功能。
- GestureListener.java:负责手势检测壁纸切换。
package com.nim.wallpaper;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import java.io.IOException;
public class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_THRESHOLD = 50;
private static final int SWIPE_VELOCITY_THRESHOLD = 50;
private static final String TAG = "GestureListener";
private final Context mContext;
private int currentWallpaperIndex = 0;
private final int[] mWallpapers = {
R.color.colorPrimary,
R.color.colorPrimaryDark,
R.color.colorAccent,
R.color.black_overlay,
R.color.black,
R.color.white
};
public GestureListener(Context context) {
mContext = context;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.d(TAG, "onFling:");
try {
float diffX = e2.getX() - e1.getX();
float diffY = e2.getY() - e1.getY();
Log.d(TAG, "x: " diffX ", y: " diffY);
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight();
} else {
onSwipeLeft();
}
return true;
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
return false;
}
private void onSwipeRight() {
Log.d(TAG, "right: " currentWallpaperIndex);
if (currentWallpaperIndex > 0) {
currentWallpaperIndex--;
} else {
currentWallpaperIndex = mWallpapers.length - 1;
}
setWallpaper(mWallpapers[currentWallpaperIndex]);
}
private void onSwipeLeft() {
Log.d(TAG, "left: " currentWallpaperIndex);
if (currentWallpaperIndex < mWallpapers.length - 1) {
currentWallpaperIndex ;
} else {
currentWallpaperIndex = 0;
}
setWallpaper(mWallpapers[currentWallpaperIndex]);
}
private void setWallpaper(int colorResourceId) {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(mContext);
int color = mContext.getResources().getColor(colorResourceId);
int screenWidth = 1920;
int screenHeight = 1080;
Bitmap colorBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(colorBitmap);
canvas.drawColor(color);
try {
wallpaperManager.setBitmap(colorBitmap);
} catch (IOException e) {
throw new RuntimeException("设置壁纸失败", e);
}
}
}
Kotlin版本
代码语言:java复制package com.nim.wallpaper
import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import androidx.core.content.ContextCompat
import java.io.IOException
class GestureListener(private val mContext: Context) : GestureDetector.SimpleOnGestureListener() {
companion object {
private const val SWIPE_THRESHOLD = 50
private const val SWIPE_VELOCITY_THRESHOLD = 50
private const val TAG = "GestureListener"
}
private var currentWallpaperIndex = 0
private val mWallpapers = intArrayOf(
R.color.colorPrimary,
R.color.colorPrimaryDark,
R.color.colorAccent,
R.color.black_overlay,
R.color.black,
R.color.white
)
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
Log.d(TAG, "onFling:")
try {
val diffX = e2!!.x - e1!!.x
val diffY = e2.y - e1.y
Log.d(TAG, "x: $diffX, y: $diffY")
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight()
} else {
onSwipeLeft()
}
return true
}
}
} catch (exception: Exception) {
exception.printStackTrace()
}
return false
}
private fun onSwipeRight() {
Log.d(TAG, "right: $currentWallpaperIndex")
if (currentWallpaperIndex > 0) {
currentWallpaperIndex--
} else {
currentWallpaperIndex = mWallpapers.size - 1
}
setWallpaper(mWallpapers[currentWallpaperIndex])
}
private fun onSwipeLeft() {
Log.d(TAG, "left: $currentWallpaperIndex")
if (currentWallpaperIndex < mWallpapers.size - 1) {
currentWallpaperIndex
} else {
currentWallpaperIndex = 0
}
setWallpaper(mWallpapers[currentWallpaperIndex])
}
private fun setWallpaper(colorResourceId: Int) {
val wallpaperManager = WallpaperManager.getInstance(mContext)
val color = ContextCompat.getColor(mContext, colorResourceId)
val screenWidth = 1920
val screenHeight = 1080
val colorBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(colorBitmap)
canvas.drawColor(color)
try {
wallpaperManager.setBitmap(colorBitmap)
} catch (e: IOException) {
throw RuntimeException("设置壁纸失败", e)
}
}
}
- MainActivity.java:应用主界面,初始化手势监听器和处理用户交互。
package com.nim.wallpaper;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private static final String TAG = "MainActivity";
FrameLayout mMainBg;
private GestureDetector gestureDetector;
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMainBg = findViewById(R.id.main);
gestureDetector = new GestureDetector(this, new GestureListener(this));
mMainBg.setOnTouchListener(this);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "触摸事件 " event.getAction());
if (v.getId() == R.id.main) {
boolean handled = gestureDetector.onTouchEvent(event);
Log.d(TAG, "手势适配类Handled: " handled);
return handled;
}
return false;
}
}
2.6.1 代码解析
gestureDetector
:初始化手势检测器绑定GestureListener
,监听用户在屏幕上的手势操作。setOnTouchListener()
:触摸事件监听器绑定到FrameLayout
,在用户触摸屏幕时能触发手势检测逻辑。onTouch()
:触摸事件传递给GestureDetector
进行处理,决定手势是否被处理。
Kotlin版本
代码语言:java复制package com.nim.wallpaper
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity(), View.OnTouchListener {
companion object {
private const val TAG = "MainActivity"
}
private lateinit var mMainBg: FrameLayout
private lateinit var gestureDetector: GestureDetector
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mMainBg = findViewById(R.id.main)
gestureDetector = GestureDetector(this, GestureListener(this))
mMainBg.setOnTouchListener(this)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
Log.d(TAG, "触摸事件: ${event?.action}")
if (v?.id == R.id.main) {
val handled = gestureDetector.onTouchEvent(event)
Log.d(TAG, "手势适配类Handled: $handled")
return handled
}
return false
}
}
2.7 效果演示
2.8 实战视频
三、技术难点
- 滑动灵敏度调整:
SWIPE_THRESHOLD
和SWIPE_VELOCITY_THRESHOLD
的设定是非常关键,值过低会导致误触,值过高会影响用户体验。 - UI 同步:保证手势切换和UI更新是同步,防止出现手势滑动后壁纸没有即时切换的情况。
- 边界处理:在壁纸数组到达边界时的回环处理逻辑需要确保不会出现数组越界错误。
四、学习技术笔记
4.1 基本概念
GestureDetector
:GestureDetector
是一个用于检测用户手势的工具类,可以识别各种手势操作,如轻触、双击、长按、滑动、快速滑动等。它通过监听用户在屏幕上的触摸事件,根据手势类型调用相应的回调方法。GestureListener
:GestureListener
是GestureDetector
的一个内部类(或接口),通过继承SimpleOnGestureListener
实现需要的手势检测方法。
4.2. 基本流程
- 创建
GestureDetector
实例:在MainActivity
中,创建一个GestureDetector
实例,自定义的GestureListener
传入。 - 绑定触摸事件:通过
View.setOnTouchListener()
触摸事件绑定到GestureDetector
,这样可以触摸事件传递给手势检测器处理。 - 处理手势事件:在
GestureListener
中,根据检测到的手势(如滑动方向、速度)触发相应的逻辑(如壁纸切换)。
五、为啥不能使用ViewPager2实现,和GestureDetector 有什么区别和优势?
在分析产品设计时,为什么选择了 GestureDetector
而不是 ViewPager
实现壁纸切换功能?我详细对比 ViewPager
和 GestureDetector
,分析两者的使用场景和各自的优势,GestureDetector
更适合壁纸切换功能。
特性 | ViewPager/ViewPager2 | GestureDetector |
---|---|---|
主要用途 | 页面滑动切换 | 手势检测响应 |
工作方式 | 基于 Adapter 管理页面Context | 分析触摸事件捕获手势 |
内置功能 | 自动管理页面加载和销毁,支持页面预加载 | 提供多种手势检测(点击、滑动、长按等) |
适用场景 | 标签页切换、图片浏览等 | 壁纸切换、手势导航、图片浏览等 |
方向支持 | 水平滑动(ViewPager),水平和垂直滑动(ViewPager2) | 任意方向手势检测 |
复杂难度 | 简单,封装好 | 灵活,需要手动管理手势逻辑 |
性能 | 内部预加载和销毁机制 | 需要实现手势优化 |
得到结论:ViewPager
和 GestureDetector
各有重点:ViewPager
侧重是多页面的滑动切换,适合场景比较固定;且GestureDetector
是灵活的手势检测工具,适合复杂、个性化的手势响应需求。
六、总结
本项目成功实现了通过手势滑动切换壁纸的功能,为了目的增强了车机应用的个性化体验。未来,计划增加更多的壁纸样式、优化加载速度,提升用户操作时的反馈效果。
有任何问题欢迎提问,感谢大家阅读 )