├── 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 | 
12 |
13 | 下载地址:https://www.pgyer.com/yZ2L
14 |
15 |
16 | # 效果展示
17 |
18 | ## 视频
19 |
20 | https://youtu.be/GY_YVJGgKtk
21 |
22 | ## 图片
23 | 
24 | 
25 | 
26 | 
27 | 
28 | 
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 | }
--------------------------------------------------------------------------------