first commit

This commit is contained in:
ocean 2026-01-04 15:23:26 +08:00
commit a076484056
120 changed files with 69659 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
WallpaperGallery

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

440
.idea/dbnavigator.xml generated Normal file
View File

@ -0,0 +1,440 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DDLFileAttachmentManager">
<mappings />
<preferences />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
<enable-sticky-paths value="true" />
<enable-quick-filters value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON_VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="JAVA_RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="JAVA CLASS" enabled="true" />
<object-type name="INNER CLASS" enabled="true" />
<object-type name="JAVA FIELD" enabled="true" />
<object-type name="JAVA METHOD" enabled="true" />
<object-type name="JAVA PARAMETER" enabled="true" />
<object-type name="JAVA RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="false" />
<object-type name="CREDENTIAL" enabled="false" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="Java" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="Groovy" enabled="true" />
<content-type name="AIDL" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Manifest" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
<exit-on-changes value="ASK" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="json view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="json view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
<mapping file-type-id="JAVA_SOURCE" extensions="sql" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<assistant-settings>
<credential-settings>
<credentials />
</credential-settings>
</assistant-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

10
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

BIN
app/Wallpaper Gallery Normal file

Binary file not shown.

166
app/build.gradle.kts Normal file
View File

@ -0,0 +1,166 @@
import java.text.SimpleDateFormat
import java.util.Date
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
val timestamp = SimpleDateFormat("MM_dd_HH_mm").format(Date())
android {
namespace = "com.gallery.free.wallpaper"
compileSdk = 36
defaultConfig {
applicationId = "com.gallery.free.wallpaper"
minSdk = 24
targetSdk = 36
versionCode = 2
versionName = "1.1"
setProperty("archivesBaseName", "WallpaperGallery_V" + versionName + "(${versionCode})_$timestamp")
testInstrumentationRunner = "androidx.live.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures{
viewBinding = true
}
}
dependencies {
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.viewpager2:viewpager2:1.0.0")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.recyclerview:recyclerview:1.3.0")
implementation("com.google.code.gson:gson:2.8.9")
implementation ("com.github.bumptech.glide:glide:5.0.5")
implementation ("jp.wasabeef:glide-transformations:4.3.0") // Glide Transformations
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-process:2.6.2")// 添加生命周期扩展
implementation("androidx.lifecycle:lifecycle-common-java8:2.6.2")
implementation("androidx.work:work-runtime-ktx:2.8.1")// 添加 WorkManager用于后台任务
// 添加协程依赖
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.ui.graphics)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(files("libs/TradPlusLibrary_01_04_12_20-release.aar"))
implementation(files("libs/UpLoadLibrary_12_03_15_13-release.aar"))
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
implementation("com.google.android.gms:play-services-location:21.0.1")
implementation("com.google.android.gms:play-services-appset:16.0.1")
// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.6.0"))
implementation("com.google.firebase:firebase-crashlytics-ndk")
implementation("com.google.firebase:firebase-analytics")
// okhttp
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// TradPlus
implementation("com.tradplusad:tradplus:15.2.0.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.appcompat:appcompat:1.3.0-alpha02")
// IronSource
implementation("com.ironsource.sdk:mediationsdk:9.0.0")
implementation("com.tradplusad:tradplus-ironsource:10.15.2.0.1")
// Pangle
implementation("com.tradplusad:tradplus-pangle:19.15.2.0.1")
implementation("com.pangle.global:pag-sdk:7.8.0.7")
// UnityAds
implementation("com.tradplusad:tradplus-unity:5.15.2.0.1")
implementation("com.unity3d.ads:unity-ads:4.16.3")
// Chartboost
implementation("com.tradplusad:tradplus-chartboostx:15.15.2.0.1")
implementation("com.chartboost:chartboost-sdk:9.10.0")
implementation("com.google.android.gms:play-services-ads-identifier:17.0.0")
implementation("com.google.android.gms:play-services-base:17.4.0")
//上面新版本下载失败用旧版本
// implementation("com.tradplusad:tradplus-chartboostx:15.14.5.0.1")
// implementation("com.chartboost:chartboost-sdk:9.8.3")
// implementation("com.google.android.gms:play-services-ads-identifier:17.0.0")
// implementation("com.google.android.gms:play-services-base:17.4.0")
// InMobi
implementation("com.tradplusad:tradplus-inmobix:23.15.2.0.1")
implementation("com.inmobi.monetization:inmobi-ads-kotlin:11.0.0")
implementation("com.squareup.okhttp3:okhttp:3.14.9")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.core:core-ktx:1.5.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0")
implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
implementation("com.google.android.gms:play-services-location:21.0.1") // optional
implementation("androidx.browser:browser:1.8.0")
implementation("com.squareup.picasso:picasso:2.8")
implementation("androidx.viewpager:viewpager:1.0.0")
implementation("androidx.recyclerview:recyclerview:1.2.1")
// Fyber
implementation("com.fyber:marketplace-sdk:8.4.0")
implementation("com.tradplusad:tradplus-fyber:24.15.2.0.1")
implementation("com.google.android.gms:play-services-ads-identifier:17.0.0")
implementation("com.google.android.gms:play-services-base:17.4.0")
// Mintegral
implementation("com.tradplusad:tradplus-mintegralx_overseas:18.15.2.0.1")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("com.mbridge.msdk.oversea:mbridge_android_sdk:16.10.11")
// Liftoff (Vungle)
implementation("com.tradplusad:tradplus-vunglex:7.15.2.0.1")
implementation("com.vungle:vungle-ads:7.6.0")
// Bigo
implementation("com.bigossp:bigo-ads:5.5.2")
implementation("com.tradplusad:tradplus-bigo:57.15.2.0.1")
// Cross Promotion
implementation("com.tradplusad:tradplus-crosspromotion:27.15.2.0.1")
// TP Exchange注意与主包版本同步
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.tradplusad:tp_exchange:40.15.2.0.1")
// Google UMP
implementation ("com.google.android.ump:user-messaging-platform:3.2.0")
}

29
app/google-services.json Normal file
View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "141099185558",
"project_id": "wallpaper-gallery-6ac52",
"storage_bucket": "wallpaper-gallery-6ac52.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:141099185558:android:53c2a086efb56b3e8159b7",
"android_client_info": {
"package_name": "com.gallery.free.wallpaper"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyB_9h8FLSf3Uv-_5JMlkhcm-Kmuwv84jhk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

Binary file not shown.

Binary file not shown.

35
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,35 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class com.gallery.free.wallpaper.WallpaperJson { *; }
-keep class com.gallery.free.wallpaper.JsonUtils { *; }
-keep class com.gallery.free.wallpaper.Links { *; }
-keep class com.gallery.free.wallpaper.Urls { *; }
-keep class com.gallery.free.wallpaper.User { *; }
-keep class com.gallery.free.wallpaper.WallpaperItem { *; }
-keep class com.google.gson.** { *; }
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keepattributes Signature
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations
-keep public class com.tradplus.** { *; }
-keep class com.tradplus.ads.** { *; }

View File

@ -0,0 +1,24 @@
package gallery.free.wallpaper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.gallery.free.wallpaper", appContext.packageName)
}
}

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 针对 Android 13+ 的媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.AD_ID" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:allowNativeHeapPointerTagging="false"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:persistableMode="persistAcrossReboots"
android:requestLegacyExternalStorage="true"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:supportsRtl="true"
tools:replace="networkSecurityConfig"
android:networkSecurityConfig="@xml/net"
android:taskAffinity=""
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
tools:targetApi="30">
<meta-data
android:name="com.startapp.sdk.MIXED_AUDIENCE"
android:value="true"/>
<activity
android:name=".LocalPreActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.NoActionBar" />
<activity
android:name=".DetailwpActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity
android:name=".SplaActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
</application>
</manifest>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@font-face {
font-family: 'AlimamaShuheiti';
src: url('file:///android_asset/fonts/alimama_shuheiti.ttf');
}
body {
font-family: 'AlimamaShuheiti', sans-serif;
line-height: 1.5;
padding: 0 20px 20px 20px;
max-width: 800px;
margin: 0 auto;
background-color: transparent;
}
h2 {
color: #555555;
margin-top: 24px;
}
.last-updated {
color: #666666;
font-style: italic;
text-align: center;
}
ul { padding-left: 20px; }
li {
margin-bottom: 7px;
color: #555555;
}
p {
color: #555555;
}
strong {
color: #3498db;
}
</style>
</head>
<body>
<p class="last-updated">Last Updated: November 27, 2025</p>
<h2>1. Information Collection</h2>
<p>Our wallpaper application respects your privacy. We commit to:</p>
<ul>
<li>Not collecting any personally identifiable information</li>
<li>Not requiring user registration or login</li>
<li>Not tracking user browsing habits</li>
<li>Not accessing your personal files or data</li>
</ul>
<h2>2. Wallpaper Download and Storage</h2>
<p>When you download wallpapers:</p>
<ul>
<li>Wallpapers are saved only in your device's local storage</li>
<li>We cannot access the wallpapers you download</li>
<li>Wallpapers are not uploaded to our servers</li>
<li>You can delete downloaded wallpapers at any time</li>
</ul>
<h2>3. Permission Explanations</h2>
<p>Required app permissions and their purposes:</p>
<ul>
<li><strong>Storage Permission</strong>: For saving downloaded wallpapers to your photo gallery</li>
<li><strong>Network Permission</strong>: For loading and displaying wallpaper resources</li>
</ul>
<h2>4. Third-Party Services</h2>
<p>We may use the following third-party services:</p>
<ul>
<li><strong>Analytics</strong>: For app usage statistics (anonymous data)</li>
<li><strong>Advertising Services</strong>: If the app contains ads, relevant advertisements may be displayed</li>
</ul>
<h2>5. Data Security</h2>
<p>We take reasonable security measures to protect your information.</p>
<h2>6. Children's Privacy</h2>
<p>Our services are not directed to children under 13, and we do not knowingly collect personal information from children.</p>
<h2>7. Contact Us</h2>
<p>If you have any questions about this Privacy Policy, please contact us:</p>
<p>Email: xxx@xxx.com</p>
<h2>8. Policy Changes</h2>
<p>We may update this Privacy Policy from time to time. The updated version will be posted on this page.</p>
</body>
</html>

View File

@ -0,0 +1,36 @@
package com.gallery.free.wallpaper
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity: AppCompatActivity() {
protected var backPressedCallback: OnBackPressedCallback? = null
/** 子类是否需要拦截返回 */
protected open fun shouldInterceptBackPress(): Boolean = false
/** 子类定义拦截后的操作(例如弹窗) */
protected open fun onInterceptBackPressed() {}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupBackPressedCallback()//初始化back事件
}
private fun setupBackPressedCallback() {
backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (shouldInterceptBackPress()) {
// 由子类处理拦截动作
onInterceptBackPressed()
} else {
// 不拦截:关闭自己
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
}
onBackPressedDispatcher.addCallback(this, backPressedCallback!!)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
package com.gallery.free.wallpaper
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
class Dialog_SetWp : DialogFragment() {
companion object {
const val TYPE_HOME = 1
const val TYPE_LOCK = 2
const val TYPE_BOTH = 3
}
interface OnWallpaperTypeSelectedListener {
fun onWallpaperTypeSelected(type: Int)
}
private var listener: OnWallpaperTypeSelectedListener? = null
fun setListener(listener: OnWallpaperTypeSelectedListener) {
this.listener = listener
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_setwp, container, false)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
// 设置对话框样式
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<View>(R.id.layout_home).setOnClickListener {
listener?.onWallpaperTypeSelected(TYPE_HOME)
dismiss()
}
view.findViewById<View>(R.id.layout_lock).setOnClickListener {
listener?.onWallpaperTypeSelected(TYPE_LOCK)
dismiss()
}
view.findViewById<View>(R.id.layout_both).setOnClickListener {
listener?.onWallpaperTypeSelected(TYPE_BOTH)
dismiss()
}
}
}

View File

@ -0,0 +1,40 @@
package com.gallery.free.wallpaper
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class GridItemDecoration(
private val spanCount: Int,
private val spacing: Int,
private val includeEdge: Boolean = true
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val column = position % spanCount
if (includeEdge) {
// 计算左右间距,两列宽度一致
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
// 上下间距保持一致
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing
} else {
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
if (position >= spanCount) {
outRect.top = spacing
}
}
}
}

View File

@ -0,0 +1,63 @@
package com.gallery.free.wallpaper
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.gallery.free.wallpaper.fragment.WpGridFragment
class HoPagerAdapter(
fragment: Fragment,
private val categories: List<String> = listOf("Film", "Nature", "Travel")
) : FragmentStateAdapter(fragment) {
companion object {
private const val TAG = "HomePagerAdapter"
}
private val fragments = mutableMapOf<Int, WpGridFragment>()
override fun getItemCount(): Int = categories.size
override fun createFragment(position: Int): Fragment {
val categoryName = categories[position]
val fragment = WpGridFragment.newInstance(categoryName)
fragments[position] = fragment
Log.d(TAG, "创建Fragment - 位置: $position, 分类: $categoryName")
return fragment
}
fun getTabTitle(position: Int): String {
return if (position in categories.indices) {
categories[position]
} else {
""
}
}
fun getFragment(position: Int): WpGridFragment? {
return fragments[position]
}
/**
* 更新分类列表
*/
fun updateCategories(newCategories: List<String>) {
Log.d(TAG, "更新分类列表: $newCategories")
}
/**
* 获取所有 Fragment
*/
fun getAllFragments(): List<WpGridFragment> {
return fragments.values.toList()
}
/**
* 清除 Fragment 缓存
*/
fun clearFragmentCache() {
fragments.clear()
Log.d(TAG, "已清除Fragment缓存")
}
}

View File

@ -0,0 +1,624 @@
package com.gallery.free.wallpaper
import androidx.activity.OnBackPressedCallback
import android.app.WallpaperManager
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.gallery.free.wallpaper.databinding.ActivityLocalpreBinding
import java.lang.ref.WeakReference
class LocalPreActivity : AppCompatActivity(),
Dialog_SetWp.OnWallpaperTypeSelectedListener {
private lateinit var binding: ActivityLocalpreBinding
private lateinit var wallpaperManager: WallpaperManager
private var setWallpaperDialog: Dialog_SetWp? = null
private var isSettingWallpaper = false
private var currentWallpaperBitmap: Bitmap? = null
private var imageUri: Uri? = null
private var screenWidth = 0
private var screenHeight = 0
private var isLaunchedFromSetting = false
private var hasShownSuccessToast = false
private var isInGallerySelection = false
companion object {
private const val TAG = "LocalImagePreview"
private const val SET_WALLPAPER_STATE_IDLE = 0
private const val SET_WALLPAPER_STATE_LOADING = 1
private const val SET_WALLPAPER_STATE_SUCCESS = 2
private const val SET_WALLPAPER_STATE_FAILED = 3
private const val DELAY_BEFORE_RETURN = 800L
private const val REQUEST_GALLERY_AGAIN = 1001
const val EXTRA_IMAGE_URI = "selected_image_uri"
const val EXTRA_FROM_SETTING = "from_setting"
const val RESULT_SET_SUCCESS = 200
}
private var setWallpaperState = SET_WALLPAPER_STATE_IDLE
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Utils_com.initFull(this, true)
setTheme(R.style.TransparentPreviewTheme)
overridePendingTransition(0, 0)
binding = ActivityLocalpreBinding.inflate(layoutInflater)
setContentView(binding.root)
overridePendingTransition(0, 0)
binding = ActivityLocalpreBinding.inflate(layoutInflater)
setContentView(binding.root)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleBackButtonClick()
}
})
Log.d(TAG, "=== LocalImagePreviewActivity_onCreate ===")
Log.d(TAG, "Activity实例: ${this.hashCode()}")
showLoading(true)
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
screenWidth = displayMetrics.widthPixels
screenHeight = displayMetrics.heightPixels
Log.d(TAG, "Screen size: $screenWidth x $screenHeight")
wallpaperManager = WallpaperManager.getInstance(this)
// 获取图片URI
val intentUri = intent.getStringExtra(EXTRA_IMAGE_URI)?.let { Uri.parse(it) }
if (intentUri != null) {
imageUri = intentUri
Log.d(TAG, "从Intent获取图片URI: $imageUri")
}
isLaunchedFromSetting = intent.getBooleanExtra(EXTRA_FROM_SETTING, true)
Log.d(TAG, "是否从设置页面启动: $isLaunchedFromSetting")
if (imageUri == null) {
Toast.makeText(this, "No image selected", Toast.LENGTH_SHORT).show()
showLoading(false)
finish()
return
}
// 检查是否是重新创建的情况
if (savedInstanceState != null) {
Log.d(TAG, "Activity被重新创建")
setWallpaperState = savedInstanceState.getInt("setWallpaperState", SET_WALLPAPER_STATE_IDLE)
isLaunchedFromSetting = savedInstanceState.getBoolean("isLaunchedFromSetting", true)
isInGallerySelection = savedInstanceState.getBoolean("isInGallerySelection", false)
updateSetWallpaperButtonState()
}
hideSystemUI()
setupViews()
loadImage()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("setWallpaperState", setWallpaperState)
outState.putBoolean("isLaunchedFromSetting", isLaunchedFromSetting)
outState.putBoolean("isInGallerySelection", isInGallerySelection)
Log.d(TAG, "保存状态: setWallpaperState=$setWallpaperState, isLaunchedFromSetting=$isLaunchedFromSetting, isInGallerySelection=$isInGallerySelection")
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent被调用")
// 当Activity已经存在时更新URI
val newUri = intent?.getStringExtra(EXTRA_IMAGE_URI)?.let { Uri.parse(it) }
if (newUri != null && newUri != imageUri) {
imageUri = newUri
currentWallpaperBitmap = null
setWallpaperState = SET_WALLPAPER_STATE_IDLE
// 检查是否从设置页面启动
isLaunchedFromSetting = intent.getBooleanExtra(EXTRA_FROM_SETTING, false)
Log.d(TAG, "更新图片URI: $imageUri, 是否从设置页面启动: $isLaunchedFromSetting")
loadImage()
updateSetWallpaperButtonState()
}
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.let { controller ->
controller.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
controller.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}
}
private fun setupViews() {
Log.d(TAG, "setupViews")
// 返回按钮
binding.buttonBack.setOnClickListener {
Log.d(TAG, "Back button clicked")
handleBackButtonClick()
}
// 设置壁纸按钮
binding.buttonSetWallpaper.setOnClickListener {
Log.d(TAG, "Set wallpaper button clicked")
if (setWallpaperState != SET_WALLPAPER_STATE_LOADING) {
showSetWallpaperDialog()
}
}
updateSetWallpaperButtonState()
}
private fun updateSetWallpaperButtonState() {
Log.d(TAG, "更新按钮状态: $setWallpaperState")
when (setWallpaperState) {
SET_WALLPAPER_STATE_IDLE -> {
binding.buttonSetWallpaper.setImageResource(R.drawable.ic_setwp)
binding.buttonSetWallpaper.isEnabled = true
binding.buttonBack.isEnabled = true
}
SET_WALLPAPER_STATE_LOADING -> {
binding.buttonSetWallpaper.setImageResource(R.drawable.loading_animation)
binding.buttonSetWallpaper.isEnabled = false
binding.buttonBack.isEnabled = false
}
SET_WALLPAPER_STATE_SUCCESS -> {
binding.buttonSetWallpaper.setImageResource(R.drawable.ic_success)
binding.buttonSetWallpaper.isEnabled = false
binding.buttonBack.isEnabled = false
if (!hasShownSuccessToast) {
Toast.makeText(this, "setting successfully!", Toast.LENGTH_SHORT).show()
hasShownSuccessToast = true
}
setResult(RESULT_SET_SUCCESS)
handler.postDelayed({
returnToSettingFragment()
}, DELAY_BEFORE_RETURN)
}
SET_WALLPAPER_STATE_FAILED -> {
binding.buttonSetWallpaper.setImageResource(R.drawable.close_detailwp)
binding.buttonSetWallpaper.isEnabled = true
binding.buttonBack.isEnabled = true
// 设置失败显示Toast
Toast.makeText(this, "setting failed...", Toast.LENGTH_SHORT).show()
handler.postDelayed({
setWallpaperState = SET_WALLPAPER_STATE_IDLE
updateSetWallpaperButtonState()
}, DELAY_BEFORE_RETURN)
}
}
}
private fun returnToSettingFragment() {
try {
Log.d(TAG, "返回到SettingFragmentisLaunchedFromSetting=$isLaunchedFromSetting")
if (isLaunchedFromSetting) {
val settingIntent = Intent(this, MainActivity::class.java).apply {
putExtra("SHOW_SETTING_FRAGMENT", true)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(settingIntent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
Log.d(TAG, "已启动MainActivity显示SettingFragment")
} else {
Log.d(TAG, "不是从设置页面启动,直接结束")
finish()
}
} catch (e: Exception) {
Log.e(TAG, "返回SettingFragment失败: ${e.message}")
finish() // 如果失败,至少正常结束
}
}
private var isFinishingByUser = false
private var isProcessingBack = false
private fun handleBackButtonClick() {
// 如果正在设置中,禁止返回
if (setWallpaperState == SET_WALLPAPER_STATE_LOADING ||
setWallpaperState == SET_WALLPAPER_STATE_SUCCESS
) {
return
}
// 防止重复点击
if (isProcessingBack) return
isProcessingBack = true
// 标记为用户主动关闭
isFinishingByUser = true
Toast.makeText(this, "setting cancelled...", Toast.LENGTH_SHORT).show()
currentWallpaperBitmap = null
handler.postDelayed({
try {
if (isInGallerySelection) {
// 如果用户正在重新选择图片(已经在图库中),返回到设置页面
Log.d(TAG, "用户在图库中点击返回,返回到设置页面")
returnToSettingFragment()
} else {
// 用户在预览页面点击返回,重新打开图库
Log.d(TAG, "用户在预览页面点击返回,重新打开图库")
isInGallerySelection = true // 标记为正在重新选择
openGalleryForNewImage()
}
} catch (e: Exception) {
e.printStackTrace()
returnToSettingFragment() // 如果出错,也返回到设置页面
} finally {
isProcessingBack = false
isFinishingByUser = false
}
}, 300)
}
private fun openGalleryForNewImage() {
try {
val galleryIntent = Intent(Intent.ACTION_PICK)
galleryIntent.type = "image/*"
galleryIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
startActivityForResult(galleryIntent, REQUEST_GALLERY_AGAIN)
Log.d(TAG, "打开图库选择新图片")
} catch (e: Exception) {
Log.e(TAG, "打开图库失败: ${e.message}")
Toast.makeText(this, "无法打开图库", Toast.LENGTH_SHORT).show()
returnToSettingFragment() // 失败时返回到设置页面
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, isLaunchedFromSetting=$isLaunchedFromSetting, isInGallerySelection=$isInGallerySelection")
when (requestCode) {
REQUEST_GALLERY_AGAIN -> {
// 重置状态
isInGallerySelection = false
if (resultCode == android.app.Activity.RESULT_OK && data != null) {
val uri = data.data
if (uri != null) {
// 用户重新选择了图片
Log.d(TAG, "用户重新选择了图片: $uri")
// 启动新的预览页面
val newIntent = Intent(this, LocalPreActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, uri.toString())
putExtra(EXTRA_FROM_SETTING, isLaunchedFromSetting)
}
// 结束当前预览页面
finish()
// 延迟启动新预览页面
handler.postDelayed({
startActivity(newIntent)
overridePendingTransition(0, 0)
}, 100)
} else {
// 没有获取到图片,返回到设置页面
Log.d(TAG, "图库返回但没有选择图片,返回到设置页面")
returnToSettingFragment()
}
} else {
// 用户取消了图库选择,返回到设置页面
Log.d(TAG, "用户取消了图库选择,返回到设置页面")
returnToSettingFragment()
}
}
}
}
private fun loadImage() {
Log.d(TAG, "开始加载图片: $imageUri")
// 先显示加载状态
showLoading(true)
// 重置状态
setWallpaperState = SET_WALLPAPER_STATE_IDLE
updateSetWallpaperButtonState()
// 加载预览图
Glide.with(this)
.load(imageUri)
.into(binding.imageViewPreview)
// 加载Bitmap供设置壁纸使用
Glide.with(this)
.asBitmap()
.load(imageUri)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
currentWallpaperBitmap = resource
showLoading(false)
Log.d(TAG, "图片加载成功,大小: ${resource.width}x${resource.height}")
}
override fun onLoadCleared(placeholder: Drawable?) {
showLoading(false)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
showLoading(false)
Toast.makeText(
this@LocalPreActivity,
"Failed to load image",
Toast.LENGTH_SHORT
).show()
Log.e(TAG, "图片加载失败: $imageUri")
// 根据启动源返回不同的结果
if (isLaunchedFromSetting) {
setResult(android.app.Activity.RESULT_CANCELED)
}
finish()
}
})
}
private fun showLoading(show: Boolean) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
}
private fun showSetWallpaperDialog() {
if (setWallpaperDialog == null) {
setWallpaperDialog = Dialog_SetWp()
setWallpaperDialog?.setListener(this)
}
if (setWallpaperDialog?.isAdded != true) {
setWallpaperDialog?.show(supportFragmentManager, "SetWallpaperDialog")
}
}
override fun onWallpaperTypeSelected(type: Int) {
setWallpaper(type)
}
private fun setWallpaper(type: Int) {
if (isSettingWallpaper || setWallpaperState == SET_WALLPAPER_STATE_SUCCESS) return
Log.d(TAG, "开始设置壁纸,类型: $type, 是否从设置页面启动: $isLaunchedFromSetting")
isSettingWallpaper = true
setWallpaperState = SET_WALLPAPER_STATE_LOADING
updateSetWallpaperButtonState()
if (currentWallpaperBitmap != null) {
SetWallpaperTask(this, type, currentWallpaperBitmap!!).execute()
return
}
// 如果Bitmap还没缓存重新加载
Glide.with(this)
.asBitmap()
.load(imageUri)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
currentWallpaperBitmap = resource
SetWallpaperTask(this@LocalPreActivity, type, resource).execute()
}
override fun onLoadCleared(placeholder: Drawable?) {
// 清理资源
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
isSettingWallpaper = false
setWallpaperState = SET_WALLPAPER_STATE_FAILED
updateSetWallpaperButtonState()
}
})
}
private class SetWallpaperTask(
activity: LocalPreActivity,
private val type: Int,
private val bitmap: Bitmap
) : AsyncTask<Void, Void, Boolean>() {
private val activityRef = WeakReference(activity)
private fun createCenteredWallpaperBitmap(
originalBitmap: Bitmap,
screenWidth: Int,
screenHeight: Int
): Bitmap {
val bitmapWidth = originalBitmap.width.toFloat()
val bitmapHeight = originalBitmap.height.toFloat()
// 计算缩放比例,使图片至少能覆盖整个屏幕
val scaleX = screenWidth / bitmapWidth
val scaleY = screenHeight / bitmapHeight
val scale = maxOf(scaleX, scaleY)
// 创建缩放后的图片
val matrix = Matrix()
matrix.postScale(scale, scale)
val scaledBitmap = Bitmap.createBitmap(
originalBitmap,
0, 0,
originalBitmap.width, originalBitmap.height,
matrix, true
)
// 从缩放后的图片中裁剪出居中区域
val scaledWidth = scaledBitmap.width
val scaledHeight = scaledBitmap.height
// 计算裁剪的起始点(居中)
val startX = (scaledWidth - screenWidth) / 2
val startY = (scaledHeight - screenHeight) / 2
// 确保裁剪区域在图片范围内
val safeStartX = maxOf(0, startX)
val safeStartY = maxOf(0, startY)
val safeEndX = minOf(safeStartX + screenWidth, scaledWidth)
val safeEndY = minOf(safeStartY + screenHeight, scaledHeight)
val safeWidth = safeEndX - safeStartX
val safeHeight = safeEndY - safeStartY
// 创建最终居中裁剪的图片
return Bitmap.createBitmap(
scaledBitmap,
safeStartX, safeStartY,
safeWidth, safeHeight
)
}
override fun doInBackground(vararg params: Void?): Boolean {
val activity = activityRef.get() ?: return false
return try {
Log.d("SetWallpaperTask", "开始处理壁纸图片")
// 创建居中裁剪的壁纸
val centeredBitmap = createCenteredWallpaperBitmap(
bitmap, // 注意这里的bitmap是Glide管理的不要回收它
activity.screenWidth,
activity.screenHeight
)
Log.d("SetWallpaperTask", "裁剪后图片大小: ${centeredBitmap.width}x${centeredBitmap.height}")
// 根据选择的类型设置壁纸
when (type) {
Dialog_SetWp.TYPE_HOME -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.wallpaperManager.setBitmap(
centeredBitmap,
null,
true,
WallpaperManager.FLAG_SYSTEM
)
} else {
activity.wallpaperManager.setBitmap(centeredBitmap)
}
Log.d("SetWallpaperTask", "设置主屏幕壁纸")
}
Dialog_SetWp.TYPE_LOCK -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.wallpaperManager.setBitmap(
centeredBitmap,
null,
true,
WallpaperManager.FLAG_LOCK
)
} else {
activity.wallpaperManager.setBitmap(centeredBitmap)
}
Log.d("SetWallpaperTask", "设置锁屏壁纸")
}
Dialog_SetWp.TYPE_BOTH -> {
activity.wallpaperManager.setBitmap(centeredBitmap)
Log.d("SetWallpaperTask", "设置主屏幕和锁屏壁纸")
}
}
// 关键修复点2只回收自己创建的centeredBitmap
// 不回收传入的bitmap因为它是Glide管理的
centeredBitmap.recycle()
true
} catch (e: Exception) {
Log.e("SetWallpaperTask", "设置壁纸失败: ${e.message}", e)
false
}
}
override fun onPostExecute(success: Boolean) {
val activity = activityRef.get() ?: return
activity.isSettingWallpaper = false
if (success) {
// 设置成功
activity.setWallpaperState = SET_WALLPAPER_STATE_SUCCESS
Log.d("SetWallpaperTask", "壁纸设置成功,启动源: ${activity.isLaunchedFromSetting}")
} else {
// 设置失败
activity.setWallpaperState = SET_WALLPAPER_STATE_FAILED
Log.d("SetWallpaperTask", "壁纸设置失败")
}
activity.updateSetWallpaperButtonState()
}
}
override fun finish() {
super.finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Activity销毁: ${this.hashCode()}, 是否从设置页面启动: $isLaunchedFromSetting")
isSettingWallpaper = false
if (!isDestroyed && !isFinishing) {
Glide.with(this).clear(binding.imageViewPreview)
} else {
Log.w(TAG, "Activity已销毁或正在结束跳过Glide清理")
}
currentWallpaperBitmap = null
handler.removeCallbacksAndMessages(null)
}
}

View File

@ -0,0 +1,230 @@
package com.gallery.free.wallpaper
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.gallery.free.wallpaper.databinding.ActivityMainBinding
import com.gallery.free.wallpaper.fragment.ColleFragment
import com.gallery.free.wallpaper.fragment.HoFragment
import com.gallery.free.wallpaper.fragment.SeFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
private lateinit var vb: ActivityMainBinding
// 保存 Fragment 实例和状态
private var homeFragment: HoFragment? = null
private var collectionFragment: ColleFragment? = null
private var settingFragment: SeFragment? = null
private var currentFragmentTag: String = "home"
// 添加协程作用域
private val activityScope = CoroutineScope(Dispatchers.Main + Job())
companion object {
private const val KEY_CURRENT_FRAGMENT = "current_fragment"
private const val TAG_HOME = "home"
private const val TAG_COLLECTION = "collection"
private const val TAG_SETTING = "setting"
const val PERMISSION_GALLERY_REQUEST_CODE = 1002
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vb = ActivityMainBinding.inflate(layoutInflater)
Utils_com.initFull(this, true)
setContentView(vb.root)
// 初始化 Application 数据
initApplicationData()
// 恢复状态
if (savedInstanceState != null) {
currentFragmentTag = savedInstanceState.getString(KEY_CURRENT_FRAGMENT, TAG_HOME)
// 恢复 Fragment 实例
homeFragment = supportFragmentManager.findFragmentByTag(TAG_HOME) as? HoFragment
collectionFragment = supportFragmentManager.findFragmentByTag(TAG_COLLECTION) as? ColleFragment
settingFragment = supportFragmentManager.findFragmentByTag(TAG_SETTING) as? SeFragment
}
setupBottomNavigation()
checkIfShouldShowSettingFragment()
// 显示初始 Fragment
if (savedInstanceState == null) {
showFragment(currentFragmentTag)
} else {
showFragment(currentFragmentTag)
}
}
private fun checkIfShouldShowSettingFragment() {
intent?.let { intent ->
if (intent.hasExtra("SHOW_SETTING_FRAGMENT")) {
Log.d("MainActivity", "接收到SHOW_SETTING_FRAGMENT标志显示SettingFragment")
currentFragmentTag = TAG_SETTING
updateBottomNavigation(2) // 选中设置按钮
intent.removeExtra("SHOW_SETTING_FRAGMENT") // 清除标志
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.d("MainActivity", "onNewIntent被调用")
// ✅ 处理从预览页面返回的情况
this.intent = intent
checkIfShouldShowSettingFragment()
}
/**
* 初始化 Application 数据
*/
private fun initApplicationData() {
activityScope.launch {
try {
withContext(Dispatchers.IO) {
}
// 获取 Application 实例
val myApp = application as? MyApplication
myApp?.let {
Log.d("MainActivity", "Application 数据初始化完成")
}
} catch (e: Exception) {
Log.e("MainActivity", "Application 数据初始化失败", e)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_CURRENT_FRAGMENT, currentFragmentTag)
}
private fun setupBottomNavigation() {
// 设置首页按钮选中状态
updateBottomNavigation(0)
// 首页按钮点击
vb.homeLayout.setOnClickListener {
showFragment(TAG_HOME)
updateBottomNavigation(0)
}
// 收藏按钮点击
vb.collectionLayout.setOnClickListener {
showFragment(TAG_COLLECTION)
updateBottomNavigation(1)
}
// 设置按钮点击
vb.settingsLayout.setOnClickListener {
showFragment(TAG_SETTING)
updateBottomNavigation(2)
}
}
private fun updateBottomNavigation(selectedPosition: Int) {
// 重置所有按钮状态
vb.homeLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.white))
vb.collectionLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.white))
vb.settingsLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.white))
vb.homeIcon.setImageResource(R.drawable.ic_home_disabled)
vb.collectionIcon.setImageResource(R.drawable.ic_collection_disabled)
vb.settingsIcon.setImageResource(R.drawable.ic_settings_disabled)
// 设置选中按钮状态
when (selectedPosition) {
0 -> {
vb.homeLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.light_purple))
vb.homeIcon.setImageResource(R.drawable.ic_home)
}
1 -> {
vb.collectionLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.light_purple))
vb.collectionIcon.setImageResource(R.drawable.ic_collection)
}
2 -> {
vb.settingsLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.light_purple))
vb.settingsIcon.setImageResource(R.drawable.ic_settings)
}
}
}
private fun showFragment(tag: String) {
Log.d("MainActivity", "显示Fragment: $tag")
currentFragmentTag = tag
val transaction = supportFragmentManager.beginTransaction()
// 隐藏所有 Fragment
homeFragment?.let { transaction.hide(it) }
collectionFragment?.let { transaction.hide(it) }
settingFragment?.let { transaction.hide(it) }
when (tag) {
TAG_HOME -> {
if (homeFragment == null) {
homeFragment = HoFragment.newInstance()
transaction.add(R.id.fragment_container, homeFragment!!, TAG_HOME)
} else {
transaction.show(homeFragment!!)
}
}
TAG_COLLECTION -> {
if (collectionFragment == null) {
collectionFragment = ColleFragment.newInstance()
transaction.add(R.id.fragment_container, collectionFragment!!, TAG_COLLECTION)
} else {
transaction.show(collectionFragment!!)
}
}
TAG_SETTING -> {
if (settingFragment == null) {
settingFragment = SeFragment.newInstance()
transaction.add(R.id.fragment_container, settingFragment!!, TAG_SETTING)
} else {
transaction.show(settingFragment!!)
}
}
}
transaction.commit()
}
/**
* 获取应用实例
*/
fun getMyApplication(): MyApplication? {
return application as? MyApplication
}
// 处理权限请求结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// 将权限结果传递给当前显示的Fragment
when (currentFragmentTag) {
TAG_SETTING -> {
settingFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
override fun onDestroy() {
super.onDestroy()
// 取消所有协程
activityScope.coroutineContext.cancelChildren()
}
}

View File

@ -0,0 +1,582 @@
package com.gallery.free.wallpaper
import android.app.Application
import android.content.ComponentCallbacks2
import android.content.Context
import android.util.Log
import android.util.LruCache
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.gallery.free.wallpaper.database.FavorWpManager
import com.up.uploadlibrary.UpLoadManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MyApplication : Application() {
companion object {
@Volatile
private var instance: MyApplication? = null
val FIXED_CATEGORIES = listOf("Film", "Nature", "Travel")
private const val JSON_SUFFIX = "_two.json"
private const val PREFS_NAME = "wallpaper_app_settings"
private const val KEY_FIRST_LAUNCH = "first_launch"
private const val KEY_DEFAULT_CATEGORY = "default_category"
private const val KEY_IMAGE_QUALITY = "image_quality"
private const val KEY_LAST_CLEANUP = "last_cleanup_time"
private const val MAX_WALLPAPERS_PER_CATEGORY = 200
private const val MAX_CACHE_CATEGORIES = 3
private const val CACHE_STATS_KEY = "cache_stats"
}
// 协程作用域
private val applicationScope = CoroutineScope(Dispatchers.IO + Job())
// 管理器
private lateinit var favoriteManager: FavorWpManager
// 缓存
private val wallpaperCache = LruCache<String, List<WallpaperJson>>(1)
private val wallpaperItemCache = LruCache<String, List<WpItem>>(1)
// 配置
private lateinit var appConfig: AppConfig
// 缓存统计
private var cacheHits = 0
private var totalLoads = 0
data class AppConfig(
val fixedCategories: List<String> = FIXED_CATEGORIES,
val preloadCategories: List<String> = FIXED_CATEGORIES, // 预加载也使用固定的三个分类
val cacheSizeLimit: Int = MAX_CACHE_CATEGORIES,
val enableAutoCleanup: Boolean = true,
val imageQuality: String = "balanced",
val maxWallpapersPerCategory: Int = MAX_WALLPAPERS_PER_CATEGORY
)
override fun onCreate() {
super.onCreate()
instance = this
Log.d("MyApplication", "=== 应用启动 ===")
Log.d("MyApplication", "包名: ${packageName}")
Log.d("MyApplication", "数据库路径: ${getDatabasePath("FavoriteWallpapers.db")?.absolutePath}")
Log.d("MyApplication", "缓存配置: 最多缓存 $MAX_CACHE_CATEGORIES 个分类,每分类 $MAX_WALLPAPERS_PER_CATEGORY 张")
Log.d("MyApplication", "固定分类: ${FIXED_CATEGORIES.joinToString(", ")}")
appConfig = AppConfig()
initApplication()
startMemoryMonitoring()
// 验证固定分类文件
validateFixedCategories()
UpLoadManager.init(this,"ocean", callback = { _, _ -> })
}
/**
* 验证固定分类文件是否存在
*/
private fun validateFixedCategories() {
applicationScope.launch {
try {
val assets = assets.list("") ?: emptyArray()
val availableFiles = assets.filter { it.endsWith(".json") }
Log.d("MyApplication", "=== 验证固定分类文件 ===")
Log.d("MyApplication", "Assets中的JSON文件: ${availableFiles.joinToString(", ")}")
for (category in FIXED_CATEGORIES) {
val expectedFileName = "${category}${JSON_SUFFIX}"
if (availableFiles.contains(expectedFileName)) {
Log.d("MyApplication", "✅ [$category]: $expectedFileName 存在")
} else {
Log.e("MyApplication", "❌ [$category]: $expectedFileName 不存在 - 将无法加载数据")
}
}
} catch (e: Exception) {
Log.e("MyApplication", "验证分类文件失败", e)
}
}
}
private fun initApplication() {
Log.d("MyApplication", "开始初始化应用")
initDatabase() // 1. 初始化数据库
preloadWallpaperData() // 2. 预加载固定的三个分类
configureImageLoader() // 3. 配置图片加载库
initAppLifecycleObserver() // 4. 初始化应用状态监听
initGlobalSettings() // 5. 其他全局初始化
Log.d("MyApplication", "应用初始化完成")
}
/**
* 启动内存监控
*/
private fun startMemoryMonitoring() {
applicationScope.launch {
while (true) {
kotlinx.coroutines.delay(60000)
val runtime = Runtime.getRuntime()
val usedMemoryMB = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024)
val maxMemoryMB = runtime.maxMemory() / (1024 * 1024)
val usagePercent = (usedMemoryMB.toFloat() / maxMemoryMB.toFloat()) * 100
Log.d("MemoryMonitor", "内存使用: ${"%.1f".format(usagePercent)}% ($usedMemoryMB/$maxMemoryMB MB)")
if (usagePercent > 85) {
Log.w("MemoryMonitor", "内存使用率超过85%,执行清理")
clearExcessCaches()
}
if (totalLoads > 0 && totalLoads % 200 == 0) {
logCacheStatistics()
}
}
}
}
/**
* 记录缓存统计
*/
private fun logCacheStatistics() {
val hitRate = if (totalLoads > 0) {
(cacheHits.toFloat() / totalLoads.toFloat()) * 100
} else 0f
val cachedCategories = wallpaperItemCache.snapshot().keys
val totalWallpapers = cachedCategories.sumOf { category ->
wallpaperItemCache.get(category)?.size ?: 0
}
Log.d("CacheStats", "=== 缓存统计 ===")
Log.d("CacheStats", "命中率: ${"%.1f".format(hitRate)}% ($cacheHits/$totalLoads)")
Log.d("CacheStats", "缓存分类数: ${cachedCategories.size}/$MAX_CACHE_CATEGORIES")
Log.d("CacheStats", "总壁纸数: $totalWallpapers")
Log.d("CacheStats", "缓存分类: ${cachedCategories.joinToString()}")
// 保存统计
getSharedPreferences(CACHE_STATS_KEY, Context.MODE_PRIVATE).edit()
.putInt("totalLoads", totalLoads)
.putInt("cacheHits", cacheHits)
.putLong("lastCheck", System.currentTimeMillis())
.apply()
}
/**
* 清理多余缓存
*/
private fun clearExcessCaches() {
Log.d("MemoryMonitor", "清理多余缓存...")
// 清理Glide内存缓存
Glide.get(this).clearMemory()
// 如果缓存分类超过限制,清理最少使用的
if (wallpaperItemCache.size() > MAX_CACHE_CATEGORIES) {
val snapshot = wallpaperItemCache.snapshot()
val keys = snapshot.keys.toList()
// 保留最近使用的,清理其他的(但不是全部清理)
val excessCount = keys.size - MAX_CACHE_CATEGORIES
if (excessCount > 0) {
for (i in 0 until excessCount) {
wallpaperItemCache.remove(keys[i])
wallpaperCache.remove(keys[i])
}
Log.d("MemoryMonitor", "清理了 $excessCount 个分类缓存")
}
}
if (System.currentTimeMillis() % 5 == 0L) {
System.gc()
}
}
/**
* 初始化数据库
*/
private fun initDatabase() {
applicationScope.launch {
try {
favoriteManager = FavorWpManager.getInstance(this@MyApplication)
val favoriteCount = favoriteManager.getFavoriteCount()
Log.d("MyApplication", "✅ 数据库初始化完成")
Log.d("MyApplication", " 数据库路径: ${getDatabasePath("FavoriteWallpapers.db")?.absolutePath}")
Log.d("MyApplication", " 收藏数量: $favoriteCount")
if (favoriteCount > 0) {
Log.d("MyApplication", " 数据库中有 $favoriteCount 条收藏记录,数据已保留")
}
} catch (e: Exception) {
Log.e("MyApplication", "❌ 数据库初始化失败: ${e.message}", e)
}
}
}
/**
* 预加载壁纸数据每个分类只加载200张
*/
private fun preloadWallpaperData() {
applicationScope.launch {
try {
Log.d("MyApplication", "开始预加载壁纸数据(固定三个分类,每分类${appConfig.maxWallpapersPerCategory}张)")
// 只预加载固定的三个分类
FIXED_CATEGORIES.forEach { category ->
preloadWallpaperCategory(category)
}
Log.d("MyApplication", "壁纸数据预加载完成")
} catch (e: Exception) {
Log.e("MyApplication", "壁纸数据预加载失败: ${e.message}", e)
}
}
}
/**
* 预加载特定分类的壁纸数据只加载200张
*/
private fun preloadWallpaperCategory(category: String) {
applicationScope.launch {
try {
Log.d("MyApplication", "预加载固定分类: $category (限${appConfig.maxWallpapersPerCategory}张)")
val fileName = "${category}${JSON_SUFFIX}"
// JsonUtils 加载数据
val allWallpapers = Utils_json.loadWallpapersFromAssets(
this@MyApplication,
fileName
)
if (allWallpapers.isNotEmpty()) {
val limitedWallpapers = if (allWallpapers.size > appConfig.maxWallpapersPerCategory) {
Log.d("MyApplication", "分类 [$category] 有 ${allWallpapers.size} 张,只加载前 ${appConfig.maxWallpapersPerCategory}")
allWallpapers.take(appConfig.maxWallpapersPerCategory)
} else {
allWallpapers
}
// 缓存原始 JSON 数据
synchronized(wallpaperCache) {
wallpaperCache.put(category, limitedWallpapers)
}
val wallpaperItems = limitedWallpapers.map { wallpaperJson ->
Utils_json.convertToWallpaperItem(wallpaperJson)
}
synchronized(wallpaperItemCache) {
wallpaperItemCache.put(category, wallpaperItems)
}
Log.d("MyApplication", "✅ 预加载分类 [$category] 完成,数量: ${limitedWallpapers.size}")
} else {
Log.w("MyApplication", "⚠️ 分类 [$category] 没有数据或加载失败")
}
} catch (e: Exception) {
Log.e("MyApplication", "❌ 预加载分类 [$category] 失败: ${e.message}", e)
}
}
}
/**
* 获取壁纸数据优先从缓存获取最多200张
*/
suspend fun getWallpaperData(category: String): List<WallpaperJson> {
totalLoads++
return withContext(Dispatchers.IO) {
val categoryName = if (category.endsWith("_two.json")) {
category.replace("_two.json", "")
} else {
category
}
// 先从缓存获取
synchronized(wallpaperCache) {
val cachedData = wallpaperCache.get(categoryName)
if (cachedData != null) {
cacheHits++
Log.d("MyApplication", "缓存命中: [$categoryName] 数量: ${cachedData.size}")
return@withContext cachedData
}
}
// 缓存中没有,则加载
Log.d("MyApplication", "缓存未命中: [$categoryName],开始加载")
val fileName = if (category.endsWith(".json")) category else "${categoryName}_two.json"
val allWallpapers = Utils_json.loadWallpapersFromAssets(
this@MyApplication,
fileName
)
val wallpapers = if (allWallpapers.size > appConfig.maxWallpapersPerCategory) {
Log.d("MyApplication", "分类 [$categoryName] 有 ${allWallpapers.size} 张,只返回前 ${appConfig.maxWallpapersPerCategory}")
allWallpapers.take(appConfig.maxWallpapersPerCategory)
} else {
allWallpapers
}
// 更新缓存(如果数据有效)
if (wallpapers.isNotEmpty()) {
synchronized(wallpaperCache) {
wallpaperCache.put(categoryName, wallpapers)
}
Log.d("MyApplication", "已缓存分类: [$categoryName] 数量: ${wallpapers.size}")
}
return@withContext wallpapers
}
}
/**
* 获取转换后的壁纸项目优先从缓存获取最多200张
*/
suspend fun getWallpaperItems(category: String): List<WpItem> {
totalLoads++
return withContext(Dispatchers.IO) {
val categoryName = if (category.endsWith("_two.json")) {
category.replace("_two.json", "")
} else {
category
}
// 先从缓存获取
synchronized(wallpaperItemCache) {
val cachedItems = wallpaperItemCache.get(categoryName)
if (cachedItems != null) {
cacheHits++
Log.d("MyApplication", "缓存命中: [$categoryName] 数量: ${cachedItems.size}")
return@withContext cachedItems
}
}
// 缓存中没有,则获取数据并转换
val wallpapers = getWallpaperData(category)
val wallpaperItems = wallpapers.map { wallpaperJson ->
Utils_json.convertToWallpaperItem(wallpaperJson)
}
// 更新缓存
if (wallpaperItems.isNotEmpty()) {
synchronized(wallpaperItemCache) {
wallpaperItemCache.put(categoryName, wallpaperItems)
}
}
return@withContext wallpaperItems
}
}
/**
* 配置图片加载库
*/
private fun configureImageLoader() {
try {
val requestOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.timeout(10000)
.skipMemoryCache(true)
Log.d("MyApplication", "✅ 图片加载库配置完成")
} catch (e: Exception) {
Log.e("MyApplication", "❌ 图片加载库配置失败: ${e.message}", e)
}
}
/**
* 初始化应用生命周期监听
*/
private fun initAppLifecycleObserver() {
try {
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
Log.d("MyApplication", "✅ 应用生命周期监听已初始化")
} catch (e: Exception) {
Log.e("MyApplication", "❌ 应用生命周期监听初始化失败: ${e.message}", e)
}
}
/**
* 初始化全局设置
*/
private fun initGlobalSettings() {
applicationScope.launch {
try {
initDefaultPreferences()
if (appConfig.enableAutoCleanup) {
cleanTempFiles()
}
Log.d("MyApplication", "✅ 全局设置初始化完成")
} catch (e: Exception) {
Log.e("MyApplication", "❌ 全局设置初始化失败: ${e.message}", e)
}
}
}
/**
* 初始化默认的首选项设置
*/
private fun initDefaultPreferences() {
val sharedPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val isFirstLaunch = !sharedPrefs.contains(KEY_FIRST_LAUNCH) ||
sharedPrefs.getBoolean(KEY_FIRST_LAUNCH, true)
if (isFirstLaunch) {
Log.d("MyApplication", "🎉 首次启动应用")
}
with(sharedPrefs.edit()) {
if (!sharedPrefs.contains(KEY_FIRST_LAUNCH)) {
putBoolean(KEY_FIRST_LAUNCH, true)
}
if (!sharedPrefs.contains(KEY_DEFAULT_CATEGORY)) {
putString(KEY_DEFAULT_CATEGORY, "Film")
}
if (!sharedPrefs.contains(KEY_IMAGE_QUALITY)) {
putString(KEY_IMAGE_QUALITY, "balanced")
}
if (!sharedPrefs.contains(KEY_LAST_CLEANUP)) {
putLong(KEY_LAST_CLEANUP, System.currentTimeMillis())
}
apply()
}
Log.d("MyApplication", "✅ 默认首选项设置完成")
if (isFirstLaunch) {
sharedPrefs.edit().putBoolean(KEY_FIRST_LAUNCH, false).apply()
Log.d("MyApplication", "已将首次启动标记设置为 false")
}
}
/**
* 清理临时文件
*/
private fun cleanTempFiles() {
try {
val sharedPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val lastCleanupTime = sharedPrefs.getLong(KEY_LAST_CLEANUP, 0)
val currentTime = System.currentTimeMillis()
if (currentTime - lastCleanupTime > 7 * 24 * 60 * 60 * 1000L) {
applicationScope.launch {
Glide.get(this@MyApplication).clearDiskCache()
val snapshot = wallpaperItemCache.snapshot()
val keys = snapshot.keys.toList()
if (keys.size > MAX_CACHE_CATEGORIES) {
for (i in 0 until keys.size - MAX_CACHE_CATEGORIES) {
wallpaperItemCache.remove(keys[i])
wallpaperCache.remove(keys[i])
}
}
sharedPrefs.edit().putLong(KEY_LAST_CLEANUP, currentTime).apply()
Log.d("MyApplication", "✅ 临时文件清理完成")
}
}
} catch (e: Exception) {
Log.w("MyApplication", "清理临时文件时出错: ${e.message}")
}
}
/**
* 应用生命周期观察者
*/
inner class AppLifecycleObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onAppForegrounded() {
Log.d("MyApplication", "📱 应用进入前台")
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onAppBackgrounded() {
Log.d("MyApplication", "📱 应用进入后台")
// 进入后台时清理Glide内存缓存
Glide.get(this@MyApplication).clearMemory()
System.gc()
}
}
/**
* 当系统内存不足时调用
*/
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.d("MyApplication", "🔄 内存紧张,级别: $level")
when (level) {
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
// 清理Glide内存缓存
Glide.get(this).clearMemory()
// 清理应用缓存
synchronized(wallpaperCache) {
wallpaperCache.trimToSize(0)
}
synchronized(wallpaperItemCache) {
wallpaperItemCache.trimToSize(0)
}
// 强制垃圾回收
System.gc()
System.runFinalization()
Log.d("MyApplication", "清理了所有缓存")
}
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
ComponentCallbacks2.TRIM_MEMORY_MODERATE,
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
// 清理所有资源
synchronized(wallpaperCache) {
wallpaperCache.evictAll()
}
synchronized(wallpaperItemCache) {
wallpaperItemCache.evictAll()
}
Glide.get(this).clearMemory()
Log.d("MyApplication", "应用在后台,清理了所有资源")
}
}
}
/**
* 当应用终止时调用
*/
override fun onTerminate() {
// 清理资源
synchronized(wallpaperCache) {
wallpaperCache.evictAll()
}
synchronized(wallpaperItemCache) {
wallpaperItemCache.evictAll()
}
// 取消所有协程
applicationScope.coroutineContext.cancelChildren()
instance = null
super.onTerminate()
Log.d("MyApplication", "🛑 应用终止")
}
}

View File

@ -0,0 +1,55 @@
package com.gallery.free.wallpaper
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import androidx.appcompat.app.AppCompatActivity
import com.ad.tradpluslibrary.TPAdManager
import com.gallery.free.wallpaper.databinding.ActivitySplaBinding
class SplaActivity : AppCompatActivity() {
private var countDownTimer: CountDownTimer? = null
private var vb: ActivitySplaBinding? = null
private val totalTime: Long = 15000
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vb = ActivitySplaBinding.inflate(layoutInflater)
Utils_com.initFull(this, true)
setContentView(vb?.getRoot())
TPAdManager.init(
this,
"ocean",
"EF4D3DDD83B3D1EF16ED3E6995AF0E11",
"58E5429FFDD5BDF2034D6D78B157C012",
"8DB63FE93F30FEA99D2D275BFC974C12",
"09E55308FBBE7CD2C0C5571EFCEB8312"
) {
}
countDownTimer = TPAdManager.showWelcomeAd(this, totalTime, { millisUntilFinished ->
val progressPercentage = ((100 * millisUntilFinished) / totalTime).toInt()
val countdownPercentage = 100 - progressPercentage
vb?.progressbar?.progress = countdownPercentage
}) {
vb?.progressbar?.progress = 100
val intent = Intent(this@SplaActivity, MainActivity::class.java)
startActivity(intent)
finish()
}
countDownTimer?.start()
}
override fun onDestroy() {
super.onDestroy()
if (countDownTimer != null) {
countDownTimer!!.cancel()
countDownTimer = null
}
}
}

View File

@ -0,0 +1,105 @@
package com.gallery.free.wallpaper
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
class StaggeredGridSpacingItemDecoration(
private val spanCount: Int,
private val spacing: Int
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val layoutParams = view.layoutParams as? StaggeredGridLayoutManager.LayoutParams
val spanIndex = layoutParams?.spanIndex ?: 0
// 根据列数动态计算间距
when (spanCount) {
2 -> {
// 两列布局
when (spanIndex) {
0 -> {
// 第一列:左边间距大,右边间距小
outRect.left = spacing
outRect.right = spacing / 2
}
1 -> {
// 第二列:左边间距小,右边间距大
outRect.left = spacing / 2
outRect.right = spacing
}
}
// 垂直方向间距
outRect.top = if (isFirstRow(position, spanCount)) 0 else spacing
outRect.bottom = spacing
}
else -> {
// 其他列数使用均匀间距
setupUniformSpacing(outRect, spanIndex, spanCount, position)
}
}
}
private fun setupUniformSpacing(
outRect: Rect,
spanIndex: Int,
spanCount: Int,
position: Int
) {
// 水平间距
val column = spanIndex % spanCount
// 左边距:第一列有完整间距,其他列使用一半间距
outRect.left = if (column == 0) spacing else spacing / 2
// 右边距:最后一列有完整间距,其他列使用一半间距
outRect.right = if (column == spanCount - 1) spacing else spacing / 2
// 垂直间距
outRect.top = if (isFirstRow(position, spanCount)) 0 else spacing
outRect.bottom = spacing
}
private fun isFirstRow(position: Int, spanCount: Int): Boolean {
return position < spanCount
}
}
object GridDecorationUtils {
/**
* RecyclerView 快速添加间距装饰器
*/
fun applySpacing(
recyclerView: RecyclerView,
spacingDp: Int,
spanCount: Int = 2
) {
val spacingInPixels = recyclerView.context.resources
.getDimensionPixelSize(spacingDp)
recyclerView.addItemDecoration(
StaggeredGridSpacingItemDecoration(spanCount, spacingInPixels)
)
}
/**
* 计算网格列数
*/
fun calculateSpanCount(context: Context, itemWidthDp: Int = 180): Int {
val displayMetrics = context.resources.displayMetrics
val screenWidthPx = displayMetrics.widthPixels
val screenWidthDp = screenWidthPx / displayMetrics.density
return maxOf(2, (screenWidthDp / itemWidthDp).toInt())
}
}

View File

@ -0,0 +1,19 @@
package com.gallery.free.wallpaper;
import android.app.Activity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
public class Utils_com {
public static void initFull(Activity activity, boolean light) {
Window window = activity.getWindow();
View decorView = window.getDecorView();
if (light) {
} else {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}

View File

@ -0,0 +1,64 @@
package com.gallery.free.wallpaper
import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
object Utils_json {
private val gson = Gson()
private const val TAG = "JsonUtils"
fun loadWallpapersFromAssets(context: Context, fileName: String): List<WallpaperJson> {
return try {
Log.d(TAG, "Loading wallpapers from: $fileName")
// 检查文件是否存在
val fileList = context.assets.list("")
if (fileList?.contains(fileName) != true) {
Log.e(TAG, "File not found in assets: $fileName")
return emptyList()
}
val inputStream: InputStream = context.assets.open(fileName)
val jsonString = inputStream.bufferedReader().use { it.readText() }
if (jsonString.isBlank()) {
Log.e(TAG, "Empty JSON file: $fileName")
return emptyList()
}
val listType = object : TypeToken<List<WallpaperJson>>() {}.type
val result = gson.fromJson<List<WallpaperJson>>(jsonString, listType)
Log.d(TAG, "Successfully loaded ${result.size} wallpapers from $fileName")
result
} catch (e: Exception) {
Log.e(TAG, "Error loading wallpapers from $fileName", e)
emptyList()
}
}
fun convertToWallpaperItem(wallpaperJson: WallpaperJson): WpItem {
val name = wallpaperJson.altDescription ?: "Unknown"
val smallUrl = wallpaperJson.urls.small
val fullUrl = wallpaperJson.urls.full
val finalSmallUrl = if (smallUrl.isEmpty()) {
wallpaperJson.urls.small
} else {
smallUrl
}
val description = wallpaperJson.altDescription ?: wallpaperJson.user.name ?: ""
return WpItem(
smallUrl = finalSmallUrl,
fullUrl = fullUrl,
description = description
)
}
}

View File

@ -0,0 +1,98 @@
package com.gallery.free.wallpaper
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
class WpAdapter(
private val wallpaperList: List<WpItem>,
private val onItemClick: (Int, WpItem) -> Unit
) : RecyclerView.Adapter<WpAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imageView: ImageView = itemView.findViewById(R.id.imageView)
val descriptionText: TextView = itemView.findViewById(R.id.descriptionText)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_wp, parent, false)
return ViewHolder(view)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, @SuppressLint("RecyclerView") position: Int) {
val wallpaper = wallpaperList[position]
// 设置描述文字
val displayText = if (wallpaper.description.isNotEmpty()) {
wallpaper.description
} else {
"Unnamed Wallpaper"
}
// 截断过长的描述文字
val maxLength = 50
val finalText = if (displayText.length > maxLength) {
displayText.substring(0, maxLength) + "..."
} else {
displayText
}
holder.descriptionText.text = finalText
// 加载图片
if (wallpaper.smallUrl.isNotEmpty()) {
Glide.with(holder.itemView.context)
.load(wallpaper.smallUrl)
.transition(DrawableTransitionOptions.withCrossFade())
.addListener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable?>?,
isFirstResource: Boolean
): Boolean {
Log.d("WallpaperAdapter", "Smallnail load failed: ${e?.message}")
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
Log.d("WallpaperAdapter", "Thumbnail load successful")
return false
}
})
.placeholder(R.drawable.placeho_homepage)
.error(R.drawable.placeho_homepage)
.centerCrop()
.into(holder.imageView)
} else {
// 如果 URL 为空,使用本地资源
holder.imageView.setImageResource(wallpaper.imageRes)
}
holder.itemView.setOnClickListener {
onItemClick(position, wallpaper)
}
}
override fun getItemCount(): Int = wallpaperList.size
}

View File

@ -0,0 +1,39 @@
package com.gallery.free.wallpaper
import android.os.Parcel
import android.os.Parcelable
data class WpItem(
val smallUrl: String, // 缩略图URL
val fullUrl: String, // 全尺寸URL
val description: String = "", // 描述字段
val imageRes: Int = 0
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.readInt()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(smallUrl)
parcel.writeString(fullUrl)
parcel.writeString(description)
parcel.writeInt(imageRes)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<WpItem> {
override fun createFromParcel(parcel: Parcel): WpItem {
return WpItem(parcel)
}
override fun newArray(size: Int): Array<WpItem?> {
return arrayOfNulls(size)
}
}
}

View File

@ -0,0 +1,65 @@
package com.gallery.free.wallpaper
import com.google.gson.annotations.SerializedName
data class WallpaperJson(
@SerializedName("alt_description")
val altDescription: String?,
@SerializedName("links")
val links: Links,
@SerializedName("urls")
val urls: Urls,
@SerializedName("user")
val user: User
)
data class Links(
@SerializedName("download")
val download: String,
@SerializedName("download_location")
val downloadLocation: String,
@SerializedName("html")
val html: String
)
data class Urls(
@SerializedName("full")
val full: String,
@SerializedName("raw")
val raw: String,
@SerializedName("regular")
val regular: String,
@SerializedName("small")
val small: String,
@SerializedName("thumb")
val thumb: String
)
data class User(
@SerializedName("authorHtml")
val authorHtml: String,
@SerializedName("header_large")
val headerLarge: String,
@SerializedName("header_medium")
val headerMedium: String,
@SerializedName("header_small")
val headerSmall: String,
@SerializedName("name")
val name: String,
@SerializedName("portfolio_url")
val portfolioUrl: String
)

View File

@ -0,0 +1,162 @@
package com.gallery.free.wallpaper
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
class WpPagerAdapter(
private val wallpaperItems: List<WpItem>,
private val loadListener: OnImageLoadListener? = null // 添加加载监听器
) : RecyclerView.Adapter<WpPagerAdapter.ViewHolder>() {
// 添加加载监听接口
interface OnImageLoadListener {
fun onImageLoadSuccess(position: Int)
fun onImageLoadFailed(position: Int, error: Throwable)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imageView: ImageView = itemView.findViewById(R.id.pager_image_view)
val loadingProgress: ImageView = itemView.findViewById(R.id.loading_progress)
// 添加标识防止错乱的UI更新
var currentPosition: Int = -1
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_wp_pager, parent, false)
return ViewHolder(view)
}
@SuppressLint("RecyclerView")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val wallpaperItem = wallpaperItems[position]
Log.d("WallpaperPagerAdapter", "onBindViewHolder position=$position")
// 设置当前position标识
holder.currentPosition = position
// 1. 立即显示loading
holder.loadingProgress.visibility = View.VISIBLE
holder.loadingProgress.bringToFront()
// 2. 立即显示smallUrl使用单独的Target
val smallUrlTarget = object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: android.graphics.drawable.Drawable,
transition: com.bumptech.glide.request.transition.Transition<in android.graphics.drawable.Drawable>?
) {
Log.d("WallpaperPagerAdapter", "smallUrl加载完成 position=$position")
// 确保还是当前position
if (holder.currentPosition == position) {
holder.imageView.setImageDrawable(resource)
}
}
override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) {
// 清理资源
}
override fun onLoadFailed(errorDrawable: android.graphics.drawable.Drawable?) {
Log.e("WallpaperPagerAdapter", "smallUrl加载失败 position=$position")
if (holder.currentPosition == position && holder.itemView.isAttachedToWindow) {
holder.imageView.setBackgroundColor(R.color.light_gray)
holder.imageView.setImageResource(R.drawable.ic_loadingpic_failed)
holder.imageView.scaleType = ImageView.ScaleType.CENTER
}
}
}
// 先清除可能的错误状态
holder.imageView.setBackgroundColor(0x00000000)
holder.imageView.scaleType = ImageView.ScaleType.CENTER_CROP
Glide.with(holder.itemView.context)
.load(wallpaperItem.smallUrl)
.dontAnimate()
.skipMemoryCache(true) // 跳过内存缓存,确保每次都触发回调
.into(smallUrlTarget)
// 3. 加载fullUrl
Glide.with(holder.itemView.context)
.load(wallpaperItem.fullUrl)
.dontAnimate()
.diskCacheStrategy(com.bumptech.glide.load.engine.DiskCacheStrategy.ALL)
.listener(object : RequestListener<android.graphics.drawable.Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<android.graphics.drawable.Drawable>?,
isFirstResource: Boolean
): Boolean {
Log.e("WallpaperPagerAdapter", "fullUrl加载失败 position=$position: ${e?.message}")
if (holder.currentPosition == position && holder.itemView.isAttachedToWindow) {
holder.loadingProgress.visibility = View.GONE
// 自定义错误显示:设置灰色背景和居中图标
holder.imageView.setBackgroundColor(R.color.light_gray)
holder.imageView.setImageResource(R.drawable.ic_loadingpic_failed)
holder.imageView.scaleType = ImageView.ScaleType.CENTER
}
// 通知加载失败
loadListener?.onImageLoadFailed(position, e ?: Exception("图片加载失败"))
return true
}
override fun onResourceReady(
resource: android.graphics.drawable.Drawable?,
model: Any?,
target: Target<android.graphics.drawable.Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
Log.d("WallpaperPagerAdapter", "fullUrl加载完成 position=$position")
if (holder.currentPosition == position && holder.itemView.isAttachedToWindow) {
holder.loadingProgress.visibility = View.GONE
// 清除可能的错误状态
holder.imageView.setBackgroundColor(0x00000000) // 透明背景
holder.imageView.scaleType = ImageView.ScaleType.CENTER_CROP // 恢复原来的缩放模式
}
// 通知加载成功
loadListener?.onImageLoadSuccess(position)
return false
}
})
.into(holder.imageView)
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
// 清理ImageViewGlide会自动取消相关请求
Glide.with(holder.itemView.context).clear(holder.imageView)
// 重置标识
holder.currentPosition = -1
}
override fun getItemCount(): Int = wallpaperItems.size
// 添加重新加载方法
fun retryLoadImage(position: Int) {
notifyItemChanged(position)
}
fun clearAllCache() {
// 如果确实需要清除所有缓存可以在Activity中调用但通常不需要手动清理
// Glide.get(context).clearMemory()
}
}

View File

@ -0,0 +1,263 @@
package com.gallery.free.wallpaper.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class FavorWpDbHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
const val DATABASE_VERSION = 2
const val DATABASE_NAME = "FavoriteWallpapers.db"
const val TABLE_NAME = "favorite_wallpapers"
const val COLUMN_ID = "id"
const val COLUMN_IMAGE_URL = "image_url"
const val COLUMN_DESCRIPTION = "description"
const val COLUMN_TIMESTAMP = "timestamp"
}
override fun onCreate(db: SQLiteDatabase) {
val createTable = """
CREATE TABLE $TABLE_NAME (
$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_IMAGE_URL TEXT UNIQUE NOT NULL,
$COLUMN_DESCRIPTION TEXT DEFAULT '',
$COLUMN_TIMESTAMP INTEGER DEFAULT (strftime('%s','now'))
)
""".trimIndent()
db.execSQL(createTable)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) {
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN $COLUMN_DESCRIPTION TEXT DEFAULT ''")
}
}
// 添加壁纸到收藏
fun addFavorite(imageUrl: String, description: String = ""): Boolean {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_IMAGE_URL, imageUrl)
if (description.isNotEmpty()) {
put(COLUMN_DESCRIPTION, description)
}
}
return try {
val result = db.insertWithOnConflict(
TABLE_NAME,
null,
values,
SQLiteDatabase.CONFLICT_IGNORE
)
result != -1L
} catch (e: Exception) {
false
} finally {
db.close()
}
}
// 更新已存在的收藏信息(更新描述)
fun updateFavoriteDescription(imageUrl: String, description: String): Boolean {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_DESCRIPTION, description)
}
return try {
val result = db.update(
TABLE_NAME,
values,
"$COLUMN_IMAGE_URL = ?",
arrayOf(imageUrl)
)
result > 0
} catch (e: Exception) {
false
} finally {
db.close()
}
}
// 从收藏中移除壁纸
fun removeFavorite(imageUrl: String): Boolean {
val db = writableDatabase
return try {
val result = db.delete(
TABLE_NAME,
"$COLUMN_IMAGE_URL = ?",
arrayOf(imageUrl)
)
result > 0
} catch (e: Exception) {
false
} finally {
db.close()
}
}
// 检查壁纸是否在收藏中
fun isFavorite(imageUrl: String): Boolean {
val db = readableDatabase
var cursor: Cursor? = null
return try {
cursor = db.query(
TABLE_NAME,
arrayOf(COLUMN_IMAGE_URL),
"$COLUMN_IMAGE_URL = ?",
arrayOf(imageUrl),
null, null, null
)
cursor?.count ?: 0 > 0
} catch (e: Exception) {
false
} finally {
cursor?.close()
db.close()
}
}
data class FavoriteInfo(
val imageUrl: String,
val description: String
)
// 获取所有收藏的壁纸信息URL和描述
fun getAllFavoritesWithInfo(): List<FavoriteInfo> {
val db = readableDatabase
val favorites = mutableListOf<FavoriteInfo>()
var cursor: Cursor? = null
try {
cursor = db.query(
TABLE_NAME,
arrayOf(COLUMN_IMAGE_URL, COLUMN_DESCRIPTION),
null, null, null, null,
"$COLUMN_TIMESTAMP DESC"
)
cursor?.let {
val urlIndex = it.getColumnIndex(COLUMN_IMAGE_URL)
val descIndex = it.getColumnIndex(COLUMN_DESCRIPTION)
while (it.moveToNext()) {
if (urlIndex != -1) {
val url = it.getString(urlIndex)
val description = if (descIndex != -1) it.getString(descIndex) else ""
favorites.add(FavoriteInfo(url, description))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
cursor?.close()
db.close()
}
return favorites
}
// 获取所有收藏的壁纸URL保持向后兼容
fun getAllFavorites(): List<String> {
val db = readableDatabase
val favorites = mutableListOf<String>()
var cursor: Cursor? = null
try {
cursor = db.query(
TABLE_NAME,
arrayOf(COLUMN_IMAGE_URL),
null, null, null, null,
"$COLUMN_TIMESTAMP DESC"
)
cursor?.let {
val urlIndex = it.getColumnIndex(COLUMN_IMAGE_URL)
while (it.moveToNext()) {
if (urlIndex != -1) {
favorites.add(it.getString(urlIndex))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
cursor?.close()
db.close()
}
return favorites
}
// 获取收藏数量
fun getFavoriteCount(): Int {
val db = readableDatabase
var cursor: Cursor? = null
return try {
cursor = db.rawQuery("SELECT COUNT(*) FROM $TABLE_NAME", null)
if (cursor?.moveToFirst() == true) {
cursor.getInt(0)
} else {
0
}
} catch (e: Exception) {
0
} finally {
cursor?.close()
db.close()
}
}
// 检查是否有指定URL的收藏
fun hasFavorite(imageUrl: String): Boolean {
return isFavorite(imageUrl)
}
// 批量获取收藏状态
fun getFavoriteStatus(urls: List<String>): Map<String, Boolean> {
val db = readableDatabase
val result = mutableMapOf<String, Boolean>()
var cursor: Cursor? = null
try {
// 为每个URL创建一个占位符
val placeholders = urls.joinToString(",") { "?" }
val query = """
SELECT $COLUMN_IMAGE_URL
FROM $TABLE_NAME
WHERE $COLUMN_IMAGE_URL IN ($placeholders)
""".trimIndent()
cursor = db.rawQuery(query, urls.toTypedArray())
while (cursor?.moveToNext() == true) {
val urlIndex = cursor.getColumnIndex(COLUMN_IMAGE_URL)
if (urlIndex != -1) {
result[cursor.getString(urlIndex)] = true
}
}
// 设置不在结果中的URL为false
urls.forEach { url ->
if (!result.containsKey(url)) {
result[url] = false
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
cursor?.close()
db.close()
}
return result
}
}

View File

@ -0,0 +1,62 @@
package com.gallery.free.wallpaper.database
import android.content.Context
class FavorWpManager private constructor(context: Context) {
private val dbHelper = FavorWpDbHelper(context)
companion object {
@Volatile
private var INSTANCE: FavorWpManager? = null
fun getInstance(context: Context): FavorWpManager {
return INSTANCE ?: synchronized(this) {
val instance = FavorWpManager(context)
INSTANCE = instance
instance
}
}
}
// 添加收藏,包含描述
fun addFavorite(imageUrl: String, description: String = ""): Boolean {
return dbHelper.addFavorite(imageUrl, description)
}
// 更新收藏描述
fun updateFavoriteDescription(imageUrl: String, description: String): Boolean {
return dbHelper.updateFavoriteDescription(imageUrl, description)
}
// 添加或更新收藏(如果存在则更新描述)
fun addOrUpdateFavorite(imageUrl: String, description: String = ""): Boolean {
return if (isFavorite(imageUrl)) {
updateFavoriteDescription(imageUrl, description)
} else {
addFavorite(imageUrl, description)
}
}
fun removeFavorite(imageUrl: String): Boolean {
return dbHelper.removeFavorite(imageUrl)
}
fun isFavorite(imageUrl: String): Boolean {
return dbHelper.isFavorite(imageUrl)
}
// 获取所有收藏的URL向后兼容
fun getAllFavorites(): List<String> {
return dbHelper.getAllFavorites()
}
// 获取所有收藏的详细信息
fun getAllFavoritesWithInfo(): List<FavorWpDbHelper.FavoriteInfo> {
return dbHelper.getAllFavoritesWithInfo()
}
fun getFavoriteCount(): Int {
return dbHelper.getFavoriteCount()
}
}

View File

@ -0,0 +1,261 @@
package com.gallery.free.wallpaper.fragment
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.gallery.free.wallpaper.DetailwpActivity
import com.gallery.free.wallpaper.R
import com.gallery.free.wallpaper.StaggeredGridSpacingItemDecoration
import com.gallery.free.wallpaper.WpAdapter
import com.gallery.free.wallpaper.WpItem
import com.gallery.free.wallpaper.database.FavorWpDbHelper
import com.gallery.free.wallpaper.database.FavorWpManager
import com.gallery.free.wallpaper.databinding.FragmentColleBinding
class ColleFragment : Fragment() {
private var _binding: FragmentColleBinding? = null
private val binding get() = _binding!!
private lateinit var favoriteManager: FavorWpManager
private var adapter: WpAdapter? = null
private var currentFavoriteInfos: List<FavorWpDbHelper.FavoriteInfo> = emptyList()
// 存储当前显示的壁纸项列表
private var currentWallpaperItems: List<WpItem> = emptyList()
// 添加广播接收器
private val favoriteChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
DetailwpActivity.Companion.ACTION_FAVORITE_CHANGED -> {
Log.d("CollectionFragment", "收到收藏变更广播")
activity?.runOnUiThread {
refreshDataFromDatabase()
}
}
}
}
}
companion object {
fun newInstance(): ColleFragment {
return ColleFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentColleBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 初始化数据库管理
favoriteManager = FavorWpManager.Companion.getInstance(requireContext())
// 注册广播接收器
registerFavoriteChangeReceiver()
setupWallpaperGrid()
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private fun registerFavoriteChangeReceiver() {
val filter = IntentFilter(DetailwpActivity.Companion.ACTION_FAVORITE_CHANGED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireActivity().registerReceiver(favoriteChangeReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
requireActivity().registerReceiver(favoriteChangeReceiver, filter)
}
}
private fun unregisterFavoriteChangeReceiver() {
try {
requireActivity().unregisterReceiver(favoriteChangeReceiver)
} catch (e: IllegalArgumentException) {
// 如果接收器未注册,忽略异常
}
}
private fun setupWallpaperGrid() {
// 获取收藏列表(包含详细信息)
val newFavoriteInfos = favoriteManager.getAllFavoritesWithInfo()
// 检查是否有变化,没有变化则不重新加载
if (newFavoriteInfos == currentFavoriteInfos && adapter != null) {
Log.d("CollectionFragment", "收藏无变化,跳过重新加载")
return
}
currentFavoriteInfos = newFavoriteInfos
updateFavoriteCount(newFavoriteInfos.size)
if (newFavoriteInfos.isEmpty()) {
showEmptyState()
} else {
showWallpaperGrid(newFavoriteInfos)
}
}
private fun getLikedWallpapers(favoriteInfos: List<FavorWpDbHelper.FavoriteInfo>): List<WpItem> {
// 创建完整的 WpItem 对象列表
val wallpaperItems = favoriteInfos.map { info ->
// 使用描述,如果描述为空则使用默认文本
val displayDescription = if (info.description.isNotEmpty()) {
info.description
} else {
"Collected Wallpaper"
}
// 创建完整的 WpItem 对象
// 注意收藏的壁纸只有完整的URL所以smallUrl和fullUrl使用相同的URL
WpItem(
smallUrl = info.imageUrl,
fullUrl = info.imageUrl,
description = displayDescription
)
}
// 存储当前显示的壁纸项
currentWallpaperItems = wallpaperItems
return wallpaperItems
}
// 更新收藏数量显示
private fun updateFavoriteCount(count: Int) {
if (count > 0) {
binding.textFavoriteCount.text = "$count photo collected"
binding.textFavoriteCountContainer.visibility = View.VISIBLE
} else {
binding.textFavoriteCountContainer.visibility = View.GONE
}
}
private fun showEmptyState() {
val emptyLayout = binding.root.findViewById<View>(R.id.empty_likes_layout)
emptyLayout?.visibility = View.VISIBLE
binding.wallpaperRecyclerView.visibility = View.GONE
binding.textFavoriteCountContainer.visibility = View.GONE
}
private fun showWallpaperGrid(favoriteInfos: List<FavorWpDbHelper.FavoriteInfo>) {
val emptyLayout = binding.root.findViewById<View>(R.id.empty_likes_layout)
emptyLayout?.visibility = View.GONE
binding.wallpaperRecyclerView.visibility = View.VISIBLE
val wallpaperList = getLikedWallpapers(favoriteInfos)
// 使用StaggeredGridLayoutManager瀑布流每行2列
val spanCount = 2
val layoutManager = StaggeredGridLayoutManager(
spanCount,
StaggeredGridLayoutManager.VERTICAL
)
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_NONE
// 只在第一次或布局管理器为空时设置
if (binding.wallpaperRecyclerView.layoutManager == null) {
binding.wallpaperRecyclerView.layoutManager = layoutManager
// 使用StaggeredGridSpacingItemDecoration来设置间距
val spacingInPixels = resources.getDimensionPixelSize(R.dimen.grid_spacing)
binding.wallpaperRecyclerView.addItemDecoration(
StaggeredGridSpacingItemDecoration(spanCount, spacingInPixels)
)
}
// 传递 WpItem 对象列表而不是URL列表
adapter = WpAdapter(wallpaperList) { position, wallpaper ->
// 检查是否有数据
if (currentWallpaperItems.isEmpty()) return@WpAdapter
// 获取所有壁纸的完整 WpItem 列表
val intent = Intent(requireContext(), DetailwpActivity::class.java).apply {
putExtra("wallpaper_url", wallpaper.fullUrl)
putExtra("wallpaper_description", wallpaper.description)
// 传递完整的 WpItem 列表
putParcelableArrayListExtra("wallpaper_item_list", ArrayList(currentWallpaperItems))
putExtra("current_position", position)
}
startActivity(intent)
requireActivity().overridePendingTransition(
android.R.anim.fade_in,
android.R.anim.fade_out
)
}
binding.wallpaperRecyclerView.adapter = adapter
}
override fun onResume() {
super.onResume()
Log.d("CollectionFragment", "onResume 调用")
// 只检查是否有变化,不强制重新加载
val newFavoriteInfos = favoriteManager.getAllFavoritesWithInfo()
if (newFavoriteInfos != currentFavoriteInfos) {
Log.d("CollectionFragment", "收藏有变化,重新加载")
// 如果收藏有变化,才重新加载
currentFavoriteInfos = newFavoriteInfos
updateFavoriteCount(newFavoriteInfos.size)
if (newFavoriteInfos.isEmpty()) {
showEmptyState()
} else {
showWallpaperGrid(newFavoriteInfos)
}
} else {
Log.d("CollectionFragment", "onResume 中收藏无变化")
}
}
// 直接从数据库刷新数据的方法
private fun refreshDataFromDatabase() {
Log.d("CollectionFragment", "从数据库刷新数据")
val newFavoriteInfos = favoriteManager.getAllFavoritesWithInfo()
// 即使没有变化也强制更新UI确保数据同步
currentFavoriteInfos = newFavoriteInfos
updateFavoriteCount(newFavoriteInfos.size)
if (newFavoriteInfos.isEmpty()) {
showEmptyState()
} else {
showWallpaperGrid(newFavoriteInfos)
}
}
override fun onDestroyView() {
super.onDestroyView()
// 注销广播接收器
unregisterFavoriteChangeReceiver()
// 清除适配器和数据
adapter = null
binding.wallpaperRecyclerView.adapter = null
currentWallpaperItems = emptyList() // 清理数据
_binding = null
}
}

View File

@ -0,0 +1,333 @@
package com.gallery.free.wallpaper.fragment
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import androidx.fragment.app.Fragment
import androidx.viewpager2.widget.ViewPager2
import com.gallery.free.wallpaper.HoPagerAdapter
import com.gallery.free.wallpaper.MyApplication
import com.gallery.free.wallpaper.MyApplication.Companion.FIXED_CATEGORIES
import com.gallery.free.wallpaper.R
import com.gallery.free.wallpaper.databinding.FragmentHoBinding
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HoFragment : Fragment() {
private var _binding: FragmentHoBinding? = null
private val binding get() = _binding!!
private lateinit var viewPager: ViewPager2
private lateinit var pagerAdapter: HoPagerAdapter
private var currentViewPagerPosition = 0
private val VIEW_PAGER_POSITION_KEY = "view_pager_position"
// 刷新状态
private var isRefreshing = false
// 添加协程作用域
private val fragmentScope = CoroutineScope(Dispatchers.Main + Job())
private var categories: List<String> = FIXED_CATEGORIES
companion object {
private const val TAG = "HomeFragment"
fun newInstance(): HoFragment {
return HoFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 恢复状态
savedInstanceState?.let {
currentViewPagerPosition = it.getInt(VIEW_PAGER_POSITION_KEY, 0)
}
// 先异步加载分类数据
loadCategoriesAndSetupUI()
}
/**
* 异步加载分类数据并设置UI
*/
private fun loadCategoriesAndSetupUI() {
fragmentScope.launch {
try {
// 使用固定分类
categories = FIXED_CATEGORIES
Log.d(TAG, "使用固定分类: $categories")
// 初始化 ViewPager 和适配器
setupViewPager()
setupTabs()
} catch (e: Exception) {
Log.e(TAG, "初始化分类失败,使用默认分类", e)
categories = FIXED_CATEGORIES
setupViewPager()
setupTabs()
}
// 设置按钮和状态
setupRefreshButton()
setupScrollToTopButton()
binding.viewPager.post {
if (currentViewPagerPosition > 0 && _binding != null) {
binding.viewPager.setCurrentItem(currentViewPagerPosition, false)
}
updateScrollToTopButton()
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(VIEW_PAGER_POSITION_KEY, binding.viewPager.currentItem)
}
private fun setupViewPager() {
// 传递分类列表给适配器
pagerAdapter = HoPagerAdapter(this, categories)
viewPager = binding.viewPager
viewPager.adapter = pagerAdapter
viewPager.offscreenPageLimit = 2
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentViewPagerPosition = position
animateTabSelection(position)
// 切换标签页时更新滚动状态
updateScrollToTopButton()
// 异步预加载相邻页面的数据
preloadAdjacentPages(position)
}
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if (state == ViewPager2.SCROLL_STATE_IDLE) {
updateScrollToTopButton()
}
}
})
}
/**
* 预加载相邻页面的数据
*/
private fun preloadAdjacentPages(currentPosition: Int) {
val myApp = requireActivity().application as? MyApplication ?: return
fragmentScope.launch {
try {
// 预加载前一个页面
if (currentPosition > 0) {
val prevCategory = categories[currentPosition - 1]
val prevFileName = "${prevCategory}_two.json"
withContext(Dispatchers.IO) {
myApp.getWallpaperItems(prevFileName)
}
}
// 预加载后一个页面
if (currentPosition < categories.size - 1) {
val nextCategory = categories[currentPosition + 1]
val nextFileName = "${nextCategory}_two.json"
withContext(Dispatchers.IO) {
myApp.getWallpaperItems(nextFileName)
}
}
} catch (e: Exception) {
Log.w(TAG, "预加载相邻页面数据失败", e)
}
}
}
private fun setupTabs() {
val tabLayout = binding.tabLayout
val tabTitles = categories.toTypedArray()
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = tabTitles[position]
}.attach()
// 初始选中第一个标签
animateTabSelection(currentViewPagerPosition)
}
private fun setupRefreshButton() {
binding.btnRefresh.setOnClickListener {
if (!isRefreshing) {
startRefresh()
}
}
}
private fun setupScrollToTopButton() {
binding.btnScrollToTop.setOnClickListener {
scrollToTop()
}
binding.btnScrollToTop.visibility = View.GONE
}
private fun startRefresh() {
isRefreshing = true
binding.btnRefresh.isEnabled = false
startRefreshAnimation()
// 获取当前 Fragment 并刷新
val currentFragment = getCurrentFragment()
if (currentFragment != null) {
Log.d(TAG, "开始刷新当前Fragment: $currentFragment")
currentFragment.refreshWallpapers {
stopRefresh()
}
} else {
Log.w(TAG, "获取不到当前Fragment无法刷新")
stopRefresh()
}
}
private fun stopRefresh() {
isRefreshing = false
if (_binding != null) {
binding.btnRefresh.isEnabled = true
}
stopRefreshAnimation()
}
private fun startRefreshAnimation() {
if (_binding != null) {
binding.btnRefresh.setImageResource(R.drawable.loading_animation)
}
Log.d(TAG, "开始刷新动画")
}
private fun stopRefreshAnimation() {
if (_binding != null) {
binding.btnRefresh.setImageResource(R.drawable.ic_refreshit)
}
Log.d(TAG, "停止刷新动画")
}
private fun scrollToTop() {
val currentFragment = getCurrentFragment()
if (currentFragment != null) {
Log.d(TAG, "滚动到顶部当前Fragment: $currentFragment")
currentFragment.scrollToTop()
// 隐藏回到顶部按钮
if (_binding != null) {
binding.btnScrollToTop.visibility = View.GONE
}
} else {
Log.w(TAG, "获取不到当前Fragment无法滚动到顶部")
}
}
private fun getCurrentFragment(): WpGridFragment? {
if (!isAdded || _binding == null) return null
return try {
// 通过适配器获取当前 Fragment
val fragment = pagerAdapter.getFragment(viewPager.currentItem)
if (fragment == null) {
Log.w(TAG, "通过适配器获取Fragment失败尝试备用方法")
// 备用方法通过ViewPager的当前项获取Fragment
val fragmentTag = "f${viewPager.currentItem}"
childFragmentManager.findFragmentByTag(fragmentTag) as? WpGridFragment
} else {
fragment
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "获取Fragment异常: ${e.message}")
null
}
}
private fun updateScrollToTopButton() {
// 检查绑定是否有效
if (!isAdded || _binding == null) {
Log.d(TAG, "HomeFragment 未附加或绑定为空,跳过更新滚动按钮")
return
}
val currentFragment = getCurrentFragment()
if (currentFragment != null) {
Log.d(TAG, "更新滚动按钮状态当前Fragment: $currentFragment")
currentFragment.setOnScrollListener { canScrollUp ->
// 再次检查绑定是否仍然有效
if (!isAdded || _binding == null) return@setOnScrollListener
val shouldShowButton = canScrollUp
binding.btnScrollToTop.visibility = if (shouldShowButton) View.VISIBLE else View.GONE
Log.d(TAG, "滚动状态改变: canScrollUp = $canScrollUp, 按钮可见性 = ${if (shouldShowButton) "VISIBLE" else "GONE"}")
}
currentFragment.checkCurrentScrollState()
} else {
binding.btnScrollToTop.visibility = View.GONE
Log.w(TAG, "获取不到当前Fragment隐藏回到顶部按钮")
}
}
private fun animateTabSelection(position: Int) {
if (!isAdded || _binding == null) return
val tabCount = binding.tabLayout.tabCount
for (i in 0 until tabCount) {
val tab = binding.tabLayout.getTabAt(i)
val tabView = tab?.view
if (i == position) {
tabView?.animate()
?.scaleX(1.1f)
?.scaleY(1.1f)
?.setDuration(200)
?.setInterpolator(OvershootInterpolator())
?.start()
} else {
tabView?.animate()
?.scaleX(1.0f)
?.scaleY(1.0f)
?.setDuration(150)
?.start()
}
}
updateScrollToTopButton()
}
override fun onDestroyView() {
super.onDestroyView()
stopRefreshAnimation()
_binding = null
// 取消所有协程
fragmentScope.coroutineContext.cancelChildren()
}
}

View File

@ -0,0 +1,356 @@
package com.gallery.free.wallpaper.fragment
import android.Manifest
import android.R
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.gallery.free.wallpaper.LocalPreActivity
import com.gallery.free.wallpaper.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
class SeFragment : Fragment() {
private var currentRating = 0
private var loadingJob: Job? = null
private val coroutineScope = CoroutineScope(Dispatchers.Main)
companion object {
private const val REQUEST_GALLERY_PERMISSION = 1001
private const val REQUEST_PREVIEW = 1002
private const val TAG = "SettingFragment"
const val EXTRA_FROM_PREVIEW = "from_preview"
fun newInstance(): SeFragment {
return SeFragment()
}
}
private val galleryLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
Log.d(TAG, "galleryLauncher返回: resultCode=${result.resultCode}")
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val uri = result.data?.data
if (uri != null) {
Log.d(TAG, "从图库选择了图片: $uri")
val intent = Intent(requireContext(), LocalPreActivity::class.java).apply {
putExtra(LocalPreActivity.Companion.EXTRA_IMAGE_URI, uri.toString())
putExtra(LocalPreActivity.Companion.EXTRA_FROM_SETTING, true)
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
}
startActivity(intent)
activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
activity?.finish()
} else {
showToast("Failed to get image")
}
} else {
Log.d(TAG, "用户取消了图库选择")
val intent = Intent(requireContext(), MainActivity::class.java).apply {
putExtra("SHOW_SETTING_FRAGMENT", true)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
requireContext().startActivity(intent)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode")
when (requestCode) {
LocalPreActivity.Companion.RESULT_SET_SUCCESS -> {
// 处理预览页面返回结果
when (resultCode) {
LocalPreActivity.Companion.RESULT_SET_SUCCESS -> {
Log.d(TAG, "壁纸设置成功,已返回设置页面")
showToast("壁纸设置成功!")
}
Activity.RESULT_CANCELED -> {
Log.d(TAG, "用户取消了壁纸设置")
}
else -> {
Log.d(TAG, "预览页面返回未知结果: $resultCode")
}
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(com.gallery.free.wallpaper.R.layout.fragment_se, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setVersionNumber(view)
setupRateUsClickListeners(view)
setupChoosePhotosClickListeners(view)
checkIfReturnedFromPreview()
}
private fun checkIfReturnedFromPreview() {
val activity = activity as? MainActivity
activity?.intent?.let { intent ->
if (intent.hasExtra("SHOW_SETTING_FRAGMENT")) {
Log.d(TAG, "从预览页面返回显示SettingFragment")
intent.removeExtra("SHOW_SETTING_FRAGMENT")
}
}
}
override fun onResume() {
super.onResume()
Log.d(TAG, "SettingFragment恢复显示")
checkIfReturnedFromPreview()
}
override fun onDestroyView() {
super.onDestroyView()
loadingJob?.cancel()
}
@SuppressLint("SetTextI18n")
private fun setVersionNumber(view: View) {
try {
val packageInfo = requireContext().packageManager
.getPackageInfo(requireContext().packageName, 0)
val versionName = packageInfo.versionName
val versionValue = view.findViewById<TextView>(com.gallery.free.wallpaper.R.id.versionValue)
if (versionValue != null) {
versionValue.text = "$versionName"
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
val versionValue = view.findViewById<TextView>(com.gallery.free.wallpaper.R.id.versionValue)
if (versionValue != null) {
versionValue.text = "1.0"
}
}
}
private fun setupChoosePhotosClickListeners(view: View) {
val choosePhotosLayout = view.findViewById<View>(com.gallery.free.wallpaper.R.id.choosePhotosLayout)
val choosePhotosText = view.findViewById<View>(com.gallery.free.wallpaper.R.id.choosePhotosText)
val choosePhotosIcon = view.findViewById<View>(com.gallery.free.wallpaper.R.id.choosePhotosIcon)
val choosePhotosArrow = view.findViewById<View>(com.gallery.free.wallpaper.R.id.choosePhotosArrow)
val clickListener = View.OnClickListener {
openGalleryForImage()
}
choosePhotosLayout?.setOnClickListener(clickListener)
choosePhotosText?.setOnClickListener(clickListener)
choosePhotosIcon?.setOnClickListener(clickListener)
choosePhotosArrow?.setOnClickListener(clickListener)
}
private fun setupRateUsClickListeners(view: View) {
val rateUsLayout = view.findViewById<View>(com.gallery.free.wallpaper.R.id.rateUsLayout)
val rateUsText = view.findViewById<View>(com.gallery.free.wallpaper.R.id.rateUsText)
val rateUsIcon = view.findViewById<View>(com.gallery.free.wallpaper.R.id.rateUsIcon)
val rateUsArrow = view.findViewById<View>(com.gallery.free.wallpaper.R.id.rateUsArrow)
val clickListener = View.OnClickListener { showRateUsDialog() }
rateUsLayout?.setOnClickListener(clickListener)
rateUsText?.setOnClickListener(clickListener)
rateUsIcon?.setOnClickListener(clickListener)
rateUsArrow?.setOnClickListener(clickListener)
if (rateUsLayout == null && rateUsText == null && rateUsIcon == null && rateUsArrow == null) {
println("Warning: No rate us views found in fragment_setting_two.xml")
}
}
private fun openGalleryForImage() {
// 检查权限
if (hasGalleryPermission()) {
launchGallery()
} else {
requestGalleryPermission()
}
}
private fun getGalleryPermission(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
}
private fun hasGalleryPermission(): Boolean {
return ContextCompat.checkSelfPermission(
requireContext(),
getGalleryPermission()
) == PackageManager.PERMISSION_GRANTED
}
private fun requestGalleryPermission() {
val permission = getGalleryPermission()
if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), permission)) {
// 解释为什么需要权限
showPermissionRationaleDialog(permission)
} else {
// 直接请求权限
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permission),
REQUEST_GALLERY_PERMISSION
)
}
}
private fun showPermissionRationaleDialog(permission: String) {
AlertDialog.Builder(requireContext())
.setTitle("Permission Required")
.setMessage("Need permission to access your photos to set as wallpaper")
.setPositiveButton("OK") { _, _ ->
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permission),
REQUEST_GALLERY_PERMISSION
)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun launchGallery() {
try {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
// 使用registerForActivityResult的方式
galleryLauncher.launch(intent)
} catch (e: Exception) {
e.printStackTrace()
showToast("Cannot open gallery")
}
}
private fun showToast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_GALLERY_PERMISSION -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
launchGallery()
} else {
showToast("Permission denied")
}
}
}
}
private fun showRateUsDialog() {
try {
val dialogView = LayoutInflater.from(requireContext()).inflate(com.gallery.free.wallpaper.R.layout.dialog_rateus, null)
val dialog = AlertDialog.Builder(requireContext())
.setView(dialogView)
.setCancelable(true)
.create()
dialog.window?.setBackgroundDrawableResource(R.color.transparent)
currentRating = 0
val btnRate = dialogView.findViewById<Button>(com.gallery.free.wallpaper.R.id.btn_rate)
btnRate?.isEnabled = false
setupStarClickListeners(dialogView, btnRate)
val btnCancel = dialogView.findViewById<View>(com.gallery.free.wallpaper.R.id.btn_cancel)
btnCancel?.setOnClickListener {
dialog.dismiss()
}
btnRate?.setOnClickListener {
if (currentRating > 0) {
dialog.dismiss()
}
}
dialog.show()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setupStarClickListeners(dialogView: View, rateButton: Button?) {
val stars = listOf(
dialogView.findViewById<ImageView>(com.gallery.free.wallpaper.R.id.star1),
dialogView.findViewById<ImageView>(com.gallery.free.wallpaper.R.id.star2),
dialogView.findViewById<ImageView>(com.gallery.free.wallpaper.R.id.star3),
dialogView.findViewById<ImageView>(com.gallery.free.wallpaper.R.id.star4),
dialogView.findViewById<ImageView>(com.gallery.free.wallpaper.R.id.star5)
)
if (stars.any { it == null }) {
println("Warning: Some star views are missing in dialog_rate_us_two.xml")
return
}
stars.forEachIndexed { index, star ->
star?.setOnClickListener {
val clickedRating = index + 1
updateStarRating(stars, clickedRating, rateButton)
}
}
}
private fun updateStarRating(stars: List<ImageView?>, rating: Int, rateButton: Button?) {
currentRating = rating
stars.forEachIndexed { index, star ->
if (index < rating) {
star?.setImageResource(com.gallery.free.wallpaper.R.drawable.star_picked)
} else {
star?.setImageResource(com.gallery.free.wallpaper.R.drawable.star_disabled)
}
}
if (rating > 0) {
rateButton?.isEnabled = true
rateButton?.setBackgroundResource(com.gallery.free.wallpaper.R.drawable.button_rateit)
} else {
rateButton?.isEnabled = false
rateButton?.setBackgroundResource(com.gallery.free.wallpaper.R.drawable.button_rateit_disa)
}
}
}

View File

@ -0,0 +1,390 @@
package com.gallery.free.wallpaper.fragment
import android.R
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.gallery.free.wallpaper.DetailwpActivity
import com.gallery.free.wallpaper.MyApplication
import com.gallery.free.wallpaper.StaggeredGridSpacingItemDecoration
import com.gallery.free.wallpaper.Utils_json
import com.gallery.free.wallpaper.WpAdapter
import com.gallery.free.wallpaper.WpItem
import com.gallery.free.wallpaper.databinding.FragmentWpgridBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class WpGridFragment : Fragment() {
private var _binding: FragmentWpgridBinding? = null
private val binding get() = _binding!!
private lateinit var category: String
private var wallpaperList: List<WpItem> = emptyList()
private var adapter: WpAdapter? = null
// 滚动监听回调
private var scrollListener: ((Boolean) -> Unit)? = null
// 添加协程作用域
private val fragmentScope = CoroutineScope(Dispatchers.Main + Job())
companion object {
private const val ARG_CATEGORY = "category"
private const val TAG = "WallpaperGridFragment"
fun newInstance(category: String): WpGridFragment {
val fragment = WpGridFragment()
val args = Bundle()
args.putString(ARG_CATEGORY, category)
fragment.arguments = args
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
category = it.getString(ARG_CATEGORY, "Film")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWpgridBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupWallpaperGrid()
}
// 设置滚动监听回调
fun setOnScrollListener(listener: (Boolean) -> Unit) {
this.scrollListener = listener
}
fun checkCurrentScrollState() {
if (!isAdded || _binding == null) {
// Fragment 未附加或绑定为null直接返回
scrollListener?.invoke(false)
return
}
try {
val canScrollUp = binding.wallpaperRecyclerView.canScrollVertically(-1)
Log.d(TAG, "checkCurrentScrollState: canScrollUp = $canScrollUp")
scrollListener?.invoke(canScrollUp)
} catch (e: Exception) {
e.printStackTrace()
scrollListener?.invoke(false)
}
}
// 刷新壁纸
fun refreshWallpapers(onComplete: () -> Unit) {
fragmentScope.launch {
try {
if (!isAdded || _binding == null) {
onComplete()
return@launch
}
// 使用 Application 获取数据
val fileName = when (category) {
"Film" -> "Film_two.json"
"Nature" -> "Nature_two.json"
"Travel" -> "Travel_two.json"
else -> "Film_two.json"
}
val myApp = activity?.application as? MyApplication
if (myApp != null) {
// 从 Application 缓存获取数据
val wallpaperItems = withContext(Dispatchers.IO) {
try {
myApp.getWallpaperItems(fileName)
} catch (e: Exception) {
// 如果缓存失败,回退到原始方法
Log.w(TAG, "从缓存获取数据失败,回退到原始方法", e)
val wallpaperJsonList = Utils_json.loadWallpapersFromAssets(requireContext(), fileName)
wallpaperJsonList.map { json ->
Utils_json.convertToWallpaperItem(json)
}
}
}
if (wallpaperItems.isEmpty()) {
showErrorState()
onComplete()
return@launch
}
wallpaperList = wallpaperItems
// 重新打乱壁纸顺序
val shuffledList = wallpaperList.shuffled()
val newAdapter = WpAdapter(shuffledList) { position: Int, wallpaper: WpItem ->
if (!isAdded) return@WpAdapter
val intent = Intent(requireContext(), DetailwpActivity::class.java).apply {
putExtra("wallpaper_url", wallpaper.fullUrl)
putExtra("wallpaper_description", wallpaper.description)
// 传递完整的WpItem列表
putParcelableArrayListExtra(
"wallpaper_item_list",
ArrayList(shuffledList)
)
putExtra("current_position", position)
}
startActivity(intent)
requireActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
// 更新适配器
binding.wallpaperRecyclerView.adapter = newAdapter
adapter = newAdapter
// 检查滚动状态
checkCurrentScrollState()
onComplete()
} else {
// Application 不可用,使用原始方法
loadWallpapersWithOriginalMethod(onComplete)
}
} catch (e: Exception) {
e.printStackTrace()
if (isAdded && _binding != null) {
showErrorState()
}
onComplete()
}
}
}
// 使用原始方法加载壁纸(兼容性回退)
private fun loadWallpapersWithOriginalMethod(onComplete: () -> Unit) {
Handler(Looper.getMainLooper()).postDelayed({
try {
if (!isAdded || _binding == null) {
onComplete()
return@postDelayed
}
val fileName = when (category) {
"Film" -> "Film_two.json"
"Nature" -> "Nature_two.json"
"Travel" -> "Travel_two.json"
else -> "Film_two.json"
}
val wallpaperJsonList = Utils_json.loadWallpapersFromAssets(requireContext(), fileName)
if (wallpaperJsonList.isEmpty()) {
showErrorState()
onComplete()
return@postDelayed
}
wallpaperList = wallpaperJsonList.map { json ->
Utils_json.convertToWallpaperItem(json)
}
// 重新打乱壁纸顺序
val shuffledList = wallpaperList.shuffled()
val newAdapter = WpAdapter(shuffledList) { position: Int, wallpaper: WpItem ->
if (!isAdded) return@WpAdapter
val intent = Intent(requireContext(), DetailwpActivity::class.java).apply {
putExtra("wallpaper_url", wallpaper.fullUrl)
putExtra("wallpaper_description", wallpaper.description)
// 传递完整的WpItem列表
putParcelableArrayListExtra("wallpaper_item_list", ArrayList(shuffledList))
putExtra("current_position", position)
}
startActivity(intent)
requireActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
// 更新适配器
binding.wallpaperRecyclerView.adapter = newAdapter
adapter = newAdapter
checkCurrentScrollState()
onComplete()
} catch (e: Exception) {
e.printStackTrace()
if (isAdded && _binding != null) {
showErrorState()
}
onComplete()
}
}, 1000)
}
// 回到顶部
fun scrollToTop() {
if (!isAdded || _binding == null) return
try {
binding.wallpaperRecyclerView.smoothScrollToPosition(0)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setupWallpaperGrid() {
fragmentScope.launch {
try {
val fileName = when (category) {
"Film" -> "Film_two.json"
"Nature" -> "Nature_two.json"
"Travel" -> "Travel_two.json"
else -> "Film_two.json"
}
// 使用 Application 获取数据
val myApp = activity?.application as? MyApplication
val wallpaperItems = if (myApp != null) {
// 从 Application 缓存获取数据
withContext(Dispatchers.IO) {
try {
myApp.getWallpaperItems(fileName)
} catch (e: Exception) {
// 如果缓存失败,回退到原始方法
Log.w(TAG, "从缓存获取数据失败,回退到原始方法", e)
val wallpaperJsonList = Utils_json.loadWallpapersFromAssets(requireContext(), fileName)
wallpaperJsonList.map { json ->
Utils_json.convertToWallpaperItem(json)
}
}
}
} else {
// Application 不可用,使用原始方法
val wallpaperJsonList = Utils_json.loadWallpapersFromAssets(requireContext(), fileName)
wallpaperJsonList.map { json ->
Utils_json.convertToWallpaperItem(json)
}
}
if (wallpaperItems.isEmpty()) {
showErrorState()
return@launch
}
wallpaperList = wallpaperItems
val shuffledList = wallpaperList.shuffled()
// 使用StaggeredGridLayoutManager瀑布流 每行2列
val spanCount = 2
val layoutManager = StaggeredGridLayoutManager(
spanCount,
StaggeredGridLayoutManager.VERTICAL
)
// 设置间距均匀
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_NONE
binding.wallpaperRecyclerView.layoutManager = layoutManager
val spacingInPixels = resources.getDimensionPixelSize(com.gallery.free.wallpaper.R.dimen.grid_spacing)
binding.wallpaperRecyclerView.addItemDecoration(
StaggeredGridSpacingItemDecoration(spanCount, spacingInPixels)
)
adapter = WpAdapter(shuffledList) { position: Int, wallpaper: WpItem ->
if (!isAdded) return@WpAdapter
val intent = Intent(requireContext(), DetailwpActivity::class.java).apply {
putExtra("wallpaper_url", wallpaper.fullUrl) // 传递全尺寸URL
putExtra("wallpaper_description", wallpaper.description) // 传递描述
putParcelableArrayListExtra("wallpaper_item_list", ArrayList(shuffledList))
putExtra("current_position", position)
}
startActivity(intent)
requireActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
binding.wallpaperRecyclerView.adapter = adapter
// 设置滚动监听
setupScrollListener()
// 初始检查滚动状态
binding.wallpaperRecyclerView.post {
checkCurrentScrollState()
}
} catch (e: Exception) {
Log.e(TAG, "设置壁纸网格失败", e)
showErrorState()
}
}
}
private fun setupScrollListener() {
binding.wallpaperRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
// 检查 Fragment 是否仍然有效
if (!isAdded || _binding == null) return
try {
val canScrollUp = recyclerView.canScrollVertically(-1)
Log.d(TAG, "onScrolled: canScrollUp = $canScrollUp, dy = $dy")
scrollListener?.invoke(canScrollUp)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!isAdded || _binding == null) return
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
try {
val canScrollUp = recyclerView.canScrollVertically(-1)
Log.d(TAG, "scroll state changed: canScrollUp = $canScrollUp")
scrollListener?.invoke(canScrollUp)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
})
}
private fun showErrorState() {
if (isAdded) {
Toast.makeText(requireContext(), "加载失败...", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroyView() {
super.onDestroyView()
// 清除滚动监听回调
scrollListener = null
_binding = null
adapter = null
// 取消所有协程
fragmentScope.coroutineContext.cancelChildren()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#CCCCCCFF" />
<corners android:radius="20dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="#F5F5F5" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="@color/rate_button" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="#CCCCCC" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="16dp" />
<solid android:color="@color/light_purple" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_loadingpic"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
<font
android:fontStyle="normal"
android:fontWeight="400"
android:font="@font/alimama_shuheiti_" />
</font-family>

Binary file not shown.

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gallery.free.wallpaper.DetailwpActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/wallpaperViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/button_prev"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:background="@drawable/bg_icon"
android:src="@drawable/ic_left_disabled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:saturation="1.0"
app:brightness="0.0"
app:contrast="1.0" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/button_next"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_icon"
android:src="@drawable/ic_right_disabled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:saturation="1.0"
app:brightness="0.0"
app:contrast="1.0" />
<ImageView
android:id="@+id/imageview_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="15dp"
android:layout_marginTop="20dp"
android:background="@drawable/bg_icon"
android:src="@drawable/close_detailwp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="80dp"
android:background="@drawable/bg_icon"
android:orientation="horizontal"
android:gravity="center"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/button_like"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_weight="1"
android:src="@drawable/ic_like_disabled"
android:padding="8dp" />
<ImageView
android:id="@+id/button_set_wallpaper"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_weight="1"
android:src="@drawable/ic_setwp"
android:padding="8dp" />
<ImageView
android:id="@+id/button_save"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_weight="1"
android:src="@drawable/ic_savewp"
android:padding="8dp"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gallery.free.wallpaper.LocalPreActivity">
<ImageView
android:id="@+id/imageView_preview"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:contentDescription="Preview image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/button_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="15dp"
android:layout_marginTop="20dp"
android:background="@drawable/bg_icon"
android:src="@drawable/close_detailwp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:saturation="1.0"
app:brightness="0.0"
app:contrast="1.0" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="80dp"
android:background="@drawable/bg_icon"
android:orientation="horizontal"
android:gravity="center"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/button_set_wallpaper"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_weight="1"
android:src="@drawable/ic_setwp"
android:padding="8dp"
app:saturation="1.0"
app:brightness="0.0"
app:contrast="1.0" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bgimage"
android:paddingTop="15dp"
android:orientation="vertical">
<!-- Fragment 容器 -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 底部导航栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:elevation="8dp">
<!-- Home 按钮 -->
<LinearLayout
android:id="@+id/homeLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/homeIcon"
android:layout_width="35dp"
android:layout_height="35dp"
android:src="@drawable/ic_home_disabled" />
</LinearLayout>
<!-- Collection 按钮 -->
<LinearLayout
android:id="@+id/collectionLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/collectionIcon"
android:layout_width="35dp"
android:layout_height="35dp"
android:src="@drawable/ic_collection_disabled" />
</LinearLayout>
<!-- Settings 按钮 -->
<LinearLayout
android:id="@+id/settingsLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/settingsIcon"
android:layout_width="35dp"
android:layout_height="35dp"
android:src="@drawable/ic_settings_disabled" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gallery.free.wallpaper.SplaActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/splaimage" />
<ImageView
android:id="@+id/imageview_app_icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="250dp"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/textview_appname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/imageview_app_icon"
android:layout_centerHorizontal="true"
android:layout_marginTop="18dp"
android:fontFamily="@font/alimama_shuheiti"
android:text="@string/app_name"
android:shadowColor="#000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="4"
android:textColor="#FFD8B4FF"
android:textSize="30sp"
app:apply_font="true" />
<ProgressBar
android:id="@+id/progressbar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="250dp"
android:layout_height="10dp"
android:layout_below="@id/textview_appname"
android:layout_centerHorizontal="true"
android:layout_marginTop="26dp"
android:max="100"
android:progress="1"
android:progressBackgroundTint="@color/under_splash"
android:progressTint="@color/progressbar_color" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/dialog_bg">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Rate us"
android:textColor="@color/black"
android:textSize="20sp"
android:fontFamily="@font/alimama_shuheiti"
android:gravity="center"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="We hope this app is useful for you, if it does, would you please give us a 5 star and a nice review on Google Play, it really helps!"
android:textColor="@color/text_color"
android:fontFamily="@font/alimama_shuheiti"
android:textSize="12sp"
android:lineSpacingExtra="4dp"
android:gravity="center"
android:layout_marginBottom="18dp" />
<LinearLayout
android:id="@+id/star_rating_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginBottom="18dp">
<ImageView
android:id="@+id/star1"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
android:src="@drawable/star_disabled"
android:tag="1" />
<ImageView
android:id="@+id/star2"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
android:src="@drawable/star_disabled"
android:tag="2" />
<ImageView
android:id="@+id/star3"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
android:src="@drawable/star_disabled"
android:tag="3" />
<ImageView
android:id="@+id/star4"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
android:src="@drawable/star_disabled"
android:tag="4" />
<ImageView
android:id="@+id/star5"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
android:src="@drawable/star_disabled"
android:tag="5" />
</LinearLayout>
<!-- 按钮布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/btn_cancel"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="cancel"
android:textColor="@color/text_color"
android:fontFamily="@font/alimama_shuheiti"
android:background="@drawable/button_cancel"
android:textAllCaps="true" />
<Button
android:id="@+id/btn_rate"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="RATE IT"
android:textColor="@color/white"
android:fontFamily="@font/alimama_shuheiti"
android:background="@drawable/button_rateit_disa"
android:textAllCaps="true"
android:enabled="false" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/dialog_bg">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:padding="8dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/layout_home"
android:layout_width="match_parent"
android:layout_height="54dp"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/dialog_homescreen" />
<TextView
android:id="@+id/home"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="Home Screen"
android:fontFamily="@font/alimama_shuheiti"
android:textColor="@color/text_color"
android:textSize="18sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:background="#B3000000" />
<LinearLayout
android:id="@+id/layout_lock"
android:layout_width="match_parent"
android:layout_height="54dp"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/dialog_lockscreen" />
<TextView
android:id="@+id/lock"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="Lock Screen"
android:fontFamily="@font/alimama_shuheiti"
android:textColor="@color/text_color"
android:textSize="18sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:background="#B3000000" />
<LinearLayout
android:id="@+id/layout_both"
android:layout_width="match_parent"
android:layout_height="54dp"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="31dp"
android:layout_height="31dp"
android:layout_marginStart="10dp"
android:src="@drawable/dialog_bothtwo" />
<TextView
android:id="@+id/both"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="Both"
android:fontFamily="@font/alimama_shuheiti"
android:textColor="@color/text_color"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.45">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_like_disabled"
android:tint="@color/light_gray" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="No Favourite Wallpaper"
android:fontFamily="@font/alimama_shuheiti"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Go Back To Homepage and Find More"
android:fontFamily="@font/alimama_shuheiti"
android:textColor="@color/text_color"
android:textSize="14sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.gallery.free.wallpaper.fragment.ColleFragment">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:layout_marginBottom="10dp"
android:elevation="4dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Collection"
android:letterSpacing="0.04"
android:textColor="@color/black"
android:fontFamily="@font/alimama_shuheiti"
android:textSize="24sp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/text_favorite_count_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="8dp"
android:visibility="gone">
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:background="@color/text_color"
android:layout_marginStart="30dp"
android:layout_marginEnd="16dp" />
<TextView
android:id="@+id/text_favorite_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 photos collected"
android:textColor="@color/text_color"
android:textSize="16sp" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:background="@color/text_color"
android:layout_marginEnd="30dp"
android:layout_marginStart="16dp" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/wallpaperRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="1dp"
android:scrollbars="vertical" />
<!-- 空状态 -->
<include
android:id="@+id/empty_likes_layout"
layout="@layout/empty_colle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@android:color/transparent"
app:tabGravity="start"
app:tabIndicatorHeight="0dp"
app:tabInlineLabel="true"
app:tabMinWidth="0dp"
app:tabMode="scrollable"
app:tabPaddingEnd="20dp"
app:tabRippleColor="@android:color/transparent"
app:tabSelectedTextColor="@color/black"
app:tabTextAppearance="@style/MyTabTextAppearance"
app:tabTextColor="#A0A0A0" />
</RelativeLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<!-- 悬浮按钮容器 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="80dp"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/btn_scroll_to_top"
android:layout_width="45dp"
android:layout_height="45dp"
android:padding="5dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_icon"
android:src="@drawable/ic_top"
android:visibility="gone"
android:clickable="true"
android:focusable="true" />
<ImageView
android:id="@+id/btn_refresh"
android:layout_width="45dp"
android:layout_height="45dp"
android:padding="8dp"
android:background="@drawable/bg_icon"
android:src="@drawable/ic_refreshit"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="38dp"
tools:context="com.gallery.free.wallpaper.fragment.SeFragment">
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/alimama_shuheiti"
android:letterSpacing="0.04"
android:text="Settings"
android:textColor="@color/black"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Choose from Photos -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/choosePhotosLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleText">
<ImageView
android:id="@+id/choosePhotosIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/localpho_settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/choosePhotosText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/alimama_shuheiti"
android:gravity="center_vertical"
android:text="Choose from Local"
android:textColor="@color/text_color"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/choosePhotosArrow"
app:layout_constraintStart_toEndOf="@id/choosePhotosIcon"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/choosePhotosArrow"
android:layout_width="28dp"
android:layout_height="28dp"
android:background="?android:attr/selectableItemBackground"
android:scaleType="fitCenter"
android:src="@drawable/arrow_setting"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/dividerChoosePhotos"
android:layout_width="0dp"
android:layout_height="1.3dp"
android:layout_marginTop="8dp"
android:background="#B3000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/choosePhotosLayout" />
<!-- Rate Us -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rateUsLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dividerChoosePhotos">
<ImageView
android:id="@+id/rateUsIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/rate_settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/rateUsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/alimama_shuheiti"
android:gravity="center_vertical"
android:text="Rate us"
android:textColor="@color/text_color"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/rateUsArrow"
app:layout_constraintStart_toEndOf="@id/rateUsIcon"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/rateUsArrow"
android:layout_width="28dp"
android:layout_height="28dp"
android:background="?android:attr/selectableItemBackground"
android:scaleType="fitCenter"
android:src="@drawable/arrow_setting"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1.3dp"
android:layout_marginTop="8dp"
android:background="#B3000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rateUsLayout" />
<!-- Version -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/versionLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="?android:attr/selectableItemBackground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider1">
<ImageView
android:id="@+id/versionIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/version_settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/versionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/alimama_shuheiti"
android:gravity="center_vertical"
android:text="Version"
android:textColor="@color/text_color"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/versionIcon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/versionValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/alimama_shuheiti"
android:gravity="center_vertical"
android:textColor="@color/text_color"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider3"
android:layout_width="0dp"
android:layout_height="1.3dp"
android:layout_marginTop="8dp"
android:background="#B3000000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/versionLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text_wallpaper_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:textSize="16sp"
android:textColor="@color/text_color"
android:gravity="center"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/wallpaperRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="2dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp">
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="0dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="500dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/textContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="vertical"
android:paddingHorizontal="10dp"
android:paddingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView">
<TextView
android:id="@+id/descriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lineSpacingExtra="1dp"
android:fontFamily="@font/alimama_shuheiti"
android:maxLines="2"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/pager_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/loading_progress"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:src="@drawable/loading_animation"
android:visibility="visible" />
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Some files were not shown because too many files have changed in this diff Show More