├── sample ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── ic_discover_more.png │ │ │ │ ├── ic_discover_next_card_back.png │ │ │ │ └── ic_discover_next_card_right.png │ │ │ ├── drawable │ │ │ │ ├── bg_oval.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── dialog_setting.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── item_card.xml │ │ │ │ └── layout_card.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── me │ │ │ │ └── hiten │ │ │ │ └── jkcardlayout │ │ │ │ └── sample │ │ │ │ ├── CardEntity.kt │ │ │ │ ├── ToolBarEntity.kt │ │ │ │ ├── Ext.kt │ │ │ │ ├── CardAdapter.kt │ │ │ │ ├── MockData.kt │ │ │ │ ├── SettingDialogFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── PullDownLayout.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── me │ │ │ └── hiten │ │ │ └── jkcardlayout │ │ │ └── sample │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── me │ │ └── hiten │ │ └── jkcardlayout │ │ └── sample │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── library ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── java │ │ │ └── me │ │ │ └── hiten │ │ │ └── jkcardlayout │ │ │ ├── OnCardLayoutListener.java │ │ │ ├── Utils.java │ │ │ ├── ItemTouchUIUtilImpl.java │ │ │ ├── CardAnimatorManager.java │ │ │ ├── JKCardLayoutManager.java │ │ │ └── CardLayoutHelper.java │ ├── test │ │ └── java │ │ │ └── me │ │ │ └── hiten │ │ │ └── jkcardlayout │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── me │ │ └── hiten │ │ └── jkcardlayout │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library', ':sample' 2 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 即刻探索页 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | library 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/CardEntity.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | data class CardEntity(val picUrl:String, val text:String) -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_discover_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/drawable-xxhdpi/ic_discover_more.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_discover_next_card_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/drawable-xxhdpi/ic_discover_next_card_back.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_discover_next_card_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HitenDev/JKCardLayout/HEAD/sample/src/main/res/drawable-xxhdpi/ic_discover_next_card_right.png -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/ToolBarEntity.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | data class ToolBarEntity(val picUrl:String, val title:String,val url:String? = "") -------------------------------------------------------------------------------- /sample/src/main/res/drawable/bg_oval.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /library/src/main/java/me/hiten/jkcardlayout/OnCardLayoutListener.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | public interface OnCardLayoutListener { 4 | void onSwipe(float dx,float dy); 5 | 6 | void onStateChanged(CardLayoutHelper.State state); 7 | } 8 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 13 11:23:50 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /library/src/main/java/me/hiten/jkcardlayout/Utils.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | import android.content.res.Resources; 4 | 5 | class Utils { 6 | 7 | static int dp2px(float dpValue) { 8 | return (int) (0.5f + dpValue * Resources.getSystem().getDisplayMetrics().density); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/test/java/me/hiten/jkcardlayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/Ext.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | import android.content.res.Resources 4 | 5 | val Float.dp: Float 6 | get() = android.util.TypedValue.applyDimension( 7 | android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics) 8 | 9 | 10 | val Int.dp: Int 11 | get() = (android.util.TypedValue.applyDimension( 12 | android.util.TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics) + 0.5f).toInt() -------------------------------------------------------------------------------- /sample/src/test/java/me/hiten/jkcardlayout/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/dialog_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /library/src/androidTest/java/me/hiten/jkcardlayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("me.hiten.jkcardlayout.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/me/hiten/jkcardlayout/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("me.hiten.jkcardlayout.sample", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | #android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | #android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion compile_version 5 | 6 | defaultConfig { 7 | minSdkVersion min_version 8 | targetSdkVersion target_version 9 | versionCode library_version_code 10 | versionName library_version 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation "com.android.support:appcompat-v7:$support_version" 27 | implementation "com.android.support:recyclerview-v7:$support_version" 28 | testImplementation 'junit:junit:4.12' 29 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 30 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 31 | } 32 | 33 | if(publish_switch){ 34 | apply plugin: 'com.novoda.bintray-release' 35 | 36 | publish{ 37 | userOrg = 'hiten' 38 | groupId = 'me.hiten' 39 | artifactId = 'jkcardlayout' 40 | publishVersion = library_version 41 | desc = 'JKCardLayout' 42 | website = 'https://github.com/HitenDev/JKCardLayout' 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 22 | 27 | 38 | 39 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | 5 | android { 6 | compileSdkVersion compile_version 7 | 8 | defaultConfig { 9 | applicationId "me.hiten.jkcardlayout.sample" 10 | minSdkVersion min_version 11 | targetSdkVersion target_version 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(include: ['*.jar'], dir: 'libs') 30 | implementation "com.android.support:appcompat-v7:$support_version" 31 | implementation "com.android.support:recyclerview-v7:$support_version" 32 | implementation "com.android.support.constraint:constraint-layout:$constraint_layout_version" 33 | implementation "com.android.support:cardview-v7:$support_version" 34 | implementation "com.github.bumptech.glide:glide:$glide_version" 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 36 | testImplementation 'junit:junit:4.12' 37 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 38 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 39 | implementation project(':library') 40 | } 41 | -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/CardAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import android.widget.Toast 10 | import com.bumptech.glide.Glide 11 | import com.bumptech.glide.request.RequestOptions 12 | 13 | class CardAdapter(data: List) : RecyclerView.Adapter() { 14 | 15 | 16 | private var mList : List = data 17 | 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { 20 | return VH(LayoutInflater.from(parent.context).inflate(R.layout.item_card, parent, false)) 21 | } 22 | 23 | override fun getItemCount(): Int { 24 | return mList.size 25 | } 26 | 27 | override fun onBindViewHolder(holder: VH, position: Int) { 28 | val exploreEntity = mList[position] 29 | holder.textView.text = exploreEntity.text 30 | val noAnimation = RequestOptions.noAnimation() 31 | Glide.with(holder.itemView.context).load(exploreEntity.picUrl).apply(noAnimation).into(holder.bgIv) 32 | holder.itemView.setOnClickListener { 33 | Toast.makeText(it.context,exploreEntity.text,Toast.LENGTH_SHORT).show() 34 | } 35 | } 36 | 37 | class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { 38 | 39 | val textView:TextView = itemView.findViewById(R.id.tv_text) 40 | val bgIv:ImageView = itemView.findViewById(R.id.iv_background) 41 | 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 16 | 17 | 22 | 23 | 27 | 28 | 29 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/MockData.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import org.json.JSONObject 7 | 8 | object MockData { 9 | 10 | 11 | fun getCards(context: Context, callBack: (Map) -> Unit) { 12 | 13 | Thread(Runnable { 14 | val arrayList = ArrayList() 15 | val inputStream = context.resources.assets.open("jk_daily_cards.json") 16 | val content = String(inputStream.readBytes()) 17 | 18 | 19 | val jsonObject = JSONObject(content) 20 | val cards = jsonObject.optJSONObject("data")?.optJSONArray("cards") 21 | cards?.let { 22 | for (index in 0 until it.length()) { 23 | val card = it.optJSONObject(index) 24 | val optJSONObject = card?.optJSONObject("originalPost") 25 | val contentStr = optJSONObject?.optString("content") 26 | val picUrl = optJSONObject?.optJSONArray("pictures")?.optJSONObject(0)?.optString("middlePicUrl") 27 | if (contentStr != null && picUrl != null) { 28 | arrayList.add(CardEntity(picUrl, contentStr)) 29 | } 30 | } 31 | } 32 | 33 | val toolbarList = ArrayList() 34 | val toolbars = jsonObject.optJSONObject("data")?.optJSONArray("toolbarItems") 35 | toolbars?.let { 36 | for (index in 0 until it.length()) { 37 | val toolbar = it.optJSONObject(index) 38 | val url = toolbar?.optString("url") 39 | val picUrl = toolbar?.optString("picUrl") 40 | val title = toolbar?.optString("title") 41 | if (title != null && picUrl != null) { 42 | toolbarList.add(ToolBarEntity(picUrl,title,url)) 43 | } 44 | } 45 | } 46 | Handler(Looper.getMainLooper()).post { 47 | val map = HashMap() 48 | map["cards"] = arrayList 49 | map["toolbarItems"] = toolbarList 50 | callBack(map) 51 | } 52 | }).start() 53 | } 54 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /library/src/main/java/me/hiten/jkcardlayout/ItemTouchUIUtilImpl.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | import android.graphics.Canvas; 4 | import android.os.Build; 5 | import android.support.v4.view.ViewCompat; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.helper.ItemTouchUIUtil; 8 | import android.view.View; 9 | 10 | 11 | class ItemTouchUIUtilImpl implements ItemTouchUIUtil { 12 | static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl(); 13 | 14 | @Override 15 | public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY, 16 | int actionState, boolean isCurrentlyActive) { 17 | if (Build.VERSION.SDK_INT >= 21) { 18 | if (isCurrentlyActive) { 19 | Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation); 20 | if (originalElevation == null) { 21 | originalElevation = ViewCompat.getElevation(view); 22 | float newElevation = 1f + findMaxElevation(recyclerView, view); 23 | ViewCompat.setElevation(view, newElevation); 24 | view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation); 25 | } 26 | } 27 | } 28 | 29 | view.setTranslationX(dX); 30 | view.setTranslationY(dY); 31 | } 32 | 33 | private static float findMaxElevation(RecyclerView recyclerView, View itemView) { 34 | final int childCount = recyclerView.getChildCount(); 35 | float max = 0; 36 | for (int i = 0; i < childCount; i++) { 37 | final View child = recyclerView.getChildAt(i); 38 | if (child == itemView) { 39 | continue; 40 | } 41 | final float elevation = ViewCompat.getElevation(child); 42 | if (elevation > max) { 43 | max = elevation; 44 | } 45 | } 46 | return max; 47 | } 48 | 49 | @Override 50 | public void onDrawOver(Canvas c, RecyclerView recyclerView, View view, float dX, float dY, 51 | int actionState, boolean isCurrentlyActive) { 52 | } 53 | 54 | @Override 55 | public void clearView(View view) { 56 | if (Build.VERSION.SDK_INT >= 21) { 57 | final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation); 58 | if (tag != null && tag instanceof Float) { 59 | ViewCompat.setElevation(view, (Float) tag); 60 | } 61 | view.setTag(R.id.item_touch_helper_previous_elevation, null); 62 | } 63 | 64 | view.setTranslationX(0f); 65 | view.setTranslationY(0f); 66 | } 67 | 68 | @Override 69 | public void onSelected(View view) { 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 17 | 18 | 19 | 27 | 28 | 38 | 39 | 50 | 51 | 52 | 63 | 64 | 65 | 66 | 67 | 77 | 78 | 88 | 89 | -------------------------------------------------------------------------------- /library/src/main/java/me/hiten/jkcardlayout/CardAnimatorManager.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | 4 | 5 | import android.animation.TypeEvaluator; 6 | import android.util.Property; 7 | import android.view.View; 8 | 9 | import java.util.LinkedList; 10 | import java.util.Random; 11 | 12 | /** 13 | * 卡片动画管理类,管理动画的参数和回退栈 14 | */ 15 | class CardAnimatorManager { 16 | 17 | 18 | /** 19 | * 动画旋转角度参数 20 | */ 21 | private float mRotation = 20f; 22 | 23 | /** 24 | * AnimatorInfo变化过程估值器 25 | */ 26 | public static class AnimatorInfoEvaluator implements TypeEvaluator { 27 | 28 | AnimatorInfo mTemp = new AnimatorInfo(); 29 | 30 | @Override 31 | public AnimatorInfo evaluate(float fraction, AnimatorInfo startValue, AnimatorInfo endValue) { 32 | mTemp.targetRotation = startValue.targetRotation+(endValue.targetRotation-startValue.targetRotation)*fraction; 33 | mTemp.targetXr = startValue.targetXr+(endValue.targetXr-startValue.targetXr)*fraction; 34 | mTemp.targetYr = startValue.targetYr+(endValue.targetYr-startValue.targetYr)*fraction; 35 | return mTemp; 36 | } 37 | } 38 | 39 | /** 40 | * 与AnimatorInfo对应的View属性 41 | */ 42 | public static class AnimatorInfoProperty extends Property{ 43 | 44 | /** 45 | * x偏移基数 46 | */ 47 | private float baseX; 48 | 49 | /** 50 | * y偏移基数 51 | */ 52 | private float baseY; 53 | 54 | AnimatorInfoProperty(float baseX, float baseY) { 55 | super(AnimatorInfo.class, null); 56 | this.baseX = baseX; 57 | this.baseY = baseY; 58 | } 59 | 60 | @Override 61 | public void set(View object, AnimatorInfo value) { 62 | object.setTranslationX(value.targetXr*baseX); 63 | object.setTranslationY(value.targetYr*baseY); 64 | object.setRotation(value.targetRotation); 65 | } 66 | 67 | @Override 68 | public AnimatorInfo get(View object) { 69 | return null; 70 | } 71 | } 72 | 73 | 74 | void setRotation(float maxRotation) { 75 | this.mRotation = maxRotation; 76 | } 77 | 78 | 79 | /** 80 | * 存储动画信息的回退栈 81 | */ 82 | private LinkedList mBackStack = new LinkedList<>(); 83 | 84 | /** 85 | * 尝试从回退栈栈顶获取AnimatorInfo并返回,如果栈为空,创建新的并返回 86 | * @return  87 | */ 88 | AnimatorInfo takeRecentInfo() { 89 | AnimatorInfo pop = null; 90 | if (mBackStack.size() > 0) { 91 | pop = mBackStack.pop(); 92 | } 93 | if (pop == null) { 94 | pop = new AnimatorInfo(); 95 | } 96 | return pop; 97 | } 98 | 99 | 100 | /** 101 | * 将移除的AnimatorInfo添加到回退栈 102 | * @param removed 移除栈顶 103 | */ 104 | void addToBackStack(AnimatorInfo removed) { 105 | mBackStack.push(removed); 106 | } 107 | 108 | /** 109 | * 创建一个随机的动画Info 110 | * @return  111 | */ 112 | AnimatorInfo createRandomInfo() { 113 | AnimatorInfo animatorInfo = new AnimatorInfo(); 114 | boolean b = new Random().nextBoolean(); 115 | animatorInfo.targetXr = b ? -1.0f : 1.0f; 116 | animatorInfo.targetYr = 1f; 117 | animatorInfo.targetRotation = mRotation; 118 | return animatorInfo; 119 | } 120 | 121 | 122 | static class AnimatorInfo { 123 | /** 124 | *x偏移比例 125 | */ 126 | float targetXr; 127 | 128 | /** 129 | * y偏移比例 130 | */ 131 | float targetYr; 132 | 133 | /** 134 | * 目标旋转角度 135 | */ 136 | float targetRotation; 137 | 138 | /** 139 | * 代表动画执行到原始位置 140 | */ 141 | static AnimatorInfo ZERO = new AnimatorInfo(); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/SettingDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | import android.content.DialogInterface 4 | import android.os.Bundle 5 | import android.support.v4.app.DialogFragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.SeekBar 10 | import kotlinx.android.synthetic.main.dialog_setting.* 11 | 12 | class SettingDialogFragment : DialogFragment(){ 13 | 14 | var mStartValue:Int = 0 15 | var mCurValue:Int = 0 16 | var mEndValue:Int = 0 17 | var mDivisor:Int = 1 18 | 19 | var mProgress:Int =0 20 | 21 | var mInitProgress:Int = 0 22 | 23 | var mTitlePrefix:String? = null 24 | 25 | companion object { 26 | 27 | const val KEY_TITLE = "title" 28 | const val KEY_START = "start" 29 | const val KEY_CUR = "cur" 30 | const val KEY_END = "end" 31 | const val KEY_DIVISOR = "mDivisor" 32 | 33 | fun newInstance(title: String,start:Int,cur:Int,end:Int,divisor:Int):SettingDialogFragment{ 34 | val settingDialogFragment = SettingDialogFragment() 35 | val bundle = Bundle() 36 | bundle.putString(KEY_TITLE,title) 37 | bundle.putInt(KEY_START,start) 38 | bundle.putInt(KEY_CUR,cur) 39 | bundle.putInt(KEY_END,end) 40 | bundle.putInt(KEY_DIVISOR,divisor) 41 | settingDialogFragment.arguments = bundle 42 | return settingDialogFragment 43 | } 44 | } 45 | 46 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 47 | return inflater.inflate(R.layout.dialog_setting,container,false) 48 | } 49 | 50 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 51 | super.onViewCreated(view, savedInstanceState) 52 | arguments?.let { 53 | val title = it.getString(KEY_TITLE) 54 | mStartValue = it.getInt(KEY_START) 55 | mCurValue = it.getInt(KEY_CUR) 56 | mEndValue = it.getInt(KEY_END) 57 | mDivisor = it.getInt(KEY_DIVISOR,1) 58 | tv_title.text = title 59 | mTitlePrefix = title?.split(":")?.get(0) 60 | val max = mEndValue - mStartValue 61 | mProgress = mCurValue-mStartValue 62 | mInitProgress = mProgress 63 | seek_bar.max = max 64 | seek_bar.progress = mProgress 65 | 66 | seek_bar.setOnSeekBarChangeListener(object :SeekBar.OnSeekBarChangeListener{ 67 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { 68 | mProgress = progress 69 | updateTitle() 70 | } 71 | 72 | override fun onStartTrackingTouch(seekBar: SeekBar?) { 73 | } 74 | 75 | override fun onStopTrackingTouch(seekBar: SeekBar?) { 76 | } 77 | 78 | }) 79 | } 80 | } 81 | 82 | fun updateTitle(){ 83 | val num:Number 84 | if (mDivisor==1) { 85 | num = mProgress+mStartValue 86 | }else{ 87 | num = (mProgress+mStartValue) / mDivisor.toFloat() 88 | } 89 | mTitlePrefix?.let { 90 | tv_title.text = it.plus(":").plus(num) 91 | } 92 | 93 | } 94 | 95 | override fun onDismiss(dialog: DialogInterface?) { 96 | super.onDismiss(dialog) 97 | if (mProgress == mInitProgress){ 98 | return 99 | } 100 | if (mDivisor==1) { 101 | mCallback?.invoke(mProgress+mStartValue) 102 | }else{ 103 | mCallback?.invoke((mProgress+mStartValue) / mDivisor.toFloat()) 104 | } 105 | } 106 | 107 | 108 | private var mCallback : ((Number) -> Unit)? = null 109 | 110 | fun setCallback(callback:((Number) -> Unit)){ 111 | this.mCallback = callback 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 重要提示:该交互flutter实现已经开源,快来围观:[https://github.com/HitenDev/FlutterDragCard](https://github.com/HitenDev/FlutterDragCard) 3 | === 4 | ----------以下正文---------- 5 | # JKCardLayout 6 | 本项目使用RecyclerView和自定义LayoutManager等方法实现即刻App探索页交互,支持卡片拖拽,卡片回退栈管理,下拉展示菜单等功能;欢迎大家点赞或者吐槽。 7 | 8 | 代码大部分使用Kotlin语言编写,假装娴熟,如有使用不当还请各路大神指点。 9 | 10 | # 下载Demo 11 | ![](https://www.pgyer.com/app/qrcode/yZ2L) 12 | 13 | 下载地址:https://www.pgyer.com/yZ2L 14 | 15 | 16 | # 效果展示 17 | 18 | ## 视频 19 | 20 | https://youtu.be/GY_YVJGgKtk 21 | 22 | ## 图片 23 | ![](https://upload-images.jianshu.io/upload_images/869487-6097cfbdf65c1edd.gif?imageMogr2/auto-orient/strip) 24 | ![](https://upload-images.jianshu.io/upload_images/869487-42bc90ba6542a913.gif?imageMogr2/auto-orient/strip) 25 | ![](https://upload-images.jianshu.io/upload_images/869487-e1c84f090d9990b6.gif?imageMogr2/auto-orient/strip) 26 | ![](https://upload-images.jianshu.io/upload_images/869487-e71ee69d64d91e5a.gif?imageMogr2/auto-orient/strip) 27 | ![](https://upload-images.jianshu.io/upload_images/869487-0c3417dfae3b4f36.png?imageMogr2/auto-orient/strip) 28 | ![](https://upload-images.jianshu.io/upload_images/869487-46b8372d3a7b46cd.png?imageMogr2/auto-orient/strip) 29 | 30 | 31 | 32 | 33 | # 如何使用 34 | 35 | ``` 36 | implementation 'me.hiten:jkcardlayout:0.1.1' 37 | ``` 38 | 39 | ## 卡片布局辅助类CardLayoutHelper 40 | 41 | ### 绑定RecyclerView 42 | 43 | ```Kotlin 44 | mCardLayoutHelper = CardLayoutHelper() 45 | mCardLayoutHelper.attachToRecyclerView(recycler_view) 46 | ``` 47 | ### 绑定数据源List 48 | 49 | ```Kotlin 50 | mCardLayoutHelper.bindDataSource(object : CardLayoutHelper.BindDataSource { 51 | override fun bind(): List { 52 | return list 53 | } 54 | }) 55 | ``` 56 | 绑定数据源采用回调接口形式,需要返回绑定的RecyclerView对应Adapter下的数据源List 57 | 58 | 59 | ### 卡片参数配置 60 | 61 | ```Kotlin 62 | val config = CardLayoutHelper.Config() 63 | .setCardCount(2) 64 | .setMaxRotation(20f) 65 | .setOffset(8.dp) 66 | .setSwipeThreshold(0.2f) 67 | .setDuration(200) 68 | 69 | mCardLayoutHelper.setConfig(config) 70 | ``` 71 | CardLayoutHelper.Config接受参数配置,主要参数含义: 72 | - cardCount //卡片布局最多包含卡片个数,默认是2个 73 | - offset //卡片之间的偏移量,单位是像素 74 | - duration //卡片动画执行时间 75 | - swipeThreshold //拖拽卡片触发移除的阈值 76 | - maxRotation //拖拽过程中最大旋转角度(角度制) 77 | 78 | ### 行为操作(Back和Next) 79 | 80 | ```Kotlin 81 | //check and doNext 82 | if (mCardLayoutHelper.canNext()) { 83 | mCardLayoutHelper.doNext() 84 | } 85 | //check and doBack 86 | if (mCardLayoutHelper.canBack()){ 87 | mCardLayoutHelper.doBack() 88 | } 89 | 90 | //check noBack 91 | if (mCardLayoutHelper.noBack()){ 92 | super.onBackPressed() 93 | } 94 | 95 | ``` 96 | 结合即刻案例,提供了Back和Next两种操作,使用前建议调用canXXX()进行判断 97 | 98 | ### 回调监听 99 | 100 | ```Kotlin 101 | mCardLayoutHelper.setOnCardLayoutListener(object :OnCardLayoutListener{ 102 | override fun onSwipe(dx: Float, dy: Float) { 103 | Log.d("onStateChanged","dx:$dx dy:$dy") 104 | } 105 | override fun onStateChanged(state: CardLayoutHelper.State) { 106 | Log.d("onStateChanged",state.name) 107 | } 108 | }) 109 | ``` 110 | - onSwipe(dx: Float, dy: Float) //卡片滑动距离回调 111 | - onStateChanged(state: CardLayoutHelper.State)//卡片状态监听,State详解 112 | - State.IDLE //默认状态,无拖拽和动画执行 113 | - State.SWIPE //手指拖动状态 114 | - State.BACK_ANIM //Back动画执行中,包含两种情况(释放手势卡片缓慢回到默认位置过程、调用back方法执行动画) 115 | - State.LEAVE_ANIM //LEAVE动画执行中,包括两种情况(释放手势卡片缓慢移除布局过程、调用next方法执行动画) 116 | 117 | ## 仿即刻下拉手势布局:[PullDownLayout](https://github.com/HitenDev/JKCardLayout/blob/master/sample/src/main/java/me/hiten/jkcardlayout/sample/PullDownLayout.kt) 118 | 119 | ### 基本功能设置 120 | 121 | ```Kotlin 122 | //设置阻尼 123 | pull_down_layout.setDragRatio(0.6f) 124 | //设置视觉差系数 125 | pull_down_layout.setParallaxRatio(1.1f) 126 | //设置动画时长 127 | pull_down_layout.setDuration(200) 128 | ``` 129 | 130 | 注意[PullDownLayout](https://github.com/HitenDev/JKCardLayout/blob/master/sample/src/main/java/me/hiten/jkcardlayout/sample/PullDownLayout.kt)类不在library中,如果需要使用的话,建议您clone一份代码改巴改巴 131 | 132 | 133 | 134 | # 声明 135 | 本项目是个人作品,仅用作技术学习和开源交流,涉及到即刻APP相关的接口数据和图片资源禁止用于任何商业用途;如有疑问,请联系我。 136 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.support.v7.app.AppCompatActivity 8 | import android.text.SpannableString 9 | import android.text.Spanned 10 | import android.text.method.LinkMovementMethod 11 | import android.text.style.ClickableSpan 12 | import android.util.Log 13 | import android.view.Gravity 14 | import android.view.View 15 | import android.view.ViewGroup 16 | import android.view.WindowManager 17 | import android.widget.ImageView 18 | import android.widget.LinearLayout 19 | import android.widget.TextView 20 | import android.widget.Toast 21 | import com.bumptech.glide.Glide 22 | import kotlinx.android.synthetic.main.activity_main.* 23 | import kotlinx.android.synthetic.main.layout_card.* 24 | import me.hiten.jkcardlayout.CardLayoutHelper 25 | import me.hiten.jkcardlayout.OnCardLayoutListener 26 | import java.util.* 27 | 28 | 29 | class MainActivity : AppCompatActivity() { 30 | 31 | 32 | private var list = ArrayList() 33 | private var cardAdapter : CardAdapter? = null 34 | 35 | private lateinit var mCardLayoutHelper : CardLayoutHelper 36 | 37 | 38 | private lateinit var mConfig : CardLayoutHelper.Config 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | setContentView(R.layout.activity_main) 43 | setStatusBar() 44 | 45 | mCardLayoutHelper = CardLayoutHelper() 46 | 47 | mConfig = CardLayoutHelper.Config() 48 | .setCardCount(2) 49 | .setMaxRotation(20f) 50 | .setOffset(8.dp) 51 | .setSwipeThreshold(0.2f) 52 | .setDuration(200) 53 | 54 | mCardLayoutHelper.setConfig(mConfig) 55 | 56 | mCardLayoutHelper.attachToRecyclerView(recycler_view) 57 | 58 | mCardLayoutHelper.bindDataSource(object : CardLayoutHelper.BindDataSource { 59 | override fun bind(): List { 60 | return list 61 | } 62 | }) 63 | 64 | mCardLayoutHelper.setOnCardLayoutListener(object : OnCardLayoutListener { 65 | override fun onSwipe(dx: Float, dy: Float) { 66 | Log.d("onStateChanged","dx:$dx dy:$dy") 67 | } 68 | 69 | override fun onStateChanged(state: CardLayoutHelper.State) { 70 | Log.d("onStateChanged",state.name) 71 | 72 | } 73 | 74 | }) 75 | 76 | cardAdapter = CardAdapter(list) 77 | 78 | recycler_view.adapter =cardAdapter 79 | 80 | btn_prev.setOnClickListener { 81 | if (mCardLayoutHelper.canBack()){ 82 | mCardLayoutHelper.doBack() 83 | } 84 | } 85 | 86 | btn_next.setOnClickListener { 87 | onNextPressed() 88 | } 89 | 90 | btn_menu.setOnClickListener { 91 | pull_down_layout.openMenu() 92 | } 93 | 94 | getMockData() 95 | 96 | //设置阻尼 97 | pull_down_layout.setDragRatio(0.6f) 98 | //设置视觉差系数 99 | pull_down_layout.setParallaxRatio(1.1f) 100 | //设置动画时长 101 | pull_down_layout.setDuration(200) 102 | 103 | updateConfigShow() 104 | } 105 | 106 | @Suppress("UNCHECKED_CAST") 107 | private fun castList(input: List<*>):List{ 108 | return input as List 109 | } 110 | 111 | private fun getMockData(){ 112 | MockData.getCards(this) { 113 | val cards = it["cards"] 114 | if (cards is List<*>) { 115 | list.clear() 116 | list.addAll(castList(cards)) 117 | cardAdapter?.notifyDataSetChanged() 118 | } 119 | val toolbarItems = it["toolbarItems"] 120 | if (toolbarItems is List<*>) { 121 | val items = castList(toolbarItems) 122 | if (!items.isEmpty()) { 123 | layout_top_menu.removeAllViews() 124 | for (item in items) { 125 | val linearLayout = LinearLayout(this) 126 | linearLayout.gravity = Gravity.CENTER 127 | linearLayout.orientation = LinearLayout.VERTICAL 128 | val iv = ImageView(this) 129 | val layoutParams1 = LinearLayout.LayoutParams(66.dp, 66.dp) 130 | layoutParams1.bottomMargin = 10.dp 131 | linearLayout.addView(iv, layoutParams1) 132 | Glide.with(this).load(item.picUrl).circleCrop().into(iv) 133 | 134 | val tv = TextView(this) 135 | tv.textSize = 12f 136 | tv.setTextColor(Color.parseColor("#333333")) 137 | tv.text = item.title 138 | tv.gravity = Gravity.CENTER 139 | linearLayout.addView(tv) 140 | 141 | val layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT) 142 | layoutParams.weight = 1f 143 | layout_top_menu.addView(linearLayout, layoutParams) 144 | linearLayout.setOnClickListener { 145 | Toast.makeText(this, item.title, Toast.LENGTH_SHORT).show() 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | private fun setStatusBar(){ 154 | if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){ 155 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 156 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 157 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 158 | window.statusBarColor = Color.TRANSPARENT 159 | pull_down_layout.setPadding(pull_down_layout.paddingLeft,getStatusBarHeight(this),pull_down_layout.paddingRight,pull_down_layout.paddingBottom) 160 | } 161 | } 162 | 163 | private fun getStatusBarHeight(context: Context): Int { 164 | var statusBarHeight = 0 165 | val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android") 166 | if (resourceId > 0) { 167 | statusBarHeight = context.resources.getDimensionPixelSize(resourceId) 168 | } 169 | return statusBarHeight 170 | } 171 | 172 | private fun onNextPressed(){ 173 | if (mCardLayoutHelper.canNext()) { 174 | mCardLayoutHelper.doNext() 175 | } 176 | } 177 | 178 | override fun onBackPressed() { 179 | if (mCardLayoutHelper.canBack()){ 180 | mCardLayoutHelper.doBack() 181 | }else if (mCardLayoutHelper.noBack()){ 182 | super.onBackPressed() 183 | } 184 | } 185 | 186 | private fun updateConfigShow(){ 187 | val text = "卡片数:${mConfig.cardCount} | 偏移像素:${mConfig.offset} | 最大旋转角度:${mConfig.maxRotation} | 拖拽触发阈值:${mConfig.swipeThreshold} | 下拉菜单阻尼:${pull_down_layout.getDragRatio()} | 下拉视觉差比例:${pull_down_layout.getParallaxRatio()}" 188 | val list = text.split("|") 189 | val spannableString = SpannableString(text) 190 | var start = 0 191 | for (index in 0 until list.size){ 192 | val item = list[index] 193 | val end = start + item.length 194 | spannableString.setSpan(object :ClickableSpan(){ 195 | override fun onClick(widget: View) { 196 | when(index){ 197 | 0->{ 198 | showDialog(index,item,1,mConfig.cardCount,3,1) 199 | } 200 | 1->{ 201 | showDialog(index,item,0,mConfig.offset,20.dp,1) 202 | } 203 | 2->{ 204 | val divisor = 2 205 | showDialog(index,item,0,mConfig.maxRotation.toInt()*divisor,120,divisor) 206 | } 207 | 3->{ 208 | val divisor = 10 209 | showDialog(index,item,1, (mConfig.swipeThreshold*divisor).toInt(),8,divisor) 210 | } 211 | 4->{ 212 | val divisor = 10 213 | showDialog(index,item,2, (pull_down_layout.getDragRatio()*divisor).toInt(),12,divisor) 214 | } 215 | 5->{ 216 | val divisor = 10 217 | showDialog(index,item,8,(pull_down_layout.getParallaxRatio()*divisor).toInt(),15,divisor) 218 | } 219 | 220 | } 221 | } 222 | },start,end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) 223 | start+=item.length + 1 224 | } 225 | tv_show_config.text = spannableString 226 | tv_show_config.movementMethod = LinkMovementMethod.getInstance() 227 | } 228 | 229 | private fun showDialog(index:Int,title: String,start:Int,cur:Int,end:Int,divisor:Int){ 230 | val settingDialogFragment = SettingDialogFragment.newInstance(title,start,cur,end,divisor) 231 | settingDialogFragment.show(supportFragmentManager,"dialog_setting") 232 | settingDialogFragment.setCallback { 233 | when(index){ 234 | 0->{ 235 | mConfig.cardCount = it.toInt() 236 | recycler_view.adapter?.notifyDataSetChanged() 237 | } 238 | 1->{ 239 | mConfig.offset = it.toInt() 240 | recycler_view.adapter?.notifyDataSetChanged() 241 | } 242 | 2->{ 243 | mConfig.maxRotation = it.toFloat() 244 | recycler_view.adapter?.notifyDataSetChanged() 245 | } 246 | 3->{ 247 | mConfig.swipeThreshold = it.toFloat() 248 | recycler_view.adapter?.notifyDataSetChanged() 249 | } 250 | 4->{ 251 | pull_down_layout.setDragRatio(it.toFloat()) 252 | } 253 | 5->{ 254 | pull_down_layout.setParallaxRatio(it.toFloat()) 255 | } 256 | 257 | else -> { 258 | 259 | } 260 | } 261 | updateConfigShow() 262 | } 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /library/src/main/java/me/hiten/jkcardlayout/JKCardLayoutManager.java: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.animation.ValueAnimator; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | /** 12 | * 自定义LayoutManager,主要管理RecyclerView子View的布局以及Next,Back的动画以及复用 13 | */ 14 | public class JKCardLayoutManager extends RecyclerView.LayoutManager { 15 | 16 | private RecyclerView mRecyclerView; 17 | 18 | private CardAnimatorManager mAnimatorStackManager; 19 | 20 | private OnCardLayoutListener mCardLayoutListener; 21 | 22 | private CardLayoutHelper.State mState = CardLayoutHelper.State.IDLE; 23 | 24 | 25 | private CardLayoutHelper.Config mConfig = new CardLayoutHelper.Config(); 26 | 27 | JKCardLayoutManager(RecyclerView mRecyclerView, CardAnimatorManager mAnimatorStackManager) { 28 | this.mRecyclerView = mRecyclerView; 29 | this.mAnimatorStackManager = mAnimatorStackManager; 30 | } 31 | 32 | void setConfig(CardLayoutHelper.Config config) { 33 | this.mConfig = config; 34 | } 35 | 36 | void setOnCardLayoutListener(OnCardLayoutListener onCardLayoutListener) { 37 | this.mCardLayoutListener = onCardLayoutListener; 38 | } 39 | 40 | 41 | /** 42 | * 控制是否执行Back,主要是用给onLayoutChildren做逻辑判断 43 | */ 44 | private boolean mPendingOptBack; 45 | 46 | /** 47 | * 控制是否执行Next,主要是用给onLayoutChildren做逻辑判断 48 | */ 49 | private boolean mPendingOptNext; 50 | 51 | /** 52 | * 动画是否正在执行 53 | */ 54 | private boolean mAnimatorRunning; 55 | 56 | /** 57 | * 是否能够执行Back 58 | * @return true or false 59 | */ 60 | boolean canBack(){ 61 | return !mPendingOptBack&&!mAnimatorRunning; 62 | } 63 | 64 | /** 65 | * 是否能够执行Next 66 | * @return true or false 67 | */ 68 | boolean canNext(){ 69 | return !mPendingOptNext&&!mAnimatorRunning; 70 | } 71 | 72 | /** 73 | * 即将进行Back操作,下一步会影响onLayoutChildren的行为 74 | */ 75 | void pendingOptBack() { 76 | mPendingOptBack = true; 77 | } 78 | 79 | /** 80 | * 即将进行Next操作,下一步会影响onLayoutChildren的行为 81 | */ 82 | void pendingOptNext() { 83 | mPendingOptNext = true; 84 | } 85 | 86 | @Override 87 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 88 | super.onLayoutChildren(recycler, state); 89 | if (recycler == null || state == null) { 90 | return; 91 | } 92 | if (mAnimatorRunning) { 93 | return; 94 | } 95 | int childCount = mRecyclerView.getChildCount(); 96 | //Back操作在onLayoutChildren中的本质是移除最底下的View,添加到最顶层,做动画操作假装从屏幕外进来,掩人耳目 97 | if (mPendingOptBack) { 98 | if (childCount < 0) { 99 | mPendingOptBack = false; 100 | mState = CardLayoutHelper.State.IDLE; 101 | onLayoutChildren(recycler, state); 102 | } else { 103 | mAnimatorRunning = true; 104 | //尝试先回收最底下的View 105 | if (childCount > mConfig.cardCount - 1) { 106 | detachAndScrapViewAt(0, recycler); 107 | } 108 | //复用上面回收的View 109 | View view = recycler.getViewForPosition(0); 110 | //透明度设置成0,要的就是添加的那瞬间不可见 111 | view.setAlpha(0f); 112 | addView(view); 113 | measureLayoutItemView(view); 114 | doBackAnimator(view); 115 | } 116 | return; 117 | } 118 | //Next操作在onLayoutChildren中的本质是暂时不执行View的移除,等动画完成之后,执行requestLayout刷新整个布局 119 | if (mPendingOptNext) { 120 | if (childCount < 0) { 121 | mPendingOptNext = false; 122 | mState = CardLayoutHelper.State.IDLE; 123 | onLayoutChildren(recycler, state); 124 | } else { 125 | mAnimatorRunning = true; 126 | View view = mRecyclerView.getChildAt(childCount - 1); 127 | doNextAnimator(view); 128 | } 129 | return; 130 | } 131 | 132 | //逻辑能走到这里说明没有动画的执行,就是对卡片进行布局 133 | 134 | //移除掉所有的View 135 | detachAndScrapAttachedViews(recycler); 136 | 137 | //这个ItemCount对应Adapter返回的count,不是RecyclerView孩子数 138 | int itemCount = getItemCount(); 139 | if (itemCount < 1) { 140 | return; 141 | } 142 | //计算最大卡片个数 143 | int maxCount = Math.min(mConfig.cardCount, itemCount); 144 | //要倒序遍历,因为先执行addView的View会在下面 145 | for (int position=maxCount - 1;position>=0;position--) { 146 | //position是制从大到小遍历,越大的越排在下面,执行的偏移也就越大 147 | View view = recycler.getViewForPosition(position); 148 | addView(view); 149 | measureLayoutItemView(view); 150 | view.setRotation(0f); 151 | if (position > 0) { 152 | view.setTranslationX(mConfig.offset * position); 153 | view.setTranslationY(-mConfig.offset * position); 154 | } else { 155 | view.setTranslationX(0f); 156 | view.setTranslationY(0f); 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * 测量并布局子View 163 | * @param view view 164 | */ 165 | private void measureLayoutItemView(View view){ 166 | measureChildWithMargins(view, 0, 0); 167 | RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 168 | int left = 0; 169 | int top = 0; 170 | if (lp.width>0){ 171 | int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view); 172 | int d = getWidth() - mRecyclerView.getPaddingLeft() - mRecyclerView.getPaddingRight() - decoratedMeasuredWidth - lp.leftMargin - lp.rightMargin; 173 | if (d>0){ 174 | left = (int) (d/2f+0.5f); 175 | } 176 | } 177 | if (lp.height>0){ 178 | int decoratedMeasuredHeight = mRecyclerView.getPaddingTop() - mRecyclerView.getPaddingBottom() - getDecoratedMeasuredHeight(view); 179 | int d = getHeight() - decoratedMeasuredHeight - lp.topMargin - lp.bottomMargin; 180 | if (d>0){ 181 | top = (int) (d/2f+0.5f); 182 | } 183 | } 184 | layoutDecoratedWithMargins(view, left, top, 185 | left+getDecoratedMeasuredWidth(view)+lp.leftMargin+lp.rightMargin, 186 | top+lp.topMargin+lp.bottomMargin + getDecoratedMeasuredHeight(view)); 187 | } 188 | 189 | /** 190 | * 执行Back动画 191 | * @param view targetView 192 | */ 193 | private void doBackAnimator(final View view){ 194 | final CardAnimatorManager.AnimatorInfo add = mAnimatorStackManager.takeRecentInfo(); 195 | int width = mRecyclerView.getWidth(); 196 | int height = mRecyclerView.getHeight(); 197 | ObjectAnimator objectAnimator = ObjectAnimator.ofObject(view, new CardAnimatorManager.AnimatorInfoProperty(width, height), new CardAnimatorManager.AnimatorInfoEvaluator(), add,CardAnimatorManager.AnimatorInfo.ZERO); 198 | objectAnimator.setDuration(mConfig.duration); 199 | listenerAnimator(false,objectAnimator,view,add); 200 | objectAnimator.start(); 201 | } 202 | 203 | 204 | /** 205 | * 执行Next动画 206 | * @param view targetView 207 | */ 208 | private void doNextAnimator(final View view){ 209 | final CardAnimatorManager.AnimatorInfo createRemove = mAnimatorStackManager.createRandomInfo(); 210 | int width = mRecyclerView.getWidth(); 211 | int height = mRecyclerView.getHeight(); 212 | ObjectAnimator objectAnimator = ObjectAnimator.ofObject(view, new CardAnimatorManager.AnimatorInfoProperty(width, height), new CardAnimatorManager.AnimatorInfoEvaluator(), CardAnimatorManager.AnimatorInfo.ZERO, createRemove); 213 | objectAnimator.setDuration(mConfig.duration); 214 | listenerAnimator(true,objectAnimator,view,createRemove); 215 | objectAnimator.start(); 216 | } 217 | 218 | /** 219 | * 监听动画执行 220 | * @param next true Next行为 / false Back行为 221 | * @param objectAnimator 动画 222 | * @param view targetView 223 | * @param animatorInfo 动画信息 224 | */ 225 | private void listenerAnimator(final boolean next,ObjectAnimator objectAnimator,final View view,final CardAnimatorManager.AnimatorInfo animatorInfo){ 226 | 227 | objectAnimator.addListener(new AnimatorListenerAdapter() { 228 | 229 | @Override 230 | public void onAnimationStart(Animator animation) { 231 | super.onAnimationStart(animation); 232 | if (!next){ 233 | view.setAlpha(1f); 234 | } 235 | if (next){ 236 | notifyStateListener(CardLayoutHelper.State.LEAVE_ANIM); 237 | }else { 238 | notifyStateListener(CardLayoutHelper.State.BACK_ANIM); 239 | } 240 | } 241 | 242 | @Override 243 | public void onAnimationCancel(Animator animation) { 244 | super.onAnimationCancel(animation); 245 | onAnimationEnd(animation); 246 | } 247 | 248 | @Override 249 | public void onAnimationEnd(Animator animation) { 250 | super.onAnimationEnd(animation); 251 | if (next) { 252 | mPendingOptNext = false; 253 | mAnimatorRunning = false; 254 | mAnimatorStackManager.addToBackStack(animatorInfo); 255 | notifyStateListener(CardLayoutHelper.State.IDLE); 256 | requestLayout(); 257 | }else { 258 | mPendingOptBack = false; 259 | mAnimatorRunning = false; 260 | notifyStateListener(CardLayoutHelper.State.IDLE); 261 | requestLayout(); 262 | } 263 | } 264 | }); 265 | objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 266 | int childCount = mRecyclerView.getChildCount(); 267 | //记录动画执行前的所有子View的translationX 268 | Float[] translationXs = new Float[childCount]; 269 | //记录动画执行前的所有子View的translationY 270 | Float[] translationYs = new Float[childCount]; 271 | @Override 272 | public void onAnimationUpdate(ValueAnimator animation) { 273 | notifyDxDyListener(view.getTranslationX(),view.getTranslationY()); 274 | float fraction = animation.getAnimatedFraction(); 275 | float fl = fraction * mConfig.offset; 276 | for (int i = 0;i 数据源类型 19 | */ 20 | public class CardLayoutHelper { 21 | 22 | /** 23 | * 配置参数实体 24 | */ 25 | public static class Config { 26 | 27 | /** 28 | * 旋转最大角度 29 | */ 30 | public float maxRotation = 10; 31 | 32 | /** 33 | * 展示的卡片个数,最小是2个 34 | */ 35 | public int cardCount =2; 36 | 37 | /** 38 | * 卡片位置之间的偏移量 39 | */ 40 | public int offset = Utils.dp2px(8); 41 | 42 | /** 43 | * 动画执行时间 44 | */ 45 | public long duration = 250; 46 | 47 | /** 48 | * 拖拽时触发移除的阈值比例 49 | */ 50 | public float swipeThreshold = 0.2f; 51 | 52 | public Config() { 53 | } 54 | 55 | public Config setMaxRotation(float maxRotation) { 56 | this.maxRotation = maxRotation; 57 | return this; 58 | } 59 | 60 | public Config setCardCount(int cardCount) { 61 | this.cardCount = cardCount; 62 | return this; 63 | } 64 | 65 | public Config setOffset(int offset) { 66 | this.offset = offset; 67 | return this; 68 | } 69 | 70 | public Config setDuration(long duration) { 71 | this.duration = duration; 72 | return this; 73 | } 74 | 75 | public Config setSwipeThreshold(float swipeThreshold) { 76 | this.swipeThreshold = swipeThreshold; 77 | return this; 78 | } 79 | } 80 | 81 | 82 | /** 83 | * 数据源绑定回调 84 | * @param  数据源类型 85 | */ 86 | public interface BindDataSource { 87 | 88 | /** 89 | * 返回Adapter对应的数据源 90 | * @return  91 | */ 92 | List bind(); 93 | } 94 | 95 | /** 96 | * 卡片状态 97 | */ 98 | public enum State { 99 | 100 | /** 101 | * 初始状态,即无拖动行为和动画执行 102 | */ 103 | IDLE, 104 | 105 | /** 106 | * 手指拖动中的状态 107 | */ 108 | SWIPE, 109 | 110 | /** 111 | * 执行Back动画或者松手回到原始位置的动画过程 112 | */ 113 | BACK_ANIM, 114 | 115 | /** 116 | * 执行Next动画或者松手移除卡片的动画过程 117 | */ 118 | LEAVE_ANIM 119 | } 120 | 121 | private BindDataSource mBindDataSource; 122 | 123 | 124 | /** 125 | * 移除的数据回退栈 126 | */ 127 | private LinkedList mRemovedDataStack = new LinkedList<>(); 128 | 129 | private OnCardLayoutListener mOnCardLayoutListener; 130 | 131 | private JKCardLayoutManager mJKCardLayoutManager; 132 | 133 | private CardAnimatorManager mAnimatorStackManager; 134 | 135 | private Config mConfig; 136 | 137 | private State mState = State.IDLE; 138 | 139 | 140 | private RecyclerView mRecyclerView; 141 | 142 | 143 | public void bindDataSource(BindDataSource bindDataSource) { 144 | this.mBindDataSource = bindDataSource; 145 | } 146 | 147 | public void setConfig(Config config) { 148 | if (config == null){ 149 | return; 150 | } 151 | this.mConfig = config; 152 | if (mJKCardLayoutManager!=null) { 153 | mJKCardLayoutManager.setConfig(config); 154 | } 155 | if (mAnimatorStackManager!=null){ 156 | mAnimatorStackManager.setRotation(config.maxRotation); 157 | } 158 | } 159 | 160 | public void setOnCardLayoutListener(OnCardLayoutListener onCardLayoutListener) { 161 | mOnCardLayoutListener = onCardLayoutListener; 162 | mJKCardLayoutManager.setOnCardLayoutListener(onCardLayoutListener); 163 | } 164 | 165 | public void attachToRecyclerView(RecyclerView recyclerView) { 166 | mRecyclerView = recyclerView; 167 | if (mConfig == null) { 168 | mConfig = new Config(); 169 | } 170 | mAnimatorStackManager = new CardAnimatorManager(); 171 | mAnimatorStackManager.setRotation(mConfig.maxRotation); 172 | 173 | mJKCardLayoutManager = new JKCardLayoutManager(recyclerView, mAnimatorStackManager); 174 | mJKCardLayoutManager.setConfig(mConfig); 175 | recyclerView.setLayoutManager(mJKCardLayoutManager); 176 | setItemTouchHelper(recyclerView); 177 | } 178 | 179 | 180 | private void setItemTouchHelper(final RecyclerView recyclerView) { 181 | //基于ItemTouchHelper源码修改,改动1:对动画结束位置的关键逻辑进行修改,可见代码621-629行 改动2:优化多指触控,代码413-415行 182 | ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { 183 | 184 | /** 185 | * 记录上一次有无拖动行为,为true表示手指在拖动 186 | */ 187 | private Boolean mLastIsActive = null; 188 | 189 | /** 190 | * 动画执行类型,一般是ANIMATION_TYPE_SWIPE_CANCEL或者ANIMATION_TYPE_SWIPE_SUCCESS两种情况 191 | */ 192 | private Integer mAnimationType = null; 193 | 194 | @Override 195 | public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { 196 | super.onSelectedChanged(viewHolder, actionState); 197 | if (actionState == ACTION_STATE_SWIPE) { 198 | notifyStateListener(State.SWIPE); 199 | } 200 | } 201 | 202 | @Override 203 | public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { 204 | return false; 205 | } 206 | 207 | @Override 208 | public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { 209 | super.clearView(recyclerView, viewHolder); 210 | notifyStateListener(State.IDLE); 211 | mLastIsActive = null; 212 | mAnimationType = null; 213 | } 214 | 215 | @Override 216 | public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { 217 | //该方法是在Item移除动画结束后被调用,下面的代码主要是记录数据回退栈 218 | int position = viewHolder.getLayoutPosition(); 219 | List data = getDataList(); 220 | if (data != null && !data.isEmpty()) { 221 | T removeData = data.remove(position); 222 | mRemovedDataStack.push(removeData); 223 | if (mRecyclerView.getAdapter() != null) { 224 | mRecyclerView.getAdapter().notifyDataSetChanged(); 225 | } 226 | } 227 | //动画回退栈处理 228 | if (mAnimatorStackManager != null) { 229 | CardAnimatorManager.AnimatorInfo animatorInfo = new CardAnimatorManager.AnimatorInfo(); 230 | animatorInfo.targetXr = viewHolder.itemView.getTranslationX() / recyclerView.getWidth(); 231 | animatorInfo.targetYr = viewHolder.itemView.getTranslationY() / recyclerView.getHeight(); 232 | animatorInfo.targetRotation = viewHolder.itemView.getRotation(); 233 | mAnimatorStackManager.addToBackStack(animatorInfo); 234 | } 235 | //状态回调 236 | notifyStateListener(State.IDLE); 237 | mLastIsActive = null; 238 | mAnimationType = null; 239 | } 240 | 241 | @Override 242 | public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { 243 | //控制除了LayoutManager中的第一个View,其他都不能被拖拽 244 | if (viewHolder.getAdapterPosition()!=0){ 245 | return makeMovementFlags(0, 0); 246 | } 247 | return super.getMovementFlags(recyclerView, viewHolder); 248 | 249 | } 250 | 251 | @Override 252 | public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { 253 | return mConfig.swipeThreshold; 254 | } 255 | 256 | @Override 257 | public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) { 258 | mAnimationType = animationType; 259 | return (long) (mConfig.duration * 0.5f); 260 | } 261 | 262 | 263 | @Override 264 | public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { 265 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); 266 | if (mLastIsActive!=null&&mLastIsActive&&!isCurrentlyActive){//监听手指抬起 267 | //TOUCH UP 268 | if (ANIMATION_TYPE_SWIPE_CANCEL == mAnimationType){ 269 | notifyStateListener(State.BACK_ANIM); 270 | }else if (ANIMATION_TYPE_SWIPE_SUCCESS == mAnimationType){ 271 | notifyStateListener(State.LEAVE_ANIM); 272 | } 273 | } 274 | mLastIsActive = isCurrentlyActive; 275 | //回调进度 276 | notifyDxDyListener(viewHolder.itemView.getTranslationX(), viewHolder.itemView.getTranslationY()); 277 | 278 | //下面是计算动画执行的比例 279 | float dXY = (float) Math.sqrt((dX * dX + dY * dY)); 280 | float wh = (float) Math.sqrt((recyclerView.getWidth() * recyclerView.getWidth() + recyclerView.getHeight() * recyclerView.getHeight())); 281 | float ratio = Math.min(Math.abs(dXY) / (wh * 0.5f), 1f); 282 | int childCount = recyclerView.getChildCount(); 283 | if (childCount <= 1) { 284 | return; 285 | } 286 | //除了最上面的卡片,其余都得做相应的平移 287 | for (int i = 0;i < childCount -1;i++){ 288 | View childAt = recyclerView.getChildAt(i); 289 | childAt.setTranslationX(mConfig.offset * (childCount - 1 - i - ratio)); 290 | childAt.setTranslationY(-mConfig.offset * (childCount - 1 - i - ratio)); 291 | } 292 | //最上面的卡片需要做角度的旋转 293 | viewHolder.itemView.setRotation(Math.signum(-dX) * Math.min(1f, Math.abs(dX) / recyclerView.getWidth()) * mConfig.maxRotation); 294 | 295 | } 296 | 297 | }); 298 | itemTouchHelper.attachToRecyclerView(recyclerView); 299 | } 300 | 301 | 302 | private void notifyStateListener(State state) { 303 | if (state != mState) { 304 | mState = state; 305 | if (mOnCardLayoutListener != null) { 306 | mOnCardLayoutListener.onStateChanged(mState); 307 | } 308 | } 309 | } 310 | 311 | private void notifyDxDyListener(float dx, float dy) { 312 | if (mOnCardLayoutListener != null) { 313 | mOnCardLayoutListener.onSwipe(dx, dy); 314 | } 315 | } 316 | 317 | private List getDataList() { 318 | return mBindDataSource != null ? mBindDataSource.bind() : null; 319 | } 320 | 321 | 322 | /** 323 | * 是否能够执行Next行为 324 | * @return  325 | */ 326 | public boolean canNext() { 327 | List data = getDataList(); 328 | return data != null && data.size() > 0 && mState == State.IDLE && mJKCardLayoutManager.canNext(); 329 | } 330 | 331 | /** 332 | * 是否能够执行Back行为 333 | * @return  334 | */ 335 | public boolean canBack() { 336 | return !noBack() && mState == State.IDLE && mJKCardLayoutManager.canBack(); 337 | } 338 | 339 | 340 | /** 341 | * 是不是没有回退栈了 342 | * @return  343 | */ 344 | public boolean noBack() { 345 | return mRemovedDataStack == null || mRemovedDataStack.isEmpty(); 346 | } 347 | 348 | /** 349 | * 执行Next行为 350 | */ 351 | public void doNext() { 352 | List data = getDataList(); 353 | if (data==null){ 354 | return; 355 | } 356 | if (!canNext()) { 357 | return; 358 | } 359 | T removeData = data.remove(0); 360 | mJKCardLayoutManager.pendingOptNext(); 361 | mRemovedDataStack.push(removeData); 362 | if (mRecyclerView.getAdapter() != null) { 363 | mRecyclerView.getAdapter().notifyDataSetChanged(); 364 | } 365 | } 366 | 367 | /** 368 | * 执行Back行为 369 | */ 370 | public void doBack() { 371 | List data = getDataList(); 372 | if (data==null){ 373 | return; 374 | } 375 | if (!canBack()) { 376 | return; 377 | } 378 | if (mRemovedDataStack.size() > 0) { 379 | T pop = mRemovedDataStack.pop(); 380 | if (pop != null) { 381 | mJKCardLayoutManager.pendingOptBack(); 382 | data.add(0, pop); 383 | if (mRecyclerView.getAdapter() != null) { 384 | mRecyclerView.getAdapter().notifyDataSetChanged(); 385 | } 386 | } 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /sample/src/main/java/me/hiten/jkcardlayout/sample/PullDownLayout.kt: -------------------------------------------------------------------------------- 1 | package me.hiten.jkcardlayout.sample 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.ObjectAnimator 6 | import android.content.Context 7 | import android.support.constraint.ConstraintLayout 8 | import android.support.v7.widget.RecyclerView 9 | import android.util.AttributeSet 10 | import android.view.LayoutInflater 11 | import android.view.MotionEvent 12 | import android.view.View 13 | import android.view.ViewConfiguration 14 | 15 | /** 16 | * 下拉手势布局,仿即刻探索页交互 17 | */ 18 | class PullDownLayout : ConstraintLayout { 19 | companion object { 20 | private const val DRAG_RATIO = 0.6f 21 | 22 | private const val ANIM_DURATION = 200 23 | 24 | private const val PARALLAX_RATIO = 1.1f 25 | } 26 | 27 | private var mRecyclerView: RecyclerView 28 | 29 | private var mTopMenu: View 30 | 31 | private var mMaskView : View 32 | 33 | private var mBottomMenu : View 34 | 35 | private var mTouchSlop: Int 36 | 37 | /** 38 | * 已经开始拖拽标志 39 | */ 40 | private var mIsBeingDragged: Boolean = false 41 | 42 | /** 43 | * 不能进行拖拽标志 44 | */ 45 | private var mIsUnableToDrag: Boolean = false 46 | 47 | 48 | private var mLastMotionX: Float = 0f 49 | private var mLastMotionY: Float = 0f 50 | private var mInitialMotionX: Float = 0f 51 | private var mInitialMotionY: Float = 0f 52 | 53 | /** 54 | * 多点触控下激活的手指Id 55 | */ 56 | private var mActivePointerId = -1 57 | 58 | /** 59 | * 记录总共的dx 60 | */ 61 | private var mTotalDx: Float = 0f 62 | 63 | /** 64 | * 记录总共的dy 65 | */ 66 | private var mTotalDy: Float = 0f 67 | 68 | /** 69 | * 拖拽阻尼系数 70 | */ 71 | private var mDragRatio = DRAG_RATIO 72 | 73 | /** 74 | * 动画执行时间 75 | */ 76 | private var mDuration = ANIM_DURATION 77 | 78 | /** 79 | * 视觉差系数,等同于topMenu位移/mRecyclerView位移比例 80 | */ 81 | private var mParallaxRatio = PARALLAX_RATIO 82 | 83 | /** 84 | * 菜单打开/关闭标志 85 | */ 86 | private var mOpened = false 87 | 88 | private var mObjectAnimator: ObjectAnimator? = null 89 | 90 | /** 91 | * 最大TranslationY,一般取topMenu的高度作为参考值 92 | */ 93 | private var mMaxTranslationY = 0 94 | 95 | 96 | constructor(context: Context) : this(context, null) 97 | 98 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 99 | 100 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 101 | LayoutInflater.from(context).inflate(R.layout.layout_card, this, true) 102 | mTopMenu = findViewById(R.id.layout_top_menu) 103 | mMaskView = findViewById(R.id.view_mask) 104 | mBottomMenu = findViewById(R.id.layout_bottom) 105 | mRecyclerView = findViewById(R.id.recycler_view) 106 | mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop 107 | mMaskView.visibility = View.GONE 108 | mMaskView.setOnClickListener { 109 | closeMenu() 110 | } 111 | clipChildren = false 112 | clipToPadding = false 113 | } 114 | 115 | 116 | /** 117 | * 设置视觉差比例系数 118 | */ 119 | fun setParallaxRatio(parallaxRatio: Float){ 120 | this.mParallaxRatio = parallaxRatio 121 | } 122 | 123 | fun getParallaxRatio():Float{ 124 | return this.mParallaxRatio 125 | } 126 | 127 | /** 128 | * 设置拖拽阻尼系数比例 129 | */ 130 | fun setDragRatio(dragRatio: Float){ 131 | this.mDragRatio = dragRatio 132 | } 133 | 134 | 135 | fun getDragRatio():Float{ 136 | return this.mDragRatio 137 | } 138 | 139 | /** 140 | * 设置动画时长 141 | */ 142 | fun setDuration(duration: Int){ 143 | this.mDuration = duration 144 | } 145 | 146 | 147 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 148 | //动画执行时,拦截一切触摸事件 149 | if (isAnimRunning()){ 150 | return true 151 | } 152 | return super.dispatchTouchEvent(ev) 153 | } 154 | 155 | override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 156 | val actionMasked = ev.actionMasked 157 | if (actionMasked != MotionEvent.ACTION_DOWN) { 158 | if (mIsBeingDragged) { 159 | return true 160 | } 161 | if (mIsUnableToDrag) { 162 | return false 163 | } 164 | } 165 | 166 | when (actionMasked) { 167 | MotionEvent.ACTION_DOWN -> { 168 | this.mLastMotionX = ev.x 169 | this.mInitialMotionX = ev.x 170 | this.mLastMotionY = ev.y 171 | this.mInitialMotionY = ev.y 172 | mActivePointerId = ev.getPointerId(0) 173 | mIsUnableToDrag = false 174 | mIsBeingDragged = false 175 | this.mTotalDx = 0f 176 | this.mTotalDy = 0f 177 | } 178 | MotionEvent.ACTION_POINTER_DOWN -> { 179 | this.mLastMotionX = ev.x 180 | this.mInitialMotionX = ev.x 181 | this.mLastMotionY = ev.y 182 | this.mInitialMotionY = ev.y 183 | val index = ev.actionIndex 184 | mActivePointerId = ev.getPointerId(index) 185 | } 186 | MotionEvent.ACTION_MOVE -> { 187 | val x = ev.getX(ev.findPointerIndex(mActivePointerId)) 188 | val y = ev.getY(ev.findPointerIndex(mActivePointerId)) 189 | val dx = x - mInitialMotionX 190 | val dy = y - mInitialMotionY 191 | if (mOpened) { 192 | if (Math.abs(dy) >= mTouchSlop || Math.abs(dx) >= mTouchSlop) { 193 | mIsBeingDragged = true 194 | return true 195 | } 196 | } else { 197 | if (dy >= mTouchSlop && Math.abs(dx) <= mTouchSlop) { 198 | mIsBeingDragged = true 199 | return true 200 | } 201 | if (Math.abs(dy) >= mTouchSlop) { 202 | mIsUnableToDrag = true 203 | return false 204 | } 205 | } 206 | } 207 | MotionEvent.ACTION_POINTER_UP -> { 208 | onSecondaryPointerUp(ev) 209 | } 210 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 211 | mIsBeingDragged = false 212 | mIsUnableToDrag = false 213 | } 214 | } 215 | 216 | return super.onInterceptTouchEvent(ev) 217 | } 218 | 219 | 220 | private fun onSecondaryPointerUp(ev: MotionEvent) { 221 | val pointerIndex = ev.actionIndex 222 | val pointerId = ev.getPointerId(pointerIndex) 223 | if (pointerId == mActivePointerId) { 224 | val newPointerIndex = if (pointerIndex == 0) 1 else 0 225 | mActivePointerId = ev.getPointerId(newPointerIndex) 226 | } 227 | } 228 | 229 | override fun onTouchEvent(ev: MotionEvent): Boolean { 230 | when (ev.actionMasked) { 231 | MotionEvent.ACTION_DOWN -> { 232 | this.mLastMotionX = ev.x 233 | this.mInitialMotionX = ev.x 234 | this.mLastMotionY = ev.y 235 | this.mInitialMotionY = ev.y 236 | this.mTotalDx = 0f 237 | this.mTotalDy = 0f 238 | } 239 | MotionEvent.ACTION_POINTER_DOWN -> { 240 | val index = ev.actionIndex 241 | mLastMotionY = ev.getY(index) 242 | mLastMotionX = ev.getX(index) 243 | mActivePointerId = ev.getPointerId(index) 244 | } 245 | MotionEvent.ACTION_MOVE -> { 246 | if (mIsBeingDragged) { 247 | val pointerIndex = ev.findPointerIndex(mActivePointerId) 248 | if (pointerIndex == -1) { 249 | mIsBeingDragged = false 250 | return true 251 | } 252 | val curX = ev.getX(pointerIndex) 253 | val curY = ev.getY(pointerIndex) 254 | performDrag(curX, curY) 255 | } 256 | } 257 | MotionEvent.ACTION_POINTER_UP -> { 258 | onSecondaryPointerUp(ev) 259 | mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)) 260 | mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId)) 261 | } 262 | MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { 263 | if (!mIsBeingDragged || mIsUnableToDrag) { 264 | return true 265 | } 266 | mIsBeingDragged = false 267 | mIsUnableToDrag = false 268 | 269 | var notAnim = false //是否不需要执行动画 270 | if (mTopMenu.translationY == 0f) { 271 | mOpened = false 272 | notAnim = true 273 | } else if (mTopMenu.translationY == mMaxTranslationY.toFloat()) { 274 | mOpened = true 275 | notAnim = true 276 | } 277 | //需要执行动画 278 | if (!notAnim) { 279 | if (mOpened) { 280 | closeMenu() 281 | } else { 282 | if (mTopMenu.translationY > mMaxTranslationY / 4) { 283 | openMenu() 284 | } else { 285 | closeMenu() 286 | } 287 | } 288 | }else{ 289 | mMaskView.visibility = if(mOpened) View.VISIBLE else View.GONE 290 | } 291 | } 292 | } 293 | return true 294 | } 295 | 296 | private fun startAnimator(open: Boolean) { 297 | if (mParallaxRatio <= 0f){ 298 | mParallaxRatio= PARALLAX_RATIO 299 | } 300 | val topMenuStartY = mTopMenu.translationY 301 | val topMenuEndY = if (open) mMaxTranslationY.toFloat() else 0f 302 | val rvStartY = mRecyclerView.translationY 303 | val rvEndY = if (open) mMaxTranslationY / mParallaxRatio else 0f 304 | val startAlpha = mMaskView.alpha 305 | val endAlpha = if (open) 1f else 0f 306 | if (topMenuStartY == topMenuEndY) return 307 | mObjectAnimator = ObjectAnimator.ofFloat(mTopMenu, "translationY", topMenuStartY, topMenuEndY) 308 | mObjectAnimator!!.duration = (Math.abs(topMenuEndY - topMenuStartY) / mMaxTranslationY * mDuration).toLong() 309 | mObjectAnimator!!.addUpdateListener { 310 | val animatedFraction = it.animatedFraction 311 | val translationY = rvStartY + (rvEndY - rvStartY) * animatedFraction 312 | mRecyclerView.translationY = translationY 313 | mBottomMenu.translationY = mRecyclerView.translationY 314 | 315 | val alpha = startAlpha + (endAlpha-startAlpha)*animatedFraction 316 | mMaskView.alpha = alpha 317 | mMaskView.translationY = mRecyclerView.translationY 318 | } 319 | mObjectAnimator!!.addListener(object : AnimatorListenerAdapter() { 320 | 321 | override fun onAnimationStart(animation: Animator?) { 322 | super.onAnimationStart(animation) 323 | mMaskView.visibility = View.VISIBLE 324 | } 325 | 326 | override fun onAnimationEnd(animation: Animator?) { 327 | super.onAnimationEnd(animation) 328 | mOpened = open 329 | if(!mOpened)mMaskView.visibility = View.GONE 330 | mMaskView.translationY = mRecyclerView.translationY 331 | } 332 | }) 333 | mObjectAnimator!!.start() 334 | 335 | } 336 | 337 | 338 | /** 339 | * 打开菜单 340 | */ 341 | fun openMenu() { 342 | if (mIsBeingDragged) { 343 | return 344 | } 345 | if (isAnimRunning()){ 346 | return 347 | } 348 | startAnimator(true) 349 | } 350 | 351 | 352 | private fun isAnimRunning():Boolean{ 353 | if (mObjectAnimator != null && mObjectAnimator!!.isRunning) { 354 | return true 355 | } 356 | return false 357 | } 358 | 359 | /** 360 | * 关闭菜单 361 | */ 362 | fun closeMenu() { 363 | if (mIsBeingDragged) { 364 | return 365 | } 366 | if (isAnimRunning()){ 367 | return 368 | } 369 | startAnimator(false) 370 | } 371 | 372 | private fun performDrag(curX: Float, curY: Float) { 373 | val dx = curX - mLastMotionX 374 | val dy = curY - mLastMotionY 375 | mLastMotionX = curX 376 | mLastMotionY = curY 377 | mTotalDx += dx 378 | mTotalDy += dy 379 | 380 | if (mParallaxRatio <= 0f){ 381 | mParallaxRatio= PARALLAX_RATIO 382 | } 383 | 384 | val ryTransitionY = Math.max(Math.min(mRecyclerView.translationY + dy*mDragRatio,mMaxTranslationY / mParallaxRatio),0f) 385 | 386 | val translationY = Math.max(Math.min(mTopMenu.translationY + dy * mDragRatio*mParallaxRatio, mMaxTranslationY.toFloat()), 0f) 387 | 388 | mTopMenu.translationY = translationY 389 | mRecyclerView.translationY = ryTransitionY 390 | mMaskView.visibility = View.VISIBLE 391 | mMaskView.translationY = mRecyclerView.translationY 392 | mMaskView.alpha = translationY/mMaxTranslationY.toFloat() 393 | mBottomMenu.translationY = mRecyclerView.translationY 394 | } 395 | 396 | 397 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 398 | super.onSizeChanged(w, h, oldw, oldh) 399 | mMaxTranslationY = mTopMenu.measuredHeight 400 | val measuredHeight = mBottomMenu.measuredHeight 401 | val layoutParams:MarginLayoutParams = mRecyclerView.layoutParams as MarginLayoutParams 402 | if (layoutParams.bottomMargin!=measuredHeight) { 403 | layoutParams.bottomMargin = measuredHeight 404 | mRecyclerView.layoutParams = layoutParams 405 | } 406 | } 407 | 408 | override fun onDetachedFromWindow() { 409 | super.onDetachedFromWindow() 410 | if (isAnimRunning()){ 411 | mObjectAnimator?.cancel() 412 | } 413 | } 414 | 415 | } --------------------------------------------------------------------------------