├── demo ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── xml │ │ │ ├── cvv_fillers.xml │ │ │ └── number_fillers.xml │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── maxpilotto │ │ └── creditcardviewdemo │ │ └── MainActivity.kt ├── proguard-rules.pro └── build.gradle ├── credit-card-view ├── consumer-rules.pro ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── drawable │ │ │ ├── logo_amex.png │ │ │ ├── logo_jcb.png │ │ │ ├── logo_visa.png │ │ │ ├── logo_diners.png │ │ │ ├── logo_generic.png │ │ │ ├── logo_maestro.png │ │ │ ├── logo_discover.png │ │ │ ├── logo_unionpay.png │ │ │ ├── logo_mastercard.png │ │ │ ├── background_amex.xml │ │ │ ├── background_jcb.xml │ │ │ ├── background_visa.xml │ │ │ ├── background_diners.xml │ │ │ ├── background_generic.xml │ │ │ ├── background_maestro.xml │ │ │ ├── background_discover.xml │ │ │ ├── background_mastercard.xml │ │ │ ├── background_unionpay.xml │ │ │ └── sign_strip.xml │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── public.xml │ │ │ ├── colors.xml │ │ │ ├── cards.xml │ │ │ └── attrs.xml │ │ └── layout │ │ │ └── card.xml │ │ └── java │ │ └── com │ │ └── maxpilotto │ │ └── creditcardview │ │ ├── models │ │ ├── CardInput.kt │ │ ├── TouchGrid.kt │ │ ├── Brand.kt │ │ ├── CardArea.kt │ │ └── CreditCard.kt │ │ ├── extensions │ │ ├── String.kt │ │ ├── View.kt │ │ ├── Number.kt │ │ └── TypedArray.kt │ │ ├── animations │ │ ├── Animation.kt │ │ └── RotationAnimation.kt │ │ ├── Aliases.kt │ │ ├── util │ │ ├── Filler.kt │ │ ├── Expiry.kt │ │ └── NumberFormat.kt │ │ └── CreditCardView.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── .github ├── res │ ├── areas.png │ ├── card1.png │ ├── card2.png │ ├── custom1.png │ ├── custom2.png │ ├── custom3.png │ └── custom4.png └── readme.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── runConfigurations.xml ├── inspectionProfiles │ └── Project_Default.xml └── copyright │ └── Apache2.xml ├── gradle.properties ├── gradlew.bat ├── gradlew ├── .gitignore └── LICENSE /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /credit-card-view/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /credit-card-view/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo', ':credit-card-view' 2 | rootProject.name='CreditCardView' 3 | -------------------------------------------------------------------------------- /credit-card-view/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/res/areas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/areas.png -------------------------------------------------------------------------------- /.github/res/card1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/card1.png -------------------------------------------------------------------------------- /.github/res/card2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/card2.png -------------------------------------------------------------------------------- /.github/res/custom1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/custom1.png -------------------------------------------------------------------------------- /.github/res/custom2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/custom2.png -------------------------------------------------------------------------------- /.github/res/custom3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/custom3.png -------------------------------------------------------------------------------- /.github/res/custom4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/.github/res/custom4.png -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CreditCardView 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_amex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_amex.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_jcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_jcb.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_visa.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_diners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_diners.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_generic.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_maestro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_maestro.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_discover.png -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_unionpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_unionpay.png -------------------------------------------------------------------------------- /demo/src/main/res/xml/cvv_fillers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/logo_mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpilotto/credit-card-view/HEAD/credit-card-view/src/main/res/drawable/logo_mastercard.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Oct 28 13:38:17 CET 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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_amex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_jcb.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_visa.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_diners.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_generic.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_maestro.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_discover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_mastercard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/background_unionpay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/drawable/sign_strip.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/src/main/res/xml/number_fillers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10sp 4 | 16sp 5 | 48dp 6 | 42dp 7 | 10dp 8 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/values/public.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #808088 4 | #283593 5 | #4CAF50 6 | #795548 7 | #D32F2F 8 | #FBC02D 9 | #00BCD4 10 | #1976D2 11 | #FBC02D 12 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /.idea/copyright/Apache2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/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 | -------------------------------------------------------------------------------- /credit-card-view/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 | -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/models/CardInput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.models 17 | 18 | /** 19 | * Input fields that can be paired with any existing editable field 20 | */ 21 | enum class CardInput { 22 | HOLDER, 23 | NUMBER, 24 | EXPIRY, 25 | CVV 26 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/models/TouchGrid.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.models 17 | 18 | import com.maxpilotto.creditcardview.GridClickListener 19 | 20 | internal data class TouchGrid( 21 | val rows: Int, 22 | val columns: Int, 23 | val callback: GridClickListener 24 | ) -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/extensions/String.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.extensions 17 | 18 | /** 19 | * Returns whether or not this string is numeric, 20 | * which means that it contains numbers only 21 | */ 22 | internal fun String.isNumeric(): Boolean { 23 | return Regex("[0-9]+").matches(this) 24 | } -------------------------------------------------------------------------------- /credit-card-view/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.2" 8 | 9 | defaultConfig { 10 | minSdkVersion 19 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | consumerProguardFiles 'consumer-rules.pro' 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: 'libs', include: ['*.jar']) 28 | 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 30 | implementation 'androidx.appcompat:appcompat:1.1.0' 31 | implementation 'androidx.core:core-ktx:1.1.0' 32 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.2" 8 | 9 | defaultConfig { 10 | applicationId "com.maxpilotto.creditcardviewdemo" 11 | minSdkVersion 19 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 29 | implementation 'androidx.appcompat:appcompat:1.1.0' 30 | implementation 'androidx.core:core-ktx:1.1.0' 31 | 32 | implementation project(':credit-card-view') 33 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 34 | } 35 | -------------------------------------------------------------------------------- /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 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/extensions/View.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.extensions 17 | 18 | import android.view.View 19 | 20 | /** 21 | * Sets the width of the view 22 | */ 23 | internal fun View.setWidth(width: Int) { 24 | layoutParams.width = width 25 | } 26 | 27 | /** 28 | * Sets the height of the View 29 | */ 30 | internal fun View.setHeight(height: Int) { 31 | layoutParams.height = height 32 | } 33 | 34 | /** 35 | * Sets both the height and width of the View 36 | */ 37 | internal fun View.setSizes(sizes: Int) { 38 | setHeight(sizes) 39 | setWidth(sizes) 40 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/extensions/Number.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.extensions 17 | 18 | import android.content.res.Resources 19 | 20 | /** 21 | * Converts a pixel size into a DP size 22 | */ 23 | internal val Int.dp: Int 24 | get() = (this / Resources.getSystem().displayMetrics.density).toInt() 25 | 26 | /** 27 | * Converts a DP size into a pixel size 28 | */ 29 | internal val Int.px: Int 30 | get() = (this * Resources.getSystem().displayMetrics.density).toInt() 31 | 32 | /** 33 | * Converts a pixel size into a DP size 34 | */ 35 | internal val Float.dp: Float 36 | get() = this / Resources.getSystem().displayMetrics.density 37 | 38 | /** 39 | * Converts a DP size into a pixel size 40 | */ 41 | internal val Float.px: Float 42 | get() = this * Resources.getSystem().displayMetrics.density 43 | 44 | /** 45 | * Converts a pixel size into an SP size 46 | */ 47 | internal val Float.sp: Float 48 | get() = this / Resources.getSystem().displayMetrics.scaledDensity -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/animations/Animation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.animations 17 | 18 | import android.animation.AnimatorSet 19 | import android.view.View 20 | import com.maxpilotto.creditcardview.CreditCardView 21 | 22 | /** 23 | * Base animation class 24 | */ 25 | abstract class Animation(val time: Long) { 26 | /** 27 | * Animates the given [card]'s [frontView] and [backView] 28 | * 29 | * Keep in mind that [CreditCardView.isFlipped]'s value is changed after the animation is done 30 | */ 31 | abstract fun animate( 32 | frontView: View, 33 | backView: View, 34 | card: CreditCardView 35 | ): AnimatorSet 36 | 37 | /** 38 | * Extension of Kotlin's [MutableCollection.addAll] which takes a variable number of elements 39 | * and adds them to the mutable list 40 | */ 41 | protected fun MutableCollection.addAll(vararg elements: E): Boolean { 42 | for (e in elements) { 43 | if (!add(e)) { 44 | return false 45 | } 46 | } 47 | 48 | return true 49 | } 50 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/Aliases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview 17 | 18 | import android.content.res.XmlResourceParser 19 | import android.graphics.Point 20 | import com.maxpilotto.creditcardview.models.Brand 21 | import com.maxpilotto.creditcardview.models.CardArea 22 | 23 | /** 24 | * Basic empty callback 25 | */ 26 | typealias Callback = () -> Unit 27 | 28 | /** 29 | * Advanced click listener that makes use of the clicked position and area 30 | */ 31 | typealias AreaClickListener = ( 32 | card: CreditCardView, 33 | area: CardArea 34 | ) -> Unit 35 | 36 | /** 37 | * Click listener for the [CreditCardView.setGridClickListener] method, 38 | * this gives the coordinates (x and y) on the grid you have specified and the clicked point 39 | * (the actual x and y on the screen) 40 | */ 41 | typealias GridClickListener = ( 42 | card: CreditCardView, 43 | gridPosition: Point 44 | ) -> Unit 45 | 46 | /** 47 | * Map where each Brand has its own card style 48 | */ 49 | typealias StyleMap = MutableMap 50 | 51 | /** 52 | * XML parser handler that should handle the parsing of a xml resource 53 | */ 54 | typealias XmlParserHandler = (XmlResourceParser?) -> T -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/models/Brand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.models 17 | 18 | /** 19 | * Credit Card's brands 20 | */ 21 | enum class Brand { 22 | AMEX("^3[47][0-9]{13}$"), 23 | DINERS("^3(?:0[0-5]\\d|095|6\\d{0,2}|[89]\\d{2})\\d{12,15}$"), 24 | DISCOVER("^6(?:011|[45][0-9]{2})[0-9]{12}$"), 25 | GENERIC, 26 | JCB("^(?:2131|1800|35\\d{3})\\d{11}$"), 27 | MAESTRO("^(50|[56–69]|6759|676770|676774)[0-9]+\$"), 28 | MASTERCARD("^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}\$"), 29 | UNIONPAY("^62[0-9]{14,17}$"), 30 | VISA("^4[0-9]{12}(?:[0-9]{3}){0,2}$"); 31 | 32 | private val regex: Regex? 33 | 34 | constructor() { 35 | this.regex = null 36 | } 37 | 38 | constructor(regex: String) { 39 | this.regex = regex.toRegex() 40 | } 41 | 42 | companion object { 43 | /** 44 | * Returns the brand of the given card's number 45 | */ 46 | @JvmStatic 47 | fun parse(number: String): Brand { 48 | for (b in values()) { 49 | b.regex?.let { 50 | if (it.matches(number)) { 51 | return b 52 | } 53 | } 54 | } 55 | 56 | return GENERIC 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/maxpilotto/creditcardviewdemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardviewdemo 17 | 18 | import android.os.Bundle 19 | import androidx.appcompat.app.AppCompatActivity 20 | import com.maxpilotto.creditcardview.animations.RotationAnimation 21 | import com.maxpilotto.creditcardview.models.CardArea 22 | import com.maxpilotto.creditcardview.models.CardInput 23 | import kotlinx.android.synthetic.main.activity_main.* 24 | 25 | class MainActivity : AppCompatActivity() { 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContentView(R.layout.activity_main) 29 | 30 | card.flipOnEdit = true 31 | card.flipOnEditAnimation = RotationAnimation() 32 | card.setAreaClickListener { card, area -> 33 | if (area == CardArea.LEFT) { 34 | card.flip( 35 | RotationAnimation(RotationAnimation.LEFT) 36 | ) 37 | } else if (area == CardArea.RIGHT) { 38 | card.flip( 39 | RotationAnimation(RotationAnimation.RIGHT) 40 | ) 41 | } 42 | } 43 | 44 | card.apply { 45 | pairInput(CardInput.HOLDER, _holder) 46 | pairInput(CardInput.NUMBER, _number) 47 | pairInput(CardInput.EXPIRY, _expiry) 48 | pairInput(CardInput.CVV, _cvv) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 25 | 26 | 31 | 32 | 39 | 40 | 48 | 49 | 57 | 58 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /credit-card-view/src/main/res/values/cards.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 17 | 18 | 24 | 25 | 31 | 32 | 38 | 39 | 45 | 46 | 52 | 53 | 59 | 60 | 66 | -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/util/Filler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.util 17 | 18 | import android.content.res.XmlResourceParser 19 | 20 | /** 21 | * Digit filler, this class is used to fill the credit card's number with characters 22 | * for when the user is inserting the number 23 | * 24 | * A filler has 2 properties: 25 | * + fillRange, the range that fill either be filled by the input or by the fillValue 26 | * + fillValue, value that is used to fill 27 | * 28 | * Example of how the filler works 29 | * ``` 30 | * Input = "222233334444" 31 | * MaxLength = 16 32 | * FillValue = "*" 33 | * Result = "222233334444****" 34 | * ``` 35 | * More examples, using 222233334444 as example number 36 | * ``` 37 | * Filler(16,"*") => 222233334444**** 38 | * Filler(20,"*") => 222233334444******** 39 | * Filler(16,"X") => 222233334444XXXX 40 | * Filler(16,"*") + Filler(20,"*") => 222233334444**** 41 | * ``` 42 | */ 43 | class Filler(val length: Int, val fillValue: String) : Comparable { 44 | fun format(input: String): String { 45 | if (length < input.length) { 46 | throw Exception("Filler max length should be smalled or equal to the input's length; Max length: $length, Input length: ${input.length}") 47 | } 48 | 49 | if (length == input.length) { 50 | return input 51 | } 52 | 53 | return StringBuilder().apply { 54 | val diff = this@Filler.length - input.length 55 | 56 | append(input) 57 | 58 | for (i in 0 until diff) { 59 | append(fillValue) 60 | } 61 | }.toString() 62 | } 63 | 64 | override fun compareTo(other: Filler): Int { 65 | return length - other.length 66 | } 67 | 68 | companion object { 69 | /** 70 | * Parses the given XMLResource into a List of Filler 71 | */ 72 | @JvmStatic 73 | fun parseList(xmlParser: XmlResourceParser?): List? { 74 | xmlParser?.run { 75 | val list = mutableListOf() 76 | var event = next() 77 | 78 | while (event != XmlResourceParser.END_DOCUMENT) { 79 | if (event == XmlResourceParser.START_TAG && name == "Filler") { 80 | list.add( 81 | Filler( 82 | getAttributeIntValue(null, "length", 0), 83 | getAttributeValue(null, "fillValue") ?: "*" 84 | ) 85 | ) 86 | } 87 | 88 | event = next() 89 | } 90 | } 91 | 92 | return null 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/models/CardArea.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.models 17 | 18 | /** 19 | * Areas of the card that can be notified when clicked 20 | */ 21 | enum class CardArea { 22 | /** 23 | * Top left corner of the cards, in a 3x3 grid this is 1:1 24 | * 25 | * This could be triggered together with [TOP] and [LEFT] 26 | */ 27 | TOP_LEFT, 28 | 29 | /** 30 | * Top left corner of the cards, in a 3x3 grid this is 1:2 31 | * 32 | * This could be triggered together [TOP], [RIGHT] and [LEFT] 33 | */ 34 | TOP_CENTER, 35 | 36 | /** 37 | * Top left corner of the cards, in a 3x3 grid this is 1:3 38 | * 39 | * This could be triggered together [TOP] and [RIGHT] 40 | */ 41 | TOP_RIGHT, 42 | 43 | /** 44 | * Top left corner of the cards, in a 3x3 grid this is 2:1 45 | * 46 | * This could be triggered together [TOP], [BOTTOM] and [LEFT] 47 | */ 48 | CENTER_LEFT, 49 | 50 | /** 51 | * Top left corner of the cards, in a 3x3 grid this is 3:1 52 | * 53 | * This could be triggered together [TOP], [BOTTOM], [LEFT] and [RIGHT] 54 | */ 55 | CENTER, 56 | 57 | /** 58 | * Top left corner of the cards, in a 3x3 grid this is 2:1 59 | * 60 | * This could be triggered together [TOP], [BOTTOM] and [RIGHT] 61 | */ 62 | CENTER_RIGHT, 63 | 64 | /** 65 | * Top left corner of the cards, in a 3x3 grid this is 2:1 66 | * 67 | * This could be triggered together [LEFT] and [BOTTOM] 68 | */ 69 | BOTTOM_LEFT, 70 | 71 | /** 72 | * Top left corner of the cards, in a 3x3 grid this is 2:1 73 | * 74 | * This could be triggered together [BOTTOM], [RIGHT] and [LEFT] 75 | */ 76 | BOTTOM_CENTER, 77 | 78 | /** 79 | * Top left corner of the cards, in a 3x3 grid this is 2:1 80 | * 81 | * This could be triggered together [BOTTOM] and [RIGHT] 82 | */ 83 | BOTTOM_RIGHT, 84 | 85 | /** 86 | * Top part of the card 87 | * 88 | * It has the same width of the card and half of its height 89 | * 90 | * This could be triggered together with [TOP_LEFT], [TOP_CENTER], [TOP_RIGHT], [CENTER_LEFT], [CENTER] and [CENTER_RIGHT] 91 | */ 92 | TOP, 93 | 94 | /** 95 | * Bottom part of the card 96 | * 97 | * It has the same width of the card and half of its height 98 | * 99 | * This could be triggered together with [BOTTOM_LEFT], [BOTTOM_CENTER], [BOTTOM_RIGHT], [CENTER_LEFT], [CENTER] and [CENTER_RIGHT] 100 | */ 101 | BOTTOM, 102 | 103 | /** 104 | * Left part of the card 105 | * 106 | * It has the same height of the card and half of its width 107 | * 108 | * This could be triggered together with [TOP_LEFT], [CENTER_LEFT], [BOTTOM_LEFT], [CENTER_LEFT], [CENTER] and [CENTER_RIGHT] 109 | */ 110 | LEFT, 111 | 112 | /** 113 | * Right part of the card 114 | * 115 | * It has the same height of the card and half of its width 116 | * 117 | * This could be triggered together with [TOP_RIGHT], [CENTER_RIGHT], [BOTTOM_RIGHT], [CENTER_LEFT], [CENTER] and [CENTER_RIGHT] 118 | */ 119 | RIGHT, 120 | 121 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/extensions/TypedArray.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.extensions 17 | 18 | import android.content.res.TypedArray 19 | import android.content.res.XmlResourceParser 20 | import android.graphics.drawable.Drawable 21 | import androidx.annotation.DrawableRes 22 | import androidx.annotation.StyleableRes 23 | import com.maxpilotto.creditcardview.XmlParserHandler 24 | 25 | /** 26 | * Returns the drawable for the attribute at [index] 27 | * 28 | * If the attribute is not defined, the drawable with id [default] will be returned 29 | */ 30 | internal fun TypedArray.getDrawable( 31 | @StyleableRes index: Int, 32 | @DrawableRes default: Int 33 | ): Drawable { 34 | return getDrawable(index) ?: resources.getDrawable(default) 35 | } 36 | 37 | /** 38 | * Returns the drawable for the attribute at [index] 39 | * 40 | * If the attribute is not defined, [default] will be returned 41 | */ 42 | internal fun TypedArray.getDrawable( 43 | @StyleableRes index: Int, 44 | default: Drawable? 45 | ): Drawable? { 46 | return getDrawable(index) ?: default 47 | } 48 | 49 | /** 50 | * Returns a pixel unit for the attribute at [index] 51 | * 52 | * If the attribute is not defined, [default] will be returned 53 | */ 54 | internal fun TypedArray.getDimensionPixelSize( 55 | @StyleableRes index: Int, 56 | default: Float 57 | ): Float { 58 | return if (hasValue(index)) { 59 | getDimensionPixelSize(index, 0).toFloat() 60 | } else { 61 | default 62 | } 63 | } 64 | 65 | /** 66 | * Returns an SP unit for the attribute at [index] 67 | * 68 | * If the attribute is not defined, [default] will be returned 69 | * 70 | * Note: [default] must be defined using the SP unit 71 | */ 72 | internal fun TypedArray.getDimensionFontSize( 73 | @StyleableRes index: Int, 74 | default: Float 75 | ): Float { 76 | return if (hasValue(index)) { 77 | getDimensionPixelSize(index, 0F).sp 78 | } else { 79 | default 80 | } 81 | } 82 | 83 | /** 84 | * Returns an object of type [T], which must be parsed using an [XmlResourceParser] 85 | * 86 | * The [handler] should take care of the parsing and return the parsed object 87 | */ 88 | internal inline fun TypedArray.getXml( 89 | @StyleableRes index: Int, 90 | handler: XmlParserHandler 91 | ): T? { 92 | return if (hasValue(index)) { 93 | val parser = resources.getXml(getResourceId(index, 0)) 94 | 95 | handler(parser) 96 | } else { 97 | handler(null) 98 | } 99 | } 100 | 101 | /** 102 | * Returns an object of type [T], which must be parsed using an [XmlResourceParser] 103 | * 104 | * The [handler] should take care of the parsing and return the parsed object 105 | */ 106 | internal inline fun TypedArray.getXmlOrNull( 107 | @StyleableRes index: Int, 108 | handler: XmlParserHandler 109 | ): T? { 110 | return if (hasValue(index)) { 111 | getXml(index, handler) 112 | } else { 113 | null 114 | } 115 | } 116 | 117 | /** 118 | * Returns a resource id for the attribute at [index] 119 | * 120 | * If the attribute is not defined, [default] will be returned 121 | */ 122 | internal fun TypedArray.getResourceId( 123 | @StyleableRes index: Int, 124 | default: Int? 125 | ): Int? { 126 | return if (hasValue(index)) { 127 | getResourceId(index, 0) 128 | } else { 129 | default 130 | } 131 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/util/Expiry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.util 17 | 18 | import java.text.DateFormat 19 | import java.util.* 20 | 21 | /** 22 | * Utility for parsing the expiry 23 | */ 24 | class Expiry { 25 | companion object { 26 | /** 27 | * Converts the given formats into the MMyy format 28 | * 29 | * Accepted formats (using september 2018 as an example): 30 | * + null 31 | * + EMPTY 32 | * + 09/18 33 | * + 0918 34 | * + 09-18 35 | * + 09/2018 36 | * + 092018 37 | * + 09-2018 38 | * + 2018/09 39 | * + 2018-09 40 | */ 41 | @JvmStatic 42 | fun from(expiry: String?): String { 43 | val test1 = Regex("[0-9]{2}/[0-9]{2}") // 09/18 44 | val test2 = Regex("[0-9]{4}") // 0918 45 | val test3 = Regex("[0-9]{2}-[0-9]{2}") // 09-18 46 | val test4 = Regex("[0-9]{6}") // 092018 47 | val test5 = Regex("[0-9]{2}/[0-9]{4}") // 09/2018 48 | val test6 = Regex("[0-9]{2}-[0-9]{4}") // 09-2018 49 | val test7 = Regex("[0-9]{4}/[0-9]{2}") // 2018/09 50 | val test8 = Regex("[0-9]{4}-[0-9]{2}") // 2018-09 51 | 52 | if (expiry.isNullOrEmpty()) { 53 | return "" 54 | } 55 | 56 | return with(expiry) { 57 | when { 58 | matches(test1) -> expiry.replace("/", "") 59 | matches(test2) -> expiry 60 | matches(test3) -> expiry.replace("-", "") 61 | matches(test4) -> expiry.substring(0, 2) + expiry.substring(4, 6) 62 | matches(test5) || matches(test6) -> expiry.substring(0, 2) + expiry.substring(5, 7) 63 | matches(test7) || matches(test8) -> expiry.substring(5, 7) + expiry.substring(2, 4) 64 | 65 | else -> throw Exception("Cannot parse date, unrecognized format") 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Creates an expiry from the given [month] and [year] 72 | */ 73 | @JvmStatic 74 | fun from(month: Number, year: Number): String { 75 | return "$month$year" 76 | } 77 | 78 | /** 79 | * Creates an expiry from the given date string which is formatted using the given date format 80 | */ 81 | @JvmStatic 82 | fun from(date: String, dateFormat: DateFormat): String { 83 | val calendar = Calendar.getInstance() 84 | 85 | calendar.time = dateFormat.parse(date)!! 86 | 87 | return from(calendar) 88 | } 89 | 90 | /** 91 | * Creates an expiry from the given [date] 92 | */ 93 | @JvmStatic 94 | fun from(date: Date): String { 95 | return from( 96 | date.month, 97 | date.year 98 | ) 99 | } 100 | 101 | /** 102 | * Creates an expiry from the given [calendar] 103 | */ 104 | @JvmStatic 105 | fun from(calendar: Calendar): String { 106 | return from( 107 | calendar[Calendar.MONTH], 108 | calendar[Calendar.YEAR] 109 | ) 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/models/CreditCard.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.models 17 | 18 | import com.maxpilotto.creditcardview.extensions.isNumeric 19 | import java.util.* 20 | 21 | /** 22 | * Object that represents a credit card 23 | */ 24 | open class CreditCard : Cloneable { 25 | /** 26 | * Name of the holder on the front of the credit card 27 | */ 28 | var holder: String 29 | 30 | /** 31 | * Digits on the front of the credit card 32 | */ 33 | var number: String 34 | set(value) { 35 | field = value 36 | 37 | brand = Brand.parse(value) 38 | } 39 | 40 | /** 41 | * 3 digits number on the back of the credit card 42 | */ 43 | var cvv: String 44 | 45 | /** 46 | * Expiration date on the back of the credit card 47 | */ 48 | var expiry: String 49 | 50 | /** 51 | * Pin code used to confirm payments or withdraw money, 52 | * 53 | * This code is not necessary used, you should ask the user this code only if you're 54 | * building a credit card vault or generic credential vault 55 | */ 56 | var pinCode: String 57 | 58 | /** 59 | * Brand of the credit card, this field is read-only and will change based on the [number] 60 | */ 61 | var brand: Brand 62 | private set 63 | 64 | constructor(card: CreditCard) : this( 65 | card.holder, 66 | card.number, 67 | card.cvv, 68 | card.expiry, 69 | card.pinCode 70 | ) 71 | 72 | @JvmOverloads 73 | constructor( 74 | holder: String = "", 75 | number: String = "", 76 | cvv: String = "", 77 | expiry: String = "", 78 | pinCode: String = "" 79 | ) { 80 | this.brand = Brand.GENERIC 81 | this.holder = holder 82 | this.number = number 83 | this.cvv = cvv 84 | this.expiry = expiry 85 | this.pinCode = pinCode 86 | } 87 | 88 | /** 89 | * Checks if the card's number only contains numbers 90 | */ 91 | fun isNumberValid(): Boolean { 92 | return number.isNumeric() 93 | } 94 | 95 | /** 96 | * Checks if the expiry is valid, both month and year must start from 1 97 | */ 98 | fun isExpiryValid(): Boolean { 99 | val c = Calendar.getInstance() 100 | val month: Int 101 | val year: Int 102 | 103 | return try { 104 | month = expiry.substring(0, 2).toInt() 105 | year = expiry.substring(2, 4).toInt() 106 | 107 | year >= c.get(Calendar.YEAR) % 100 && month in 1..12 108 | } catch (e: Exception) { 109 | false 110 | } 111 | } 112 | 113 | /** 114 | * Checks if the CVV is valid, a valid CVV must be 3 digits long and contain numbers only 115 | */ 116 | fun isCvvValid(): Boolean { 117 | return cvv.length == 3 && cvv.isNumeric() 118 | } 119 | 120 | override fun toString(): String { 121 | return "Holder: $holder, Number: $number, Expiry: $expiry, CVV: $cvv" 122 | } 123 | 124 | override fun equals(other: Any?): Boolean { 125 | other?.let { 126 | it as CreditCard 127 | 128 | return it.holder == holder && 129 | it.number == number && 130 | it.cvv == cvv && 131 | it.expiry == expiry 132 | } 133 | 134 | return false 135 | } 136 | 137 | override fun clone(): Any { 138 | return CreditCard( 139 | holder, 140 | number, 141 | cvv, 142 | expiry 143 | ) 144 | } 145 | } -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/animations/RotationAnimation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.animations 17 | 18 | import android.animation.Animator 19 | import android.animation.AnimatorSet 20 | import android.animation.ObjectAnimator 21 | import android.view.View 22 | import android.view.animation.AccelerateDecelerateInterpolator 23 | import com.maxpilotto.creditcardview.CreditCardView 24 | 25 | /** 26 | * [Animation] extension that animates the view using a rotation 27 | */ 28 | open class RotationAnimation( 29 | val startPoint: Int = LEFT, 30 | time: Long = 500, 31 | val zDistance: Float = 50F 32 | ) : Animation(time) { 33 | override fun animate( 34 | frontView: View, 35 | backView: View, 36 | card: CreditCardView 37 | ): AnimatorSet { 38 | with(card) { 39 | val outView = if (isFlipped) backView else frontView 40 | val inView = if (isFlipped) frontView else backView 41 | val animators = mutableListOf( 42 | ObjectAnimator.ofFloat( 43 | outView, 44 | "alpha", 45 | 1F, 0F 46 | ).apply { 47 | startDelay = time / 2 48 | duration = 0 49 | }, 50 | ObjectAnimator.ofFloat( 51 | inView, 52 | "alpha", 53 | 1F, 0F 54 | ).apply { 55 | duration = 0 56 | }, 57 | ObjectAnimator.ofFloat( 58 | inView, 59 | "alpha", 60 | 0F, 1F 61 | ).apply { 62 | startDelay = time / 2 63 | duration = 0 64 | } 65 | ) 66 | 67 | with(width * zDistance) { 68 | outView.cameraDistance = this 69 | inView.cameraDistance = this 70 | } 71 | 72 | when (startPoint) { 73 | RIGHT -> { 74 | animators.addAll( 75 | ObjectAnimator.ofFloat( 76 | outView, 77 | "rotationY", 78 | 0F, 180F 79 | ).apply { 80 | interpolator = AccelerateDecelerateInterpolator() 81 | duration = time 82 | }, 83 | ObjectAnimator.ofFloat( 84 | inView, 85 | "rotationY", 86 | 180F, 360F 87 | ).apply { 88 | interpolator = AccelerateDecelerateInterpolator() 89 | duration = time 90 | } 91 | ) 92 | } 93 | 94 | LEFT -> { 95 | animators.addAll( 96 | ObjectAnimator.ofFloat( 97 | outView, 98 | "rotationY", 99 | 0F, -180F 100 | ).apply { 101 | interpolator = AccelerateDecelerateInterpolator() 102 | duration = time 103 | }, 104 | ObjectAnimator.ofFloat( 105 | inView, 106 | "rotationY", 107 | -180F, -360F 108 | ).apply { 109 | interpolator = AccelerateDecelerateInterpolator() 110 | duration = time 111 | } 112 | ) 113 | } 114 | } 115 | 116 | return AnimatorSet().apply { 117 | playTogether(animators) 118 | } 119 | } 120 | } 121 | 122 | companion object { 123 | const val LEFT = 0 124 | const val RIGHT = 1 125 | } 126 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | xmlns:android 23 | 24 | ^$ 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | xmlns:.* 34 | 35 | ^$ 36 | 37 | 38 | BY_NAME 39 | 40 |
41 |
42 | 43 | 44 | 45 | .*:id 46 | 47 | http://schemas.android.com/apk/res/android 48 | 49 | 50 | 51 |
52 |
53 | 54 | 55 | 56 | .*:name 57 | 58 | http://schemas.android.com/apk/res/android 59 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | 67 | name 68 | 69 | ^$ 70 | 71 | 72 | 73 |
74 |
75 | 76 | 77 | 78 | style 79 | 80 | ^$ 81 | 82 | 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | ^$ 92 | 93 | 94 | BY_NAME 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | http://schemas.android.com/apk/res/android 104 | 105 | 106 | ANDROID_ATTRIBUTE_ORDER 107 | 108 |
109 |
110 | 111 | 112 | 113 | .* 114 | 115 | .* 116 | 117 | 118 | BY_NAME 119 | 120 |
121 |
122 |
123 |
124 | 125 | 134 |
135 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android ### 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | # Android Studio 3 in .gitignore file. 47 | .idea/caches 48 | .idea/modules.xml 49 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 50 | .idea/navEditor.xml 51 | 52 | # Keystore files 53 | # Uncomment the following lines if you do not want to check your keystore files in. 54 | #*.jks 55 | #*.keystore 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | 60 | # Google Services (e.g. APIs or Firebase) 61 | # google-services.json 62 | 63 | # Freeline 64 | freeline.py 65 | freeline/ 66 | freeline_project_description.json 67 | 68 | # fastlane 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | fastlane/readme.md 74 | 75 | # Version control 76 | vcs.xml 77 | 78 | # lint 79 | lint/intermediates/ 80 | lint/generated/ 81 | lint/outputs/ 82 | lint/tmp/ 83 | # lint/reports/ 84 | 85 | ### Android Patch ### 86 | gen-external-apklibs 87 | output.json 88 | 89 | # Replacement of .externalNativeBuild directories introduced 90 | # with Android Studio 3.5. 91 | .cxx/ 92 | 93 | ### Kotlin ### 94 | # Compiled class file 95 | 96 | # Log file 97 | 98 | # BlueJ files 99 | *.ctxt 100 | 101 | # Mobile Tools for Java (J2ME) 102 | .mtj.tmp/ 103 | 104 | # Package Files # 105 | *.jar 106 | *.war 107 | *.nar 108 | *.ear 109 | *.zip 110 | *.tar.gz 111 | *.rar 112 | 113 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 114 | hs_err_pid* 115 | 116 | ### Linux ### 117 | *~ 118 | 119 | # temporary files which can be created if a process still has a handle open of a deleted file 120 | .fuse_hidden* 121 | 122 | # KDE directory preferences 123 | .directory 124 | 125 | # Linux trash folder which might appear on any partition or disk 126 | .Trash-* 127 | 128 | # .nfs files are created when an open file is removed but is still being accessed 129 | .nfs* 130 | 131 | ### macOS ### 132 | # General 133 | .DS_Store 134 | .AppleDouble 135 | .LSOverride 136 | 137 | # Icon must end with two \r 138 | Icon 139 | 140 | # Thumbnails 141 | ._* 142 | 143 | # Files that might appear in the root of a volume 144 | .DocumentRevisions-V100 145 | .fseventsd 146 | .Spotlight-V100 147 | .TemporaryItems 148 | .Trashes 149 | .VolumeIcon.icns 150 | .com.apple.timemachine.donotpresent 151 | 152 | # Directories potentially created on remote AFP share 153 | .AppleDB 154 | .AppleDesktop 155 | Network Trash Folder 156 | Temporary Items 157 | .apdisk 158 | 159 | ### Windows ### 160 | # Windows thumbnail cache files 161 | Thumbs.db 162 | Thumbs.db:encryptable 163 | ehthumbs.db 164 | ehthumbs_vista.db 165 | 166 | # Dump file 167 | *.stackdump 168 | 169 | # Folder config file 170 | [Dd]esktop.ini 171 | 172 | # Recycle Bin used on file shares 173 | $RECYCLE.BIN/ 174 | 175 | # Windows Installer files 176 | *.cab 177 | *.msi 178 | *.msix 179 | *.msm 180 | *.msp 181 | 182 | # Windows shortcuts 183 | *.lnk 184 | 185 | ### Gradle ### 186 | .gradle 187 | 188 | # Ignore Gradle GUI config 189 | gradle-app.setting 190 | 191 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 192 | !gradle-wrapper.jar 193 | 194 | # Cache of project 195 | .gradletasknamecache 196 | 197 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 198 | # gradle/wrapper/gradle-wrapper.properties 199 | 200 | ### Gradle Patch ### 201 | **/build/ 202 | 203 | ### AndroidStudio ### 204 | # Covers files to be ignored for android development using Android Studio. 205 | 206 | # Built application files 207 | 208 | # Files for the ART/Dalvik VM 209 | 210 | # Java class files 211 | 212 | # Generated files 213 | 214 | # Gradle files 215 | 216 | # Signing files 217 | .signing/ 218 | 219 | # Local configuration file (sdk path, etc) 220 | 221 | # Proguard folder generated by Eclipse 222 | 223 | # Log Files 224 | 225 | # Android Studio 226 | /*/build/ 227 | /*/local.properties 228 | /*/out 229 | /*/*/build 230 | /*/*/production 231 | *.ipr 232 | *.swp 233 | 234 | # Android Patch 235 | 236 | # External native build folder generated in Android Studio 2.2 and later 237 | 238 | # NDK 239 | obj/ 240 | 241 | # IntelliJ IDEA 242 | *.iws 243 | /out/ 244 | 245 | # User-specific configurations 246 | .idea/caches/ 247 | .idea/libraries/ 248 | .idea/shelf/ 249 | .idea/.name 250 | .idea/compiler.xml 251 | .idea/copyright/profiles_settings.xml 252 | .idea/encodings.xml 253 | .idea/misc.xml 254 | .idea/scopes/scope_settings.xml 255 | .idea/vcs.xml 256 | .idea/jsLibraryMappings.xml 257 | .idea/datasources.xml 258 | .idea/dataSources.ids 259 | .idea/sqlDataSources.xml 260 | .idea/dynamic.xml 261 | .idea/uiDesigner.xml 262 | 263 | # OS-specific files 264 | .DS_Store? 265 | 266 | # Legacy Eclipse project files 267 | .classpath 268 | .project 269 | .cproject 270 | .settings/ 271 | 272 | # Mobile Tools for Java (J2ME) 273 | 274 | # Package Files # 275 | 276 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 277 | 278 | ## Plugin-specific files: 279 | 280 | # mpeltonen/sbt-idea plugin 281 | .idea_modules/ 282 | 283 | # JIRA plugin 284 | atlassian-ide-plugin.xml 285 | 286 | # Mongo Explorer plugin 287 | .idea/mongoSettings.xml 288 | 289 | # Crashlytics plugin (for Android Studio and IntelliJ) 290 | com_crashlytics_export_strings.xml 291 | crashlytics.properties 292 | crashlytics-build.properties 293 | fabric.properties 294 | 295 | ### AndroidStudio Patch ### 296 | 297 | !/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /credit-card-view/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /.github/readme.md: -------------------------------------------------------------------------------- 1 | # CreditCardView 2 | A fully customisable Android view that displays credit card's information 3 | 4 | #### Index 5 | + [Getting started](#getting-started) 6 | + [Basic usage](#basic-usage) 7 | + [Grid and Area click](#grid-and-area-click) 8 | + [Number formats](#number-formats) 9 | + [Number fillers](#number-fillers) 10 | + [Input pairing](#input-pairing) 11 | + [Flipping](#flipping) 12 | + [Customization](#Customizations) 13 | + [Expiry parsing](#expiry-parsing) 14 | + [Examples](#examples) 15 | 16 | ## Getting started 17 | In your project's build.gradle 18 | 19 | ```gradle 20 | repositories { 21 | maven { url "https://jitpack.io" } 22 | } 23 | ``` 24 | 25 | In your modules's build.gradle 26 | 27 | ```gradle 28 | dependencies { 29 | implementation 'com.github.maxpilotto:credit-card-view:$latest_version' 30 | } 31 | ``` 32 | 33 | ## Basic usage 34 | ```xml 35 | 46 | ``` 47 | 48 | 49 | ## Grid and Area click 50 | You can set up a click listener for specific areas in the card, these can be either fixed areas or custom areas, defined through a grid 51 | 52 | #### Area 53 | There are 13 defined areas, you can listen for an Area Click event by using the `setAreaClickListener()` method 54 | ```kotlin 55 | card.setAreaClickListener { card, area -> 56 | if (area == CardArea.LEFT) { 57 | // Left area clicked 58 | } else if (area == CardArea.RIGHT) { 59 | // Right area clicked 60 | } 61 | } 62 | ``` 63 | The callback will give you the `CreditCardView` on which the listener was set and the area clicked, the callback might be invoked multiple times for one single click, since some areas overlap 64 | 65 | 66 | 67 | #### Grid 68 | If you need more areas you can create your own touch grid, by using the `setGridClickListener` method 69 | ```kotlin 70 | card.setGridClickListener(10, 5) { card, point -> 71 | // Get the x and y from point 72 | // to see which cell was clicked 73 | } 74 | ``` 75 | The callback will give you the `CreditCardView` and the `Point` representing the cell that was clicked, in a 10x5 grid there's 50 cells that can be clicked 76 | 77 | ## Number formats 78 | The card number can be formatted automatically by setting the value of `numberFormat`, this uses the `NumberFormat` utility 79 | 80 | The format is used only on the shown value, the actual value is kept unformatted 81 | 82 | The following parameters can be used 83 | + `%d` , placeholder for a single or multiple digits, this can be followed by a numberthat indicates how many digits should be placed there 84 | 85 | + `%s` , placeholder for a group of 1 digit, a number can be added to specify how big each group is,the number of groups is variable and their size is specified by the number parameter (the size is not uaranteed) 86 | 87 | + `%r` , placeholder for the remaining characters, this will be replaced by the remaining characters that 88 | were not used by the other placeholders 89 | 90 | + `%c` , moves the cursor ahead, this is generally used to skip characters, it also accepts a number parameter 91 | that tells the format how many characters should be skipped 92 | 93 | ## Number fillers 94 | Fillers are used to fill in a field when some characters are missing, both the cvv and the number support fillers 95 | 96 | Fillers are composed by a `length` and a `fillValue`, when the input's `length` is smaller than the filler's `length` than the remaining size will be filled with the `fillValue` 97 | 98 | If your filler's length is 10, the fill value is `*` and your input is `"1234"` you will get `"1234******"` 99 | 100 | Multiple fillers can be used at the same time, they are applied based on their length, from the shortest to the longest 101 | 102 | Fillers can also be loaded from an XML file, they must have the following syntax 103 | ```xml 104 | 105 | 108 | 111 | 114 | 115 | ``` 116 | 117 | ## Input pairing 118 | You can pair different TextViews to the card's fields, in this way you don't need to edit the card data by yourself, it can be done like this: 119 | 120 | ```kotlin 121 | // Kotlin 122 | card.apply { 123 | pairInput(CardInput.HOLDER, holderTv) 124 | pairInput(CardInput.NUMBER, numberTv) 125 | pairInput(CardInput.EXPIRY, expiryTv) 126 | pairInput(CardInput.CVV, cvvTv) 127 | } 128 | ``` 129 | 130 | or 131 | 132 | ```java 133 | // Java 134 | card.pairInput(CardInput.HOLDER, holderTv) 135 | card.pairInput(CardInput.NUMBER, numberTv) 136 | card.pairInput(CardInput.EXPIRY, expiryTv) 137 | card.pairInput(CardInput.CVV, cvvTv) 138 | ``` 139 | 140 | ## Flipping 141 | The card can be flipped in two different ways: 142 | + onClick, when a click occurs 143 | + onDataEdit, when any of the fields is edited 144 | 145 | Both can be disabled and the animation can be customized by changing respectively `flipOnClickAnimation` and `flipOnEditAnimation` 146 | 147 | You can create a custom animation by creating a class that extends the `Animation` class 148 | 149 | ## Customizations 150 | This view supports multiple styles, one for each brand, that can be set either from the layout 151 | 152 | ```xml 153 | app:amexStyle="@style/DefaultAmex" 154 | app:dinersStyle="@style/DefaultDiners" 155 | app:discoverStyle="@style/DefaultDiscover" 156 | app:genericStyle="@style/DefaultGeneric" 157 | app:jcbStyle="@style/DefaultJcb" 158 | app:maestroStyle="@style/DefaultMaestro" 159 | app:mastercardStyle="@style/DefaultMastercard" 160 | app:unionpayStyle="@style/DefaultUnionpay" 161 | app:visaStyle="@style/DefaultVisa" 162 | ``` 163 | or programmatically 164 | ```java 165 | card.setStyle(Brand.VISA,R.style.CustomVisa) 166 | ``` 167 | 168 | Everytime the brand changes, the new style is loaded and all the attributes are loaded 169 | 170 | There is, anyway, an exception for the `number`, `cvv`, `expiry` and `holder` which are loaded the first time only (from the view's attributes) and can be changed programmatically later 171 | 172 | ## Expiry parsing 173 | The expiry can be parsed from different formats into a standard format (mmYY), this can be done using the `Expiry` utility 174 | 175 | The supported format/objects are: 176 | + Calendar 177 | + Date (java.util) 178 | + Date, with specified DateFormat 179 | + String 180 | + null 181 | + empty 182 | + 09/18 183 | + 0918 184 | + 09-18 185 | + 09/2018 186 | + 092018 187 | + 09-2018 188 | + 2018/09 189 | + 2018-09 190 | 191 | ## Examples 192 | 193 | 194 | 195 | 196 | 197 | 198 | *Ocean by [Matt Hardy](https://www.pexels.com/@matthardy)* 199 | *Bryce Canyon by [Kelsey Johnson](https://www.pexels.com/@kelsey-johnson-1226441)* 200 | 201 | ## License 202 | ``` 203 | Copyright 2018 Max Pilotto 204 | 205 | Licensed under the Apache License, Version 2.0 (the "License"); 206 | you may not use this file except in compliance with the License. 207 | You may obtain a copy of the License at 208 | 209 | http://www.apache.org/licenses/LICENSE-2.0 210 | 211 | Unless required by applicable law or agreed to in writing, software 212 | distributed under the License is distributed on an "AS IS" BASIS, 213 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 214 | See the License for the specific language governing permissions and 215 | limitations under the License. 216 | ``` -------------------------------------------------------------------------------- /credit-card-view/src/main/res/layout/card.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 23 | 24 | 37 | 38 | 47 | 48 | 61 | 62 | 73 | 74 | 75 | 76 | 81 | 82 | 91 | 92 | 103 | 104 | 114 | 115 | 127 | 128 | 139 | 140 | 150 | 151 | 163 | 164 | 176 | 177 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/util/NumberFormat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview.util 17 | 18 | import kotlin.math.ceil 19 | import kotlin.math.min 20 | 21 | /** 22 | * Formatting utility specialized in number formatting 23 | * 24 | * **Patterns** 25 | * 26 | * Patterns are placeholders that can be placed inside a string and will be replaced by a specific part of the input or the input itself 27 | * 28 | * + **%d** , placeholder for a single or multiple digits, this can be followed by a number 29 | * that indicates how many digits should be placed there 30 | * 31 | * + **%s** , placeholder for a group of 1 digit, a number can be added to specify how big each group is, 32 | * the number of groups is variable and their size is specified by the number parameter (the size is not guaranteed) 33 | * 34 | * + **%r** , placeholder for the remaining characters, this will be replaced by the remaining characters that 35 | * were not used by the other placeholders 36 | * 37 | * + **%c** , moves the cursor ahead, this is generally used to skip characters, it also accepts a number parameter 38 | * that tells the format how many characters should be skipped 39 | * 40 | * **Examples** 41 | * 42 | * ``` 43 | * 12345678 %d4 %d4 => 1234 5678 44 | * 12345678 %g2 => 1234 5678 45 | * 12345678 %d2 %d2 %4 => 12 34 5 6 7 8 46 | * 12345678 %s3 => 123 456 78 47 | * 12345678 %d3 %s2 => 123 45 67 8 48 | * 12345678 %d4 => 1234 (no extra digits) 49 | * 12345678 %d4_ => 1234_5678 (extra digits) 50 | * 1234 %d2/%d2 => 12/34 51 | * 12345678 %c3%r => 45678 52 | * 12345 ***%c3%r => ***45 53 | * ``` 54 | */ 55 | open class NumberFormat( 56 | val format: String?, 57 | val showExtraDigits: Boolean = true 58 | ) : Cloneable { 59 | /** 60 | * Formats the given [input] using the [format] specified 61 | * when the class was instantiated 62 | */ 63 | fun format(input: String): String { 64 | format?.let { 65 | var result = StringBuilder(format) 66 | var param: MatchResult? 67 | var cursor = 0 68 | 69 | if (format.isEmpty()) { 70 | return input 71 | } 72 | 73 | result = fixParameters(result) 74 | param = Patterns.BASE.find(result) 75 | 76 | while (cursor < input.length && param != null) { 77 | val value = param.value 78 | val range = param.range 79 | 80 | Patterns.D.find(value)?.let { 81 | val count = it.value.toInt() 82 | val size = min( 83 | input.length, 84 | cursor + count 85 | ) 86 | val replacement = input.substring( 87 | cursor, 88 | size 89 | ) 90 | 91 | cursor += replacement.length 92 | result = result.replace( 93 | range.first, 94 | range.last + 1, 95 | replacement 96 | ) 97 | } 98 | Patterns.S.find(value)?.let { 99 | val remaining = input.length - cursor 100 | val size = it.value.toInt() 101 | val count = ceil(remaining / size.toDouble()).toInt() 102 | var replacement = "" 103 | 104 | for (i in 0 until count) { 105 | val actualSize = min( 106 | input.length, 107 | cursor + size 108 | ) 109 | 110 | with(input.substring(cursor, actualSize)) { 111 | replacement += this 112 | cursor += length 113 | } 114 | 115 | if (i < count - 1) { 116 | replacement += " " 117 | } 118 | } 119 | 120 | result = result.replace( 121 | range.first, 122 | range.last + 1, 123 | replacement 124 | ) 125 | } 126 | Patterns.C.find(value)?.let { 127 | val count = it.value.toInt() 128 | 129 | if (count > input.length) { 130 | throw Exception("Cursor cannot be placed out of the input range, characters to skip: $count, input length: ${input.length}") 131 | } 132 | 133 | cursor += count 134 | result = result.replace( 135 | range.first, 136 | range.last + 1, 137 | "" 138 | ) 139 | } 140 | 141 | if (Patterns.R.containsMatchIn(value)) { 142 | val remaining = input.length - cursor 143 | 144 | if (remaining > 0) { 145 | result = result.replace( 146 | range.first, 147 | range.last + 1, 148 | input.substring(cursor) 149 | ) 150 | 151 | cursor += remaining 152 | } 153 | } 154 | 155 | param = Patterns.BASE.find(result) 156 | } 157 | 158 | if (cursor < input.length && showExtraDigits) { 159 | result.append( 160 | input.substring(cursor) 161 | ) 162 | } 163 | 164 | return removeParameters(result).toString() 165 | } 166 | 167 | return input 168 | } 169 | 170 | /** 171 | * Returns a clone of this class 172 | */ 173 | fun format(any: Number): String { 174 | return format(any.toString()) 175 | } 176 | 177 | /** 178 | * Fixes all the parameters that are missing the numeric value 179 | */ 180 | private fun fixParameters(input: StringBuilder): StringBuilder { 181 | var builder = StringBuilder(input) 182 | var param = Patterns.MISSING_PARAMS.find(builder) 183 | 184 | while (param != null) { 185 | builder = builder.replace( 186 | param.range.first, 187 | param.range.last + 1, 188 | "${param.value}1" 189 | ) 190 | 191 | param = Patterns.MISSING_PARAMS.find(builder) 192 | } 193 | 194 | return builder 195 | } 196 | 197 | /** 198 | * Removes the parameters from the given [input] 199 | */ 200 | private fun removeParameters(input: StringBuilder): StringBuilder { 201 | var builder = StringBuilder(input) 202 | var param = Patterns.EXTRA_PARAMS.find(builder) 203 | 204 | while (param != null) { 205 | builder = builder.replace( 206 | param.range.first, 207 | param.range.last + 1, 208 | "" 209 | ) 210 | 211 | param = Patterns.EXTRA_PARAMS.find(builder) 212 | } 213 | 214 | return builder 215 | } 216 | 217 | override fun clone(): NumberFormat { 218 | return NumberFormat(format) 219 | } 220 | 221 | override fun toString(): String { 222 | return format ?: "" 223 | } 224 | 225 | override fun equals(other: Any?): Boolean { 226 | other?.let { 227 | it as NumberFormat 228 | 229 | return it.format == format 230 | } 231 | 232 | return false 233 | } 234 | } 235 | 236 | /** 237 | * Regex patterns used by the [NumberFormat] utility 238 | */ 239 | private class Patterns { 240 | companion object { 241 | val EXTRA_PARAMS = Regex("\\s?%[drsc]\\d*") 242 | val MISSING_PARAMS = Regex("%[dsc](?!\\d)") 243 | val BASE = Regex("%[dsrc]\\d*") 244 | val D = Regex("(?<=%d)\\d*") 245 | val S = Regex("(?<=%s)\\d*") 246 | val C = Regex("(?<=%c)\\d*") 247 | val R = Regex("%r") 248 | } 249 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Max Pilotto 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /credit-card-view/src/main/java/com/maxpilotto/creditcardview/CreditCardView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Max Pilotto 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.maxpilotto.creditcardview 17 | 18 | import android.animation.AnimatorSet 19 | import android.content.Context 20 | import android.content.res.TypedArray 21 | import android.graphics.Color 22 | import android.graphics.Point 23 | import android.text.Editable 24 | import android.text.TextWatcher 25 | import android.util.AttributeSet 26 | import android.util.TypedValue 27 | import android.view.MotionEvent 28 | import android.widget.LinearLayout 29 | import android.widget.TextView 30 | import androidx.annotation.ColorInt 31 | import androidx.annotation.StyleRes 32 | import androidx.annotation.XmlRes 33 | import androidx.core.animation.doOnEnd 34 | import androidx.core.animation.doOnStart 35 | import androidx.core.content.res.use 36 | import com.maxpilotto.creditcardview.animations.Animation 37 | import com.maxpilotto.creditcardview.animations.RotationAnimation 38 | import com.maxpilotto.creditcardview.extensions.* 39 | import com.maxpilotto.creditcardview.models.* 40 | import com.maxpilotto.creditcardview.util.Expiry 41 | import com.maxpilotto.creditcardview.util.Filler 42 | import com.maxpilotto.creditcardview.util.NumberFormat 43 | import kotlinx.android.synthetic.main.card.view.* 44 | import java.util.* 45 | 46 | /** 47 | * Custom view that displays credit cards' information 48 | */ 49 | class CreditCardView : LinearLayout { 50 | /** 51 | * Map of all the brands with their own styles 52 | */ 53 | private val styleMap: StyleMap 54 | 55 | /** 56 | * Custom touch grid used to detect touch events in the defined grid 57 | */ 58 | private var touchGrid: TouchGrid? 59 | 60 | /** 61 | * Data holder for this view, all of the information are stored inside this [CreditCard] instance 62 | */ 63 | val cardData: CreditCard 64 | 65 | /** 66 | * Current style applied to the view, it can be 0 on the first 67 | * initialization or when there's no custom styles applied 68 | */ 69 | @StyleRes 70 | var currentStyle: Int 71 | private set 72 | 73 | /** 74 | * Whether or not the card should be flipped when it is clicked 75 | */ 76 | var flipOnClick: Boolean 77 | 78 | /** 79 | * Whether or not the card should be flipped when any of its field is being edited 80 | */ 81 | var flipOnEdit: Boolean 82 | 83 | /** 84 | * Animation played when the card is clicked and [flipOnClick] is set to True 85 | * 86 | * By default this will be a [RotationAnimation], which [RotationAnimation.time] is 500ms 87 | * and the [RotationAnimation.startPoint] is [RotationAnimation.LEFT] 88 | */ 89 | var flipOnClickAnimation: Animation 90 | 91 | /** 92 | * Animation played when any of the card's fields are being edited and [flipOnEdit] is set to True 93 | */ 94 | var flipOnEditAnimation: Animation 95 | 96 | /** 97 | * Whether or not this View is being animated, 98 | * if the View is finishing an animation then the [flipOnClick] value should be ignored 99 | */ 100 | var isAnimating: Boolean 101 | private set 102 | 103 | /** 104 | * Whether or not this credit card is flipped 105 | * 106 | * True means that the back of the card is showing, False (default) means that the front is showing, changing this value **will not** animate the card 107 | */ 108 | var isFlipped: Boolean 109 | private set 110 | 111 | /** 112 | * Listener invoked when an area of the card is pressed, areas are defined inside [CardArea] 113 | */ 114 | var areaClickListener: AreaClickListener? 115 | private set 116 | 117 | /** 118 | * Number on the credit card's front 119 | * 120 | * If [numberFormat] is null no format will be used 121 | */ 122 | var number: String 123 | set(value) { 124 | var result = value 125 | 126 | numberFillers?.let { 127 | if (it.isNotEmpty()) { 128 | for (f in it) { 129 | if (result.length <= f.length) { 130 | result = f.format(result) 131 | break 132 | } 133 | } 134 | } 135 | } 136 | 137 | numberFormat?.let { 138 | if (result.isNotEmpty()) { 139 | result = it.format(result) 140 | } 141 | } 142 | 143 | cardData.number = value 144 | cardNumber.text = result 145 | 146 | restoreStyle() 147 | } 148 | get() { 149 | return cardData.number 150 | } 151 | 152 | /** 153 | * Hint of the [number]'s EditText 154 | */ 155 | var numberHint: String 156 | set(value) { 157 | cardNumber.hint = value 158 | field = value 159 | } 160 | 161 | /** 162 | * Size of the [number]'s text, in SP 163 | */ 164 | var numberSize: Float 165 | set(value) { 166 | setNumberSize(TypedValue.COMPLEX_UNIT_SP, value) 167 | field = value 168 | } 169 | 170 | /** 171 | * [number]'s error 172 | */ 173 | var numberError: String 174 | set(value) { 175 | cardNumberError.text = value 176 | field = value 177 | } 178 | 179 | /** 180 | * Holder's name on the credit card's front 181 | */ 182 | var holder: String 183 | set(value) { 184 | cardData.holder = value 185 | cardHolder.text = value 186 | } 187 | get() { 188 | return cardData.holder 189 | } 190 | 191 | /** 192 | * Hint of the [holder]'s EditText 193 | */ 194 | var holderHint: String 195 | set(value) { 196 | cardHolder.hint = value 197 | field = value 198 | } 199 | 200 | /** 201 | * [holder]'s error 202 | */ 203 | var holderError: String? 204 | set(value) { 205 | cardHolderError.text = value 206 | field = value 207 | } 208 | 209 | /** 210 | * [holder]'s label 211 | */ 212 | var holderLabel: String? 213 | set(value) { 214 | cardHolderLabel.text = value 215 | field = value 216 | } 217 | 218 | /** 219 | * CVV code on the credit card's back 220 | */ 221 | var cvv: String 222 | set(value) { 223 | var result = value 224 | 225 | cvvFillers?.let { 226 | if (it.isNotEmpty()) { 227 | for (f in it) { 228 | if (value.length <= f.length) { 229 | result = f.format(value) 230 | break 231 | } 232 | } 233 | } 234 | } 235 | 236 | cardData.cvv = value 237 | cardCvv.text = result 238 | } 239 | get() { 240 | return cardData.cvv 241 | } 242 | 243 | /** 244 | * Whether or not the "/" should be added to the expiry, 245 | * if you turn this off you'll have to set it by yourself 246 | */ 247 | var formatExpiry: Boolean 248 | set(value) { 249 | field = value 250 | expiry = expiry 251 | } 252 | 253 | /** 254 | * Hint of the [cvv]'s EditText 255 | */ 256 | var cvvHint: String 257 | set(value) { 258 | cardCvv.hint = value 259 | field = value 260 | } 261 | 262 | /** 263 | * [cvv]'s label 264 | */ 265 | var cvvError: String 266 | set(value) { 267 | cardCvvError.text = value 268 | field = value 269 | } 270 | 271 | /** 272 | * Size of the [cvv]'s text, in SP 273 | */ 274 | var cvvSize: Float 275 | set(value) { 276 | cardCvv.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 277 | 278 | field = value 279 | } 280 | 281 | /** 282 | * Color of the [cvv]'s text 283 | */ 284 | @ColorInt 285 | var cvvTextColor: Int 286 | set(value) { 287 | cardCvv.setTextColor(value) 288 | field = value 289 | } 290 | 291 | /** 292 | * Expiration date on the credit card's front 293 | */ 294 | var expiry: String 295 | set(value) { 296 | var result = value 297 | 298 | if (value.length >= 2 && formatExpiry) { 299 | result = value.substring(0, 2) + "/" 300 | 301 | if (value.length > 2) { 302 | result += value.substring(2, value.length) 303 | } 304 | } 305 | 306 | cardData.expiry = value 307 | cardExpiry.text = result 308 | } 309 | get() { 310 | return cardData.expiry 311 | } 312 | 313 | /** 314 | * Hint of the [expiry]'s EditText 315 | */ 316 | var expiryHint: String 317 | set(value) { 318 | cardExpiry.hint = value 319 | field = value 320 | } 321 | 322 | /** 323 | * [expiry]'s error 324 | */ 325 | var expiryError: String 326 | set(value) { 327 | cardExpiryError.text = value 328 | field = value 329 | } 330 | 331 | /** 332 | * [expiry]'s label 333 | */ 334 | var expiryLabel: String? 335 | set(value) { 336 | cardExpiryLabel.text = value 337 | field = value 338 | } 339 | 340 | /** 341 | * Credit card's brand 342 | * 343 | * This value is directly retrieved from the [cardData] and changes whenever the [number] changes 344 | */ 345 | val brand: Brand 346 | get() { 347 | return cardData.brand 348 | } 349 | 350 | /** 351 | * Format used to format the [number] when it is updated, by default the format is "%s4" 352 | * 353 | * This value can be set to null to disable the formatter 354 | */ 355 | var numberFormat: NumberFormat? 356 | set(value) { 357 | field = value 358 | number = number 359 | } 360 | 361 | /** 362 | * Fillers used to fill the [cvv] if some digits are still not inserted by the user 363 | * 364 | * Take a look at the [Filler] class for more info, 365 | * fillers will be always applied before the [numberFormat] 366 | * 367 | * By default there's no filler set, if you need to remove all fillers either 368 | * clear the list or set this property to null 369 | */ 370 | var cvvFillers: List? 371 | set(value) { 372 | val list = value?.toMutableList() 373 | 374 | list?.let { 375 | it.sort() 376 | 377 | cvv = cvv 378 | } 379 | 380 | field = list 381 | } 382 | 383 | /** 384 | * Fillers used to fill the [number] if some digits are still not inserted by the user 385 | * 386 | * Take a look at the [Filler] class for more info, 387 | * fillers will be always applied before the [numberFormat] 388 | * 389 | * By default there's no filler set, if you need to remove all fillers either 390 | * clear the list or set this property to null 391 | */ 392 | var numberFillers: List? 393 | set(value) { 394 | val list = value?.toMutableList() 395 | 396 | list?.let { 397 | it.sort() 398 | 399 | number = number 400 | } 401 | 402 | field = list 403 | } 404 | 405 | /** 406 | * Color for all texts that are not labels or errors 407 | */ 408 | @ColorInt 409 | var textColor: Int 410 | set(value) { 411 | cardNumber.setTextColor(value) 412 | cardHolder.setTextColor(value) 413 | cardExpiry.setTextColor(value) 414 | cardCvv.setTextColor(value) 415 | 416 | field = value 417 | } 418 | 419 | /** 420 | * Color for all labels 421 | */ 422 | @ColorInt 423 | var labelColor: Int 424 | set(value) { 425 | cardHolderLabel.setTextColor(value) 426 | cardExpiryLabel.setTextColor(value) 427 | 428 | field = value 429 | } 430 | 431 | /** 432 | * Color for all errors 433 | */ 434 | @ColorInt 435 | var errorColor: Int 436 | set(value) { 437 | cardNumberError.setTextColor(value) 438 | cardHolderError.setTextColor(value) 439 | cardExpiryError.setTextColor(value) 440 | cardCvvError.setTextColor(value) 441 | 442 | field = value 443 | } 444 | 445 | /** 446 | * Color for all text hints 447 | */ 448 | @ColorInt 449 | var hintColor: Int 450 | set(value) { 451 | cardNumber.setHintTextColor(value) 452 | cardHolder.setHintTextColor(value) 453 | cardExpiry.setHintTextColor(value) 454 | cardCvv.setHintTextColor(value) 455 | 456 | field = value 457 | } 458 | 459 | /** 460 | * Size of all errors, in SP 461 | */ 462 | var errorSize: Float 463 | set(value) { 464 | cardNumberError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 465 | cardHolderError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 466 | cardExpiryError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 467 | cardCvvError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 468 | 469 | field = value 470 | } 471 | 472 | /** 473 | * Size of all labels, in SP 474 | */ 475 | var labelSize: Float 476 | set(value) { 477 | cardHolderLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 478 | cardExpiryLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 479 | 480 | field = value 481 | } 482 | 483 | /** 484 | * Size of all texts, in SP 485 | */ 486 | var textSize: Float 487 | set(value) { 488 | cardNumberError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 489 | cardHolderError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 490 | cardExpiryError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 491 | cardCvvError.setTextSize(TypedValue.COMPLEX_UNIT_SP, value) 492 | 493 | field = value 494 | } 495 | 496 | @JvmOverloads 497 | constructor( 498 | context: Context, 499 | attrs: AttributeSet? = null 500 | ) : super(context, attrs) { 501 | inflate(context, R.layout.card, this) 502 | 503 | styleMap = mutableMapOf( 504 | Brand.AMEX to null, 505 | Brand.DINERS to null, 506 | Brand.DISCOVER to null, 507 | Brand.GENERIC to null, 508 | Brand.JCB to null, 509 | Brand.MAESTRO to null, 510 | Brand.MASTERCARD to null, 511 | Brand.UNIONPAY to null, 512 | Brand.VISA to null 513 | ) 514 | touchGrid = null 515 | currentStyle = 0 516 | cardData = CreditCard() 517 | flipOnClick = false 518 | flipOnEdit = false 519 | flipOnClickAnimation = RotationAnimation() 520 | flipOnEditAnimation = RotationAnimation() 521 | isAnimating = false 522 | isFlipped = false 523 | areaClickListener = null 524 | numberHint = "" 525 | numberSize = TEXT_BIG 526 | numberError = "" 527 | holderHint = "" 528 | holderError = "" 529 | holderLabel = "" 530 | formatExpiry = true 531 | cvvHint = "" 532 | cvvError = "" 533 | cvvTextColor = Color.BLACK 534 | cvvSize = TEXT_BIG 535 | expiryHint = "" 536 | expiryError = "" 537 | expiryLabel = "" 538 | numberFormat = null 539 | cvvFillers = null 540 | numberFillers = null 541 | textColor = Color.WHITE 542 | labelColor = LABEL_COLOR 543 | errorColor = ERROR_COLOR 544 | hintColor = LABEL_COLOR 545 | errorSize = TEXT_SMALL 546 | labelSize = TEXT_SMALL 547 | textSize = TEXT_NORMAL 548 | 549 | applyStyle(attrs) 550 | flip(isFlipped) 551 | 552 | cardHolder.addTextChangedListener(object : TextWatcher { 553 | override fun afterTextChanged(s: Editable?) {} 554 | 555 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 556 | 557 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 558 | if (flipOnEdit && isFlipped) { 559 | flip(flipOnEditAnimation) 560 | } 561 | } 562 | }) 563 | cardNumber.addTextChangedListener(object : TextWatcher { 564 | override fun afterTextChanged(s: Editable?) {} 565 | 566 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 567 | 568 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 569 | if (flipOnEdit && isFlipped) { 570 | flip(flipOnEditAnimation) 571 | } 572 | } 573 | }) 574 | cardExpiry.addTextChangedListener(object : TextWatcher { 575 | override fun afterTextChanged(s: Editable?) {} 576 | 577 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 578 | 579 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 580 | if (flipOnEdit && isFlipped) { 581 | flip(flipOnEditAnimation) 582 | } 583 | } 584 | }) 585 | cardCvv.addTextChangedListener(object : TextWatcher { 586 | override fun afterTextChanged(s: Editable?) {} 587 | 588 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 589 | 590 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 591 | if (flipOnEdit && !isFlipped) { 592 | flip(flipOnEditAnimation) 593 | } 594 | } 595 | }) 596 | } 597 | 598 | override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 599 | val x = ev.x.toInt() 600 | val y = ev.y.toInt() 601 | 602 | if (flipOnClick) { 603 | flip(flipOnClickAnimation) 604 | } 605 | 606 | areaClickListener?.let { callback -> 607 | // Full grid (3x3) 608 | with(getPositionOnGrid(3, 3, x, y)) { 609 | when { 610 | contentEquals(intArrayOf(0, 0)) -> callback(this@CreditCardView, CardArea.TOP_LEFT) 611 | contentEquals(intArrayOf(1, 0)) -> callback(this@CreditCardView, CardArea.TOP_CENTER) 612 | contentEquals(intArrayOf(2, 0)) -> callback(this@CreditCardView, CardArea.TOP_RIGHT) 613 | contentEquals(intArrayOf(0, 1)) -> callback(this@CreditCardView, CardArea.CENTER_LEFT) 614 | contentEquals(intArrayOf(1, 1)) -> callback(this@CreditCardView, CardArea.CENTER) 615 | contentEquals(intArrayOf(2, 1)) -> callback(this@CreditCardView, CardArea.CENTER_RIGHT) 616 | contentEquals(intArrayOf(0, 2)) -> callback(this@CreditCardView, CardArea.BOTTOM_LEFT) 617 | contentEquals(intArrayOf(1, 2)) -> callback(this@CreditCardView, CardArea.BOTTOM_CENTER) 618 | contentEquals(intArrayOf(2, 2)) -> callback(this@CreditCardView, CardArea.BOTTOM_RIGHT) 619 | } 620 | } 621 | 622 | // Top and bottom areas 623 | with(getPositionOnGrid(1, 2, x, y)) { 624 | when { 625 | contentEquals(intArrayOf(0, 0)) -> callback(this@CreditCardView, CardArea.TOP) 626 | contentEquals(intArrayOf(0, 1)) -> callback(this@CreditCardView, CardArea.BOTTOM) 627 | } 628 | } 629 | 630 | // Left and right areas 631 | with(getPositionOnGrid(2, 1, x, y)) { 632 | when { 633 | contentEquals(intArrayOf(0, 0)) -> callback(this@CreditCardView, CardArea.LEFT) 634 | contentEquals(intArrayOf(1, 0)) -> callback(this@CreditCardView, CardArea.RIGHT) 635 | } 636 | } 637 | } 638 | 639 | touchGrid?.let { 640 | val pos = getPositionOnGrid(it.rows, it.columns, x, y) 641 | 642 | it.callback( 643 | this, 644 | Point( 645 | pos[0], 646 | pos[1] 647 | ) 648 | ) 649 | } 650 | 651 | return true 652 | } 653 | 654 | /** 655 | * Returns the coordinates on the grid formed by the given [columns] and [rows] 656 | * of the given points [x] and [y], based on the width and the height of this View 657 | */ 658 | fun getPositionOnGrid(columns: Int, rows: Int, x: Int, y: Int): IntArray { 659 | val colSize = width / columns 660 | val rowSize = height / rows 661 | 662 | for (c in 0..columns) { 663 | for (r in 0..rows) { 664 | val topLeft = Point( 665 | c * colSize, 666 | r * rowSize 667 | ) 668 | val bottomRight = Point( 669 | (c + 1) * colSize, 670 | (r + 1) * rowSize 671 | ) 672 | 673 | if (x >= topLeft.x && 674 | x <= bottomRight.x && 675 | y >= topLeft.y && 676 | y <= bottomRight.y 677 | ) { 678 | return intArrayOf(c, r) 679 | } 680 | } 681 | } 682 | 683 | return intArrayOf(-1, -1) 684 | } 685 | 686 | /** 687 | * Sets an [AreaClickListener] for this [CreditCardView], this listener 688 | * will be invoked once the view is clicked and it will tell which area of the card 689 | * was clicked 690 | * 691 | * Note: The callback could be invoked multiple times, since some of the areas overlap, for 692 | * example [CardArea.LEFT] includes [CardArea.TOP_LEFT], [CardArea.CENTER_LEFT], 693 | * [CardArea.BOTTOM_LEFT] and half vertical size of the followings: [CardArea.TOP_CENTER], 694 | * [CardArea.CENTER], [CardArea.BOTTOM_CENTER] 695 | */ 696 | fun setAreaClickListener(listener: AreaClickListener?) { 697 | this.areaClickListener = listener 698 | } 699 | 700 | /** 701 | * Defines a custom grid, with the given [rows] and [columns], which will be used when detecting touch events 702 | * 703 | * For example if you define a 2x2 grid, the card will be divided in 4 areas, 704 | * when a touch event is fired the [GridClickListener] will be invoked and the 705 | * clicked area will be passed to the listener 706 | * 707 | * The [setAreaClickListener] uses the same logic with a 3x3 grid 708 | * 709 | * @see setAreaClickListener 710 | */ 711 | fun setGridClickListener(rows: Int, columns: Int, listener: GridClickListener) { 712 | this.touchGrid = TouchGrid( 713 | columns, 714 | rows, 715 | listener 716 | ) 717 | } 718 | 719 | /** 720 | * Flips the card without playing any animation 721 | * 722 | * The given [flipped] value sets the new value to [isFlipped] and changes 723 | * the card's to its relative flipped state 724 | * 725 | * Note: This method will execute even if [isAnimating] is True 726 | */ 727 | fun flip(flipped: Boolean) { 728 | val outView = if (!flipped) cardBack else cardFront 729 | val inView = if (!flipped) cardFront else cardBack 730 | 731 | outView.rotationY = 180F 732 | outView.alpha = 0F 733 | inView.rotationY = 360F 734 | inView.alpha = 1F 735 | 736 | isFlipped = flipped 737 | } 738 | 739 | /** 740 | * Flips the card using the given [animation] and invokes the 741 | * callbacks [onStart] when the animation starts and [onEnd] when the animation ends 742 | */ 743 | @JvmOverloads 744 | fun flip( 745 | animation: Animation = RotationAnimation(), 746 | onStart: Callback = {}, 747 | onEnd: Callback = {} 748 | ) { 749 | flip( 750 | animation.animate( 751 | cardFront, 752 | cardBack, 753 | this 754 | ), 755 | onStart, 756 | onEnd 757 | ) 758 | } 759 | 760 | /** 761 | * Flips the card using the given [AnimatorSet], the [onStart] and [onEnd] callbacks 762 | * are invoked when the animation starts and when it ends, respectively 763 | * 764 | * Note: If you have a lot of animators you should consider creating your own [Animation] 765 | */ 766 | @JvmOverloads 767 | fun flip( 768 | animators: AnimatorSet, 769 | onStart: Callback = {}, 770 | onEnd: Callback = {} 771 | ) { 772 | if (!isAnimating) { 773 | isFlipped = !isFlipped 774 | 775 | animators.apply { 776 | doOnStart { 777 | isAnimating = true 778 | onStart() 779 | } 780 | doOnEnd { 781 | isAnimating = false 782 | onEnd() 783 | } 784 | 785 | start() 786 | } 787 | } 788 | } 789 | 790 | /** 791 | * Clears all the error fields 792 | * 793 | * This will empty [cvvError], [expiryError], [holderError] and [numberError] 794 | */ 795 | fun clearErrors() { 796 | cvvError = "" 797 | expiryError = "" 798 | holderError = "" 799 | numberError = "" 800 | } 801 | 802 | /** 803 | * Clears all the card's fields 804 | * 805 | * This will empty [cvv], [number], [holder] and [expiry] 806 | */ 807 | fun clearFields() { 808 | cvv = "" 809 | number = "" 810 | holder = "" 811 | expiry = "" 812 | } 813 | 814 | /** 815 | * Clears all the text and error fields 816 | * 817 | * This is the same thing as calling both [clearErrors] and [clearFields] 818 | */ 819 | fun clearAll() { 820 | clearErrors() 821 | clearFields() 822 | } 823 | 824 | /** 825 | * Sets the [numberFormat] from a string 826 | */ 827 | fun setNumberFormat(format: String) { 828 | numberFormat = NumberFormat(format) 829 | } 830 | 831 | /** 832 | * Sets the expiry from the given [date] 833 | */ 834 | fun setExpiry(date: Date) { 835 | this.expiry = Expiry.from(date) 836 | } 837 | 838 | /** 839 | * Sets the expiry from the given [month] and [year] 840 | */ 841 | fun setExpiry(month: Number, year: Number) { 842 | this.expiry = Expiry.from(month, year) 843 | } 844 | 845 | /** 846 | * Sets the expiry from a [calendar] 847 | */ 848 | fun setExpiry(calendar: Calendar) { 849 | this.expiry = Expiry.from(calendar) 850 | } 851 | 852 | /** 853 | * Sets the [cardData] value from the given parameters 854 | */ 855 | fun setCardData(holder: String, number: String, cvv: String, expiry: String) { 856 | this.holder = holder 857 | this.number = number 858 | this.cvv = cvv 859 | this.expiry = expiry 860 | } 861 | 862 | /** 863 | * Sets the [holder]'s text size with the given [unit], which can be 864 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 865 | */ 866 | fun setHolderSize(unit: Int, size: Float) { 867 | cardHolder.setTextSize(unit, size) 868 | } 869 | 870 | /** 871 | * Sets the [holderLabel]'s text size with the given [unit], which can be 872 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 873 | */ 874 | fun setHolderLabelSize(unit: Int, size: Float) { 875 | cardHolderLabel.setTextSize(unit, size) 876 | } 877 | 878 | /** 879 | * Sets the [holderError]'s text size with the given [unit], which can be 880 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 881 | */ 882 | fun setHolderErrorSize(unit: Int, size: Float) { 883 | cardHolder.setTextSize(unit, size) 884 | } 885 | 886 | /** 887 | * Sets the [number]'s text size with the given [unit], which can be 888 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 889 | */ 890 | fun setNumberSize(unit: Int, size: Float) { 891 | cardNumber.setTextSize(unit, size) 892 | } 893 | 894 | /** 895 | * Sets the [numberError]'s text size with the given [unit], which can be 896 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 897 | */ 898 | fun setNumberErrorSize(unit: Int, size: Float) { 899 | cardNumber.setTextSize(unit, size) 900 | } 901 | 902 | /** 903 | * Sets the [expiry]'s text size with the given [unit], which can be 904 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 905 | */ 906 | fun setExpirySize(unit: Int, size: Float) { 907 | cardExpiry.setTextSize(unit, size) 908 | } 909 | 910 | /** 911 | * Sets the [expiryLabel]'s text size with the given [unit], which can be 912 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 913 | */ 914 | fun setExpiryLabelSize(unit: Int, size: Float) { 915 | cardExpiryLabel.setTextSize(unit, size) 916 | } 917 | 918 | /** 919 | * Sets the [expiryError]'s text size with the given [unit], which can be 920 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 921 | */ 922 | fun setExpiryErrorSize(unit: Int, size: Float) { 923 | cardExpiryError.setTextSize(unit, size) 924 | } 925 | 926 | /** 927 | * Sets the [cvv]'s text size with the given [unit], which can be 928 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 929 | */ 930 | fun setCvvSize(unit: Int, size: Float) { 931 | cardNumber.setTextSize(unit, size) 932 | } 933 | 934 | /** 935 | * Sets the [cvv]'s text size with the given [unit], which can be 936 | * for example [TypedValue.COMPLEX_UNIT_PX] or [TypedValue.COMPLEX_UNIT_SP] 937 | */ 938 | fun setCvvErrorSize(unit: Int, size: Float) { 939 | cardExpiryError.setTextSize(unit, size) 940 | } 941 | 942 | /** 943 | * Sets the [cvvFillers] from an xml resource 944 | */ 945 | fun setCvvFillers(@XmlRes resId: Int) { 946 | cvvFillers = Filler.parseList(resources.getXml(resId)) 947 | } 948 | 949 | /** 950 | * Sets the [numberFormat] from an xml resource 951 | */ 952 | fun setNumberFillers(@XmlRes resId: Int) { 953 | numberFillers = Filler.parseList(resources.getXml(resId)) 954 | } 955 | 956 | /** 957 | * Adds the given [textWatcher] to the TextView associated with the given [input] 958 | */ 959 | fun addInputListener(input: CardInput, textWatcher: TextWatcher) { 960 | when (input) { 961 | CardInput.HOLDER -> cardHolder.addTextChangedListener(textWatcher) 962 | CardInput.NUMBER -> cardNumber.addTextChangedListener(textWatcher) 963 | CardInput.EXPIRY -> cardExpiry.addTextChangedListener(textWatcher) 964 | CardInput.CVV -> cardCvv.addTextChangedListener(textWatcher) 965 | } 966 | } 967 | 968 | /** 969 | * Removes the given [textWatcher] from the TextView associated with the given [input] 970 | */ 971 | fun removeInputListener(input: CardInput, textWatcher: TextWatcher) { 972 | when (input) { 973 | CardInput.HOLDER -> cardHolder.removeTextChangedListener(textWatcher) 974 | CardInput.NUMBER -> cardNumber.removeTextChangedListener(textWatcher) 975 | CardInput.EXPIRY -> cardExpiry.removeTextChangedListener(textWatcher) 976 | CardInput.CVV -> cardCvv.removeTextChangedListener(textWatcher) 977 | } 978 | } 979 | 980 | /** 981 | * Pairs the given [input] with the given [field] 982 | * 983 | * This will fill the card's information when the [field] is being edited 984 | */ 985 | fun pairInput(input: CardInput, field: TextView) { 986 | when (input) { 987 | CardInput.HOLDER -> field.addTextChangedListener(object : TextWatcher { 988 | override fun afterTextChanged(s: Editable?) {} 989 | 990 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 991 | 992 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 993 | holder = s.toString() 994 | } 995 | }) 996 | CardInput.NUMBER -> field.addTextChangedListener(object : TextWatcher { 997 | override fun afterTextChanged(s: Editable?) {} 998 | 999 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 1000 | 1001 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 1002 | number = s.toString() 1003 | } 1004 | 1005 | }) 1006 | CardInput.EXPIRY -> field.addTextChangedListener(object : TextWatcher { 1007 | override fun afterTextChanged(s: Editable?) {} 1008 | 1009 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 1010 | 1011 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 1012 | expiry = s.toString() 1013 | } 1014 | 1015 | }) 1016 | CardInput.CVV -> field.addTextChangedListener(object : TextWatcher { 1017 | override fun afterTextChanged(s: Editable?) {} 1018 | 1019 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 1020 | 1021 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 1022 | cvv = s.toString() 1023 | } 1024 | 1025 | }) 1026 | } 1027 | } 1028 | 1029 | /** 1030 | * Changes the style of the given [brand] 1031 | * 1032 | * This will update the view only if the given brand is the same as the currentBrand 1033 | */ 1034 | fun setStyle(brand: Brand, @StyleRes style: Int) { 1035 | styleMap[brand] = style 1036 | 1037 | if (this.brand == brand) { 1038 | applyStyle(brand) 1039 | } 1040 | } 1041 | 1042 | /** 1043 | * Applies the attribute of the style associated with the given [brand] 1044 | * 1045 | * This is called automatically whenever the [number] changes 1046 | */ 1047 | fun applyStyle(brand: Brand) { 1048 | styleMap[brand]?.let { style -> 1049 | if (currentStyle != style) { 1050 | currentStyle = style 1051 | 1052 | context.obtainStyledAttributes(style, R.styleable.CreditCardView).use { array -> 1053 | onStyle(array) 1054 | } 1055 | } 1056 | } 1057 | } 1058 | 1059 | /** 1060 | * Applies the attributes of the given style 1061 | * 1062 | * This will override the current style, call [restoreStyle] to re-apply the style 1063 | * associated with the current [brand] 1064 | */ 1065 | fun applyStyle(@StyleRes styleRes: Int) { 1066 | if (currentStyle != styleRes) { 1067 | currentStyle = styleRes 1068 | 1069 | context.obtainStyledAttributes(styleRes, R.styleable.CreditCardView).use { 1070 | onStyle(it) 1071 | } 1072 | } 1073 | } 1074 | 1075 | /** 1076 | * Restores the style according to the current [brand] 1077 | */ 1078 | fun restoreStyle() { 1079 | applyStyle(brand) 1080 | } 1081 | 1082 | /** 1083 | * Applies the attributes from the given [set] to the view 1084 | * 1085 | * This is the only method that loads the different styles 1086 | */ 1087 | private fun applyStyle(set: AttributeSet?) { 1088 | context.obtainStyledAttributes(set, R.styleable.CreditCardView).use { 1089 | styleMap[Brand.AMEX] = it.getResourceId( 1090 | R.styleable.CreditCardView_amexStyle, 1091 | R.style.DefaultAmex 1092 | ) 1093 | styleMap[Brand.DINERS] = it.getResourceId( 1094 | R.styleable.CreditCardView_dinersStyle, 1095 | R.style.DefaultDiners 1096 | ) 1097 | styleMap[Brand.DISCOVER] = it.getResourceId( 1098 | R.styleable.CreditCardView_discoverStyle, 1099 | R.style.DefaultDiscover 1100 | ) 1101 | styleMap[Brand.GENERIC] = it.getResourceId( 1102 | R.styleable.CreditCardView_genericStyle, 1103 | R.style.DefaultGeneric 1104 | ) 1105 | styleMap[Brand.JCB] = it.getResourceId( 1106 | R.styleable.CreditCardView_jcbStyle, 1107 | R.style.DefaultJcb 1108 | ) 1109 | styleMap[Brand.MAESTRO] = it.getResourceId( 1110 | R.styleable.CreditCardView_maestroStyle, 1111 | R.style.DefaultMaestro 1112 | ) 1113 | styleMap[Brand.MASTERCARD] = it.getResourceId( 1114 | R.styleable.CreditCardView_mastercardStyle, 1115 | R.style.DefaultMastercard 1116 | ) 1117 | styleMap[Brand.UNIONPAY] = it.getResourceId( 1118 | R.styleable.CreditCardView_unionpayStyle, 1119 | R.style.DefaultUnionpay 1120 | ) 1121 | styleMap[Brand.VISA] = it.getResourceId( 1122 | R.styleable.CreditCardView_visaStyle, 1123 | R.style.DefaultVisa 1124 | ) 1125 | 1126 | onStyle(it, true) 1127 | } 1128 | } 1129 | 1130 | /** 1131 | * Applies the attributes retrieved from the given [typedArray] 1132 | * 1133 | * This method is a generic method that applies the attributes, 1134 | * the properties of the view must be already initialized since their values 1135 | * will be used as default values in case some of the attributes are missing 1136 | */ 1137 | private fun onStyle( 1138 | typedArray: TypedArray, 1139 | firstRun: Boolean = false 1140 | ) { 1141 | with(typedArray) { 1142 | cardFront.background = getDrawable( 1143 | R.styleable.CreditCardView_cardFrontBackground, 1144 | cardFront.background 1145 | ) 1146 | cardBack.background = getDrawable( 1147 | R.styleable.CreditCardView_cardBackBackground, 1148 | cardBack.background 1149 | ) 1150 | cardFrontLogo.setImageDrawable( 1151 | getDrawable( 1152 | R.styleable.CreditCardView_cardFrontLogo, 1153 | cardFrontLogo.drawable 1154 | ) 1155 | ) 1156 | cardBackLogo.setImageDrawable( 1157 | getDrawable( 1158 | R.styleable.CreditCardView_cardBackLogo, 1159 | cardBackLogo.drawable 1160 | ) 1161 | ) 1162 | cardFrontLogo.setSizes( 1163 | getDimensionPixelSize( 1164 | R.styleable.CreditCardView_cardFrontLogoSize, 1165 | LOGO_FRONT 1166 | ) 1167 | ) 1168 | cardBackLogo.setSizes( 1169 | getDimensionPixelSize( 1170 | R.styleable.CreditCardView_cardBackLogoSize, 1171 | LOGO_BACK 1172 | ) 1173 | ) 1174 | cardCvv.background = getDrawable( 1175 | R.styleable.CreditCardView_cvvBackground, 1176 | cardCvv.background 1177 | ) 1178 | cardSignStrip.background = getDrawable( 1179 | R.styleable.CreditCardView_signStripBackground, 1180 | cardSignStrip.background 1181 | ) 1182 | cardMagneticStrip.background = getDrawable( 1183 | R.styleable.CreditCardView_magneticStripBackground, 1184 | cardMagneticStrip.background 1185 | ) 1186 | 1187 | numberFillers = getXmlOrNull(R.styleable.CreditCardView_cardNumberFillers) { 1188 | Filler.parseList(it) 1189 | } ?: numberFillers 1190 | cvvFillers = getXmlOrNull(R.styleable.CreditCardView_cardCvvFillers) { 1191 | Filler.parseList(it) 1192 | } ?: cvvFillers 1193 | numberFormat = NumberFormat( 1194 | getString(R.styleable.CreditCardView_cardNumberFormat) ?: numberFormat?.format 1195 | ) 1196 | textColor = getColor(R.styleable.CreditCardView_cardTextColor, textColor) 1197 | textSize = getDimensionFontSize(R.styleable.CreditCardView_cardTextSize, textSize) 1198 | labelColor = getColor(R.styleable.CreditCardView_cardLabelColor, labelColor) 1199 | labelSize = getDimensionFontSize(R.styleable.CreditCardView_cardLabelSize, labelSize) 1200 | errorColor = getColor(R.styleable.CreditCardView_cardLabelColor, errorColor) 1201 | errorSize = getDimensionFontSize(R.styleable.CreditCardView_cardLabelSize, errorSize) 1202 | hintColor = getColor(R.styleable.CreditCardView_cardHintColor, hintColor) 1203 | holderHint = getString(R.styleable.CreditCardView_cardHolderHint) ?: holderHint 1204 | holderError = getString(R.styleable.CreditCardView_cardHolderError) ?: holderError 1205 | holderLabel = getString(R.styleable.CreditCardView_cardHolderLabel) ?: holderLabel 1206 | numberSize = getDimensionFontSize(R.styleable.CreditCardView_cardNumberSize, numberSize) 1207 | numberError = getString(R.styleable.CreditCardView_cardNumberError) ?: numberError 1208 | numberHint = getString(R.styleable.CreditCardView_cardNumberHint) ?: numberHint 1209 | cvvHint = getString(R.styleable.CreditCardView_cardCvvHint) ?: cvvHint 1210 | cvvError = getString(R.styleable.CreditCardView_cardCvvError) ?: cvvError 1211 | cvvTextColor = getColor(R.styleable.CreditCardView_cardCvvTextColor, cvvTextColor) 1212 | cvvSize = getDimensionFontSize(R.styleable.CreditCardView_cardCvvSize, cvvSize) 1213 | expiryHint = getString(R.styleable.CreditCardView_cardExpiryHint) ?: expiryHint 1214 | expiryError = getString(R.styleable.CreditCardView_cardExpiryError) ?: expiryError 1215 | expiryLabel = getString(R.styleable.CreditCardView_cardExpiryLabel) ?: expiryLabel 1216 | isFlipped = getBoolean(R.styleable.CreditCardView_cardIsFlipped, isFlipped) 1217 | flipOnClick = getBoolean(R.styleable.CreditCardView_flipOnClick, flipOnClick) 1218 | flipOnEdit = getBoolean(R.styleable.CreditCardView_flipOnEdit, flipOnEdit) 1219 | formatExpiry = getBoolean(R.styleable.CreditCardView_autoFormatExpiry, formatExpiry) 1220 | 1221 | if (firstRun) { 1222 | cvv = getString(R.styleable.CreditCardView_cardCvv) ?: cvv 1223 | expiry = getString(R.styleable.CreditCardView_cardExpiry) ?: expiry 1224 | holder = getString(R.styleable.CreditCardView_cardHolder) ?: holder 1225 | number = getString(R.styleable.CreditCardView_cardNumber) ?: number 1226 | } 1227 | 1228 | } 1229 | } 1230 | 1231 | companion object { 1232 | private const val TEXT_BIG = 18F 1233 | private const val TEXT_NORMAL = 16F 1234 | private const val TEXT_SMALL = 10F 1235 | private val LOGO_BACK = 42.px 1236 | private val LOGO_FRONT = 48.px 1237 | private val LABEL_COLOR = Color.parseColor("#dddddd") 1238 | private val ERROR_COLOR = Color.parseColor("#f44336") 1239 | } 1240 | } --------------------------------------------------------------------------------