EhViewer安装开发与实战教程

一、架构解析与技术选型

EhViewer作为开源漫画阅读应用

APK包⬇️

link3.cc/yunyunjie

采用现代化Android开发技术栈构建:

// 核心技术架构示意图

class EhViewerArchitecture {

    // 数据层

    val repository: MangaRepository by lazy {

        DefaultMangaRepository(

            localDataSource = RoomMangaDatabase.getInstance(context),

            remoteDataSource = EhApiService.create()

        )

    }

    // 视图层

    val viewModel: MangaViewModel by viewModels {

        MangaViewModelFactory(repository)

    }

    // 网络层

    val okHttpClient = OkHttpClient.Builder()

        .addInterceptor(HttpLoggingInterceptor())

        .addInterceptor(CacheInterceptor())

        .build()

}

二、环境搭建与源码编译

(一)开发环境初始化脚本

#!/bin/bash

# EhViewer开发环境初始化脚本

# 安装依赖

echo "正在安装开发依赖..."

sudo apt update

sudo apt install -y openjdk-11-jdk android-sdk-build-tools android-sdk-platform-tools

# 配置Android SDK路径

echo "配置Android SDK环境变量..."

echo "export ANDROID_HOME=$HOME/Android/Sdk" >> ~/.bashrc

echo "export PATH=$PATH:$ANDROID_HOME/tools" >> ~/.bashrc

echo "export PATH=$PATH:$ANDROID_HOME/platform-tools" >> ~/.bashrc

source ~/.bashrc

# 安装必要的SDK组件

echo "安装Android SDK组件..."

sdkmanager "platforms;android-33" "build-tools;33.0.2" "sources;android-33"

echo "开发环境初始化完成!"

(二)Gradle构建优化配置

// gradle.properties 优化配置

org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

android.enableBuildCache=true

android.enableR8=true

kotlin.incremental=true

三、核心功能实现解析

(一)漫画源插件系统

// 插件接口定义

interface SourcePlugin {

    val sourceId: String

    val sourceName: String

    val baseUrl: String

    suspend fun fetchLatestManga(page: Int): Flow<List<MangaInfo>>

    suspend fun searchManga(keyword: String, page: Int): Flow<List<MangaInfo>>

    suspend fun getMangaDetails(mangaId: String): Flow<MangaDetail>

    suspend fun getChapterPages(chapterId: String): Flow<List<String>>

    // 源配置接口

    fun getConfigItems(): List<ConfigItem>

}

// 示例:实现本地文件源

class LocalFileSystem : SourcePlugin {

    override val sourceId = "local_file"

    override val sourceName = "本地文件"

    override val baseUrl = "file://"

    // 实现文件扫描逻辑

    private suspend fun scanLocalMangaDir(): List<MangaInfo> {

        return withContext(Dispatchers.IO) {

            val dir = File(Environment.getExternalStoragePublicDirectory(

                Environment.DIRECTORY_PICTURES), "Manga")

            if (!dir.exists() || !dir.isDirectory) return@withContext emptyList()

            dir.listFiles()?.filter { it.isDirectory }?.map { dir ->

                MangaInfo(

                    id = dir.absolutePath,

                    title = dir.name,

                    thumbnailUrl = findCoverImage(dir)?.absolutePath ?: ""

                )

            } ?: emptyList()

        }

    }

    // 其他接口实现...

}

(二)下载管理器实现

// 下载任务状态管理

sealed class DownloadState {

    object Pending : DownloadState()

    data class Downloading(val progress: Int) : DownloadState()

    object Completed : DownloadState()

    data class Failed(val error: Throwable) : DownloadState()

}

// 下载管理器

class DownloadManager(

    private val downloadDao: DownloadDao,

    private val networkService: NetworkService

) {

    private val _downloadStates = mutableMapOf<String, MutableStateFlow<DownloadState>>()

    val downloadStates: Map<String, StateFlow<DownloadState>> = _downloadStates

    suspend fun enqueueDownload(mangaId: String, chapterId: String) {

        withContext(Dispatchers.IO) {

            // 检查是否已下载

            if (downloadDao.isChapterDownloaded(chapterId)) return@withContext

            // 创建下载任务

            val task = DownloadTask(

                id = UUID.randomUUID().toString(),

                mangaId = mangaId,

                chapterId = chapterId,

                state = DownloadState.Pending

            )

            // 保存到数据库

            downloadDao.insertTask(task)

            // 开始下载

            startDownload(task)

        }

    }

    // 并行下载实现

    private suspend fun startDownload(task: DownloadTask) {

        val stateFlow = MutableStateFlow<DownloadState>(DownloadState.Pending)

        _downloadStates[task.id] = stateFlow

        stateFlow.value = DownloadState.Downloading(0)

        try {

            // 获取章节页面

            val pages = networkService.getChapterPages(task.chapterId)

            // 创建下载目录

            val downloadDir = createDownloadDirectory(task.mangaId, task.chapterId)

            // 并行下载页面

            pages.mapIndexed { index, pageUrl ->

                async(Dispatchers.IO) {

                    downloadPage(pageUrl, File(downloadDir, "${index+1}.jpg"))

                    index + 1

                }

            }.awaitAll()

            // 更新状态

            stateFlow.value = DownloadState.Completed

            downloadDao.updateTask(task.copy(state = DownloadState.Completed))

        } catch (e: Exception) {

            stateFlow.value = DownloadState.Failed(e)

            downloadDao.updateTask(task.copy(state = DownloadState.Failed(e)))

        }

    }

    // 其他核心方法...

}

四、UI组件与用户体验优化

(一)自定义阅读器实现

// 阅读器视图组件

class ReaderView @JvmOverloads constructor(

    context: Context,

    attrs: AttributeSet? = null,

    defStyleAttr: Int = 0

) : FrameLayout(context, attrs, defStyleAttr) {

    private val imageView: ZoomableImageView

    private val pageIndicator: TextView

    init {

        // 初始化视图

        LayoutInflater.from(context).inflate(R.layout.view_reader, this, true)

        imageView = findViewById(R.id.reader_image_view)

        pageIndicator = findViewById(R.id.page_indicator)

        // 设置手势监听

        setupGestureListeners()

    }

    // 加载页面

    fun loadPage(imageUrl: String, pageIndex: Int, totalPages: Int) {

        // 更新指示器

        pageIndicator.text = "$pageIndex/$totalPages"

        // 加载图片

        Glide.with(context)

            .load(imageUrl)

            .diskCacheStrategy(DiskCacheStrategy.ALL)

            .listener(object : RequestListener<Drawable> {

                override fun onLoadFailed(

                    e: GlideException?,

                    model: Any?,

                    target: Target<Drawable>?,

                    isFirstResource: Boolean

                ): Boolean {

                    // 处理加载失败

                    return false

                }

                override fun onResourceReady(

                    resource: Drawable?,

                    model: Any?,

                    target: Target<Drawable>?,

                    dataSource: DataSource?,

                    isFirstResource: Boolean

                ): Boolean {

                    // 处理加载成功

                    return false

                }

            })

            .into(imageView)

    }

    // 手势处理

    private fun setupGestureListeners() {

        // 实现翻页手势

        val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {

            override fun onFling(

                e1: MotionEvent,

                e2: MotionEvent,

                velocityX: Float,

                velocityY: Float

            ): Boolean {

                // 检测左右滑动手势

                val deltaX = e2.x - e1.x

                if (deltaX > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {

                    // 左翻页

                    onPageTurnListener?.onPreviousPage()

                    return true

                } else if (deltaX < -SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {

                    // 右翻页

                    onPageTurnListener?.onNextPage()

                    return true

                }

                return false

            }

        })

        // 设置触摸监听

        setOnTouchListener { _, event ->

            gestureDetector.onTouchEvent(event)

            return@setOnTouchListener true

        }

    }

    // 翻页回调接口

    var onPageTurnListener: OnPageTurnListener? = null

    interface OnPageTurnListener {

        fun onPreviousPage()

        fun onNextPage()

    }

    companion object {

        private const val SWIPE_THRESHOLD = 100

        private const val SWIPE_VELOCITY_THRESHOLD = 100

    }

}

(二)主题系统实现

<!-- 主题配置文件 res/values/themes.xml -->

<resources xmlns:tools="http://schemas.android.com/tools">

    <!-- 基础主题 -->

    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">

        <!-- 自定义属性 -->

        <item name="colorPrimary">@color/primary</item>

        <item name="colorPrimaryVariant">@color/primaryVariant</item>

        <item name="colorSecondary">@color/secondary</item>

        <item name="colorSecondaryVariant">@color/secondaryVariant</item>

        <item name="android:statusBarColor">@android:color/transparent</item>

        <item name="android:windowLightStatusBar">true</item>

        <!-- 自定义组件样式 -->

        <item name="toolbarStyle">@style/AppToolbarStyle</item>

        <item name="bottomNavigationStyle">@style/AppBottomNavigationStyle</item>

    </style>

    <!-- 暗黑主题 -->

    <style name="AppTheme.Dark" parent="AppTheme">

        <item name="android:statusBarColor">@android:color/black</item>

        <item name="android:windowLightStatusBar">false</item>

        <item name="colorSurface">@color/dark_surface</item>

        <item name="colorOnSurface">@color/dark_on_surface</item>

    </style>

    <!-- 阅读器主题 -->

    <style name="ReaderTheme" parent="AppTheme">

        <item name="android:windowFullscreen">true</item>

        <item name="android:windowContentOverlay">@null</item>

        <item name="android:windowBackground">@android:color/black</item>

    </style>

</resources>

五、性能优化与问题排查

(一)内存优化工具类

// 内存管理工具类

object MemoryManager {

    private const val MIN_FREE_MEMORY_MB = 100

    // 检查内存状态

    fun checkMemoryStatus(context: Context): Boolean {

        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

        val memoryInfo = ActivityManager.MemoryInfo()

        activityManager.getMemoryInfo(memoryInfo)

        val availableMemoryMb = memoryInfo.availMem / 1024 / 1024

        return availableMemoryMb > MIN_FREE_MEMORY_MB

    }

    // 释放图片缓存

    fun clearImageCache(context: Context) {

        Thread {

            // 清除Glide内存缓存

            Glide.get(context).clearMemory()

            // 清除Glide磁盘缓存(需在后台线程执行)

            Glide.get(context).clearDiskCache()

        }.start()

    }

    // 优化图片加载

    fun optimizeImageLoading(options: RequestOptions): RequestOptions {

        return options

            .format(DecodeFormat.PREFER_RGB_565) // 降低内存占用

            .override(Target.SIZE_ORIGINAL / 2) // 加载缩放后的图片

            .diskCacheStrategy(DiskCacheStrategy.ALL) // 启用磁盘缓存

    }

}

(二)网络请求拦截器

// 网络请求拦截器

class NetworkInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val originalRequest = chain.request()

        // 添加User-Agent

        val requestWithUserAgent = originalRequest.newBuilder()

            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")

            .build()

        // 执行请求

        val response = chain.proceed(requestWithUserAgent)

        // 缓存控制

        if (response.isSuccessful) {

            val cacheControl = CacheControl.Builder()

                .maxAge(1, TimeUnit.DAYS) // 缓存1天

                .build()

            return response.newBuilder()

                .header("Cache-Control", cacheControl.toString())

                .build()

        }

        return response

    }

}

六、扩展与贡献指南

(一)创建自定义插件

// 自定义插件示例

class MyCustomSource : SourcePlugin {

    override val sourceId = "my_custom_source"

    override val sourceName = "我的自定义源"

    override val baseUrl = "https://mycustomsource.example.com"

    // 实现接口方法

    override suspend fun fetchLatestManga(page: Int): Flow<List<MangaInfo>> = flow {

        // 模拟网络请求

        delay(500)

        // 返回示例数据

        emit(

            listOf(

                MangaInfo(

                    id = "1",

                    title = "示例漫画1",

                    thumbnailUrl = "https://picsum.photos/200/300?random=1",

                    author = "作者1",

                    status = "连载中"

                ),

                MangaInfo(

                    id = "2",

                    title = "示例漫画2",

                    thumbnailUrl = "https://picsum.photos/200/300?random=2",

                    author = "作者2",

                    status = "已完结"

                )

            )

        )

    }

    // 其他方法实现...

}

// 插件注册

class MyPluginProvider : PluginProvider {

    override fun getSourcePlugins(): List<SourcePlugin> {

        return listOf(MyCustomSource())

    }

}

(二)提交贡献流程

# 贡献代码流程指南

# 1. 克隆仓库

git clone https://github.com/ehviewer/ehviewer.git

cd ehviewer

# 2. 创建特性分支

git checkout -b feature/add-new-source

# 3. 开发并测试你的功能

# ...

# 4. 提交代码

git add .

git commit -m "feat: add new custom source plugin"

# 5. 同步上游更新

git remote add upstream https://github.com/ehviewer/ehviewer.git

git fetch upstream

git rebase upstream/stable

# 6. 推送分支

git push origin feature/add-new-source

# 7. 创建Pull Request

# 在GitHub上提交PR并等待审核

本指南详细介绍了EhViewer的技术架构、开发环境搭建、核心功能实现以及性能优化方法。通过阅读和实践,你可以深入理解这款开源应用的设计理念,并根据自己的需求进行定制开发。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值