├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── madreain │ │ └── sku │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── Product.json │ ├── java │ │ └── com │ │ │ └── madreain │ │ │ └── sku │ │ │ ├── AppGlideModule.java │ │ │ ├── MainActivity.java │ │ │ ├── ProductSkuDialog.java │ │ │ ├── bean │ │ │ ├── ProductData.java │ │ │ ├── Sku.java │ │ │ └── SkuAttribute.java │ │ │ ├── utils │ │ │ └── DisplayUtil.java │ │ │ └── view │ │ │ ├── FlowLayout.java │ │ │ ├── OnSkuListener.java │ │ │ ├── SkuItemLayout.java │ │ │ ├── SkuItemView.java │ │ │ ├── SkuMaxHeightScrollView.java │ │ │ └── SkuSelectScrollView.java │ └── res │ │ ├── anim │ │ ├── slide_in_from_bottom.xml │ │ └── slide_out_to_bottom.xml │ │ ├── color │ │ ├── quantity_operator_text_selector.xml │ │ └── sku_item_text_selector.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── shape_mfe5002.xml │ │ ├── sku_item_selector.xml │ │ ├── sku_quantity_input_selector.xml │ │ ├── sku_quantity_minus_selector.xml │ │ └── sku_quantity_plus_selector.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── dialog_sku.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── comm_close.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── comm_close.png │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── madreain │ └── sku │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── settings.gradle └── sku └── sku.gif /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android仿淘宝、京东商品选择器 2 | 3 | ### 效果图如下 4 | 5 | ![效果图](sku/sku.gif) 6 | 7 | ### sku相关数据 8 | SKU相关数据大致如下,其他参数可根据自身项目自由扩展 9 | 10 | ``` 11 | { 12 | "pid": 10001,//商品id 13 | "pictureUrl"://商品主图 14 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=95d3036dc6282a14fda223c3b55284be&imgtype=0&src=http%3A%2F%2Fimg4.vipshop.com%2Fupload%2Fmerchandise%2F27600%2FZIMMUR-ZMCQ2028-2.jpg", 15 | "stockQuantity": 939,//商品总库存 16 | "maxPrice": 199.00,//最高价 17 | "minPrice": 699.00,//最低价 18 | "skus": [ 19 | { 20 | "sid": 10001,//skuid 21 | "price": 199.00,//价格 22 | "pictureUrl": //商品图 23 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=95d3036dc6282a14fda223c3b55284be&imgtype=0&src=http%3A%2F%2Fimg4.vipshop.com%2Fupload%2Fmerchandise%2F27600%2FZIMMUR-ZMCQ2028-2.jpg", 24 | "stockQuantity": 133,//该商品库存 25 | "attributes": [ 26 | { 27 | "value": "M", 28 | "key": "尺码" 29 | }, 30 | { 31 | "value": "绿松石蓝色", 32 | "key": "颜色" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | 39 | ``` 40 | 41 | ### 相关设置 42 | 43 | 1.不同状态显示的设置 44 | sku_item_text_selector.xml sku单item字体显示的三种状态设置 45 | sku_item_selector.xml sku单item显示的三种状态设置 46 | quantity_operator_text_selector.xml 加减按钮的状态设置 47 | sku_quantity_input_selector.xml 输入框两种输入状态设置 48 | sku_quantity_minus_selector.xml 减按钮的背景状态设置 49 | sku_quantity_plus_selector.xml 加按钮的背景状态设置 50 | 51 | 2.最大高度、最小高度设置 52 | SkuMaxHeightScrollView中可在xml中设置maxSkuHeight、minSkuHeight,而SkuSelectScrollView继承自SkuMaxHeightScrollView也可直接设置 53 | 54 | ``` 55 | 62 | ``` 63 | 64 | ### 相关函数 65 | 66 | ``` 67 | scrollSkuList.setOnSkuListener(new OnSkuListener() { 68 | @Override 69 | public void onUnselected(SkuAttribute unselectedAttribute) { 70 | 71 | } 72 | 73 | @Override 74 | public void onSelect(SkuAttribute selectAttribute) { 75 | 76 | } 77 | 78 | @Override 79 | public void onSkuSelected(Sku sku) { 80 | 81 | } 82 | }); 83 | ``` 84 | 85 | 根据不同业务场景对监听方法做处理 86 | 87 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.madreain.sku" 7 | minSdkVersion 16 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | //ButterKnife 29 | implementation "com.jakewharton:butterknife:8.8.1" 30 | annotationProcessor "com.jakewharton:butterknife-compiler:8.8.1" 31 | //Glide https://github.com/bumptech/glide 32 | api "com.github.bumptech.glide:glide:4.6.1" 33 | annotationProcessor "com.github.bumptech.glide:compiler:4.6.1" 34 | //Gson 35 | api "com.google.code.gson:gson:2.8.2" 36 | } 37 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/madreain/sku/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.madreain.sku", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/assets/Product.json: -------------------------------------------------------------------------------- 1 | { 2 | "pid": 10001, 3 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=95d3036dc6282a14fda223c3b55284be&imgtype=0&src=http%3A%2F%2Fimg4.vipshop.com%2Fupload%2Fmerchandise%2F27600%2FZIMMUR-ZMCQ2028-2.jpg", 4 | "stockQuantity": 939, 5 | "maxPrice": 199.0000, 6 | "minPrice": 699.0000, 7 | "skus": [ 8 | { 9 | "sid": 10001, 10 | "price": 199.0000, 11 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=95d3036dc6282a14fda223c3b55284be&imgtype=0&src=http%3A%2F%2Fimg4.vipshop.com%2Fupload%2Fmerchandise%2F27600%2FZIMMUR-ZMCQ2028-2.jpg", 12 | "stockQuantity": 133, 13 | "attributes": [ 14 | { 15 | "value": "M", 16 | "key": "尺码" 17 | }, 18 | { 19 | "value": "绿松石蓝色", 20 | "key": "颜色" 21 | } 22 | ] 23 | }, 24 | { 25 | "sid": 10002, 26 | "price": 399.0000, 27 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=76331db1c1a7b9edad2583bd95d7e634&imgtype=0&src=http%3A%2F%2Fi1.ygimg.cn%2Fpics%2Fnowhere%2F2013%2F99862821%2F99862821_01_l.jpg", 28 | "stockQuantity": 333, 29 | "attributes": [ 30 | { 31 | "value": "M", 32 | "key": "尺码" 33 | }, 34 | { 35 | "value": "灰色", 36 | "key": "颜色" 37 | } 38 | ] 39 | }, 40 | { 41 | "sid": 10003, 42 | "price": 199.0000, 43 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=9d33c2c3adbf9d6af01f589777cd0bff&imgtype=0&src=http%3A%2F%2Fimg2.vipshop.com%2Fupload%2Fmerchandise%2F34424%2FZIMMUR-Z12CQW145WC-2.jpg", 44 | "stockQuantity": 23, 45 | "attributes": [ 46 | { 47 | "value": "L", 48 | "key": "尺码" 49 | }, 50 | { 51 | "value": "绿松石蓝色", 52 | "key": "颜色" 53 | } 54 | ] 55 | }, 56 | { 57 | "sid": 10004, 58 | "price": 499.0000, 59 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109966&di=ada65d9d4dbbfaf5e46944f469c64ebf&imgtype=0&src=http%3A%2F%2Fwww.bjzwx.com%2Fuploadfile%2F2017%2F1219%2F20171219082705892.jpg", 60 | "stockQuantity": 323, 61 | "attributes": [ 62 | { 63 | "value": "L", 64 | "key": "尺码" 65 | }, 66 | { 67 | "value": "灰色", 68 | "key": "颜色" 69 | } 70 | ] 71 | }, 72 | { 73 | "sid": 10005, 74 | "price": 699.0000, 75 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=158716506d68be6d18f033ab6a6518a7&imgtype=0&src=http%3A%2F%2Fa.vpimg3.com%2Fupload%2Fmerchandise%2F120170%2FOSA-SW332654065A-1.jpg", 76 | "stockQuantity": 3, 77 | "attributes": [ 78 | { 79 | "value": "XL", 80 | "key": "尺码" 81 | }, 82 | { 83 | "value": "粉红色", 84 | "key": "颜色" 85 | } 86 | ] 87 | }, 88 | { 89 | "sid": 10006, 90 | "price": 699.0000, 91 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=f5851b42bed9833f5f9e2a8884d027d8&imgtype=0&src=http%3A%2F%2Fimg.11665.com%2Fimg04_p%2Fi4%2FT1EX43FrFnXXXXXXXX_%2521%25210-item_pic.jpg", 92 | "stockQuantity": 1, 93 | "attributes": [ 94 | { 95 | "value": "L", 96 | "key": "尺码" 97 | }, 98 | { 99 | "value": "粉红色", 100 | "key": "颜色" 101 | } 102 | ] 103 | }, 104 | { 105 | "sid": 10007, 106 | "price": 699.0000, 107 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 108 | "stockQuantity": 123, 109 | "attributes": [ 110 | { 111 | "value": "M", 112 | "key": "尺码" 113 | }, 114 | { 115 | "value": "粉红色", 116 | "key": "颜色" 117 | } 118 | ] 119 | }, 120 | { 121 | "sid": 10008, 122 | "price": 699.0000, 123 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 124 | "stockQuantity": 123, 125 | "attributes": [ 126 | { 127 | "value": "S", 128 | "key": "尺码" 129 | }, 130 | { 131 | "value": "粉红色", 132 | "key": "颜色" 133 | } 134 | ] 135 | }, 136 | { 137 | "sid": 10010, 138 | "price": 699.0000, 139 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 140 | "stockQuantity": 123, 141 | "attributes": [ 142 | { 143 | "value": "XS", 144 | "key": "尺码" 145 | }, 146 | { 147 | "value": "粉红色", 148 | "key": "颜色" 149 | } 150 | ] 151 | }, 152 | { 153 | "sid": 10011, 154 | "price": 699.0000, 155 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 156 | "stockQuantity": 123, 157 | "attributes": [ 158 | { 159 | "value": "XXL", 160 | "key": "尺码" 161 | }, 162 | { 163 | "value": "粉红色", 164 | "key": "颜色" 165 | } 166 | ] 167 | }, 168 | { 169 | "sid": 10012, 170 | "price": 699.0000, 171 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 172 | "stockQuantity": 123, 173 | "attributes": [ 174 | { 175 | "value": "XXXL", 176 | "key": "尺码" 177 | }, 178 | { 179 | "value": "粉红色", 180 | "key": "颜色" 181 | } 182 | ] 183 | }, 184 | { 185 | "sid": 10013, 186 | "price": 699.0000, 187 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 188 | "stockQuantity": 123, 189 | "attributes": [ 190 | { 191 | "value": "XL", 192 | "key": "尺码" 193 | }, 194 | { 195 | "value": "大红色", 196 | "key": "颜色" 197 | } 198 | ] 199 | }, 200 | { 201 | "sid": 10014, 202 | "price": 699.0000, 203 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 204 | "stockQuantity": 123, 205 | "attributes": [ 206 | { 207 | "value": "XL", 208 | "key": "尺码" 209 | }, 210 | { 211 | "value": "橙色", 212 | "key": "颜色" 213 | } 214 | ] 215 | }, 216 | { 217 | "sid": 10015, 218 | "price": 699.0000, 219 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 220 | "stockQuantity": 123, 221 | "attributes": [ 222 | { 223 | "value": "XL", 224 | "key": "尺码" 225 | }, 226 | { 227 | "value": "咖啡色", 228 | "key": "颜色" 229 | } 230 | ] 231 | }, 232 | { 233 | "sid": 10016, 234 | "price": 699.0000, 235 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 236 | "stockQuantity": 123, 237 | "attributes": [ 238 | { 239 | "value": "XL", 240 | "key": "尺码" 241 | }, 242 | { 243 | "value": "橘黄色", 244 | "key": "颜色" 245 | } 246 | ] 247 | }, 248 | { 249 | "sid": 10017, 250 | "price": 699.0000, 251 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 252 | "stockQuantity": 123, 253 | "attributes": [ 254 | { 255 | "value": "XL", 256 | "key": "尺码" 257 | }, 258 | { 259 | "value": "海蓝色", 260 | "key": "颜色" 261 | } 262 | ] 263 | }, 264 | { 265 | "sid": 10018, 266 | "price": 699.0000, 267 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 268 | "stockQuantity": 123, 269 | "attributes": [ 270 | { 271 | "value": "XL", 272 | "key": "尺码" 273 | }, 274 | { 275 | "value": "宝蓝色", 276 | "key": "颜色" 277 | } 278 | ] 279 | }, 280 | { 281 | "sid": 10019, 282 | "price": 699.0000, 283 | "pictureUrl": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1564393109965&di=fff806632f0c299b7a3572a3900360f4&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fblog%2F201411%2F07%2F20141107201918_8Q4fV.jpeg", 284 | "stockQuantity": 1, 285 | "attributes": [ 286 | { 287 | "value": "XL", 288 | "key": "尺码" 289 | }, 290 | { 291 | "value": "墨绿色", 292 | "key": "颜色" 293 | } 294 | ] 295 | } 296 | ] 297 | } -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/AppGlideModule.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.bumptech.glide.GlideBuilder; 7 | import com.bumptech.glide.annotation.GlideModule; 8 | import com.bumptech.glide.load.DecodeFormat; 9 | import com.bumptech.glide.load.engine.DiskCacheStrategy; 10 | import com.bumptech.glide.request.RequestOptions; 11 | 12 | /** 13 | * @author madreain 14 | * @date 2019-07-29. 15 | * module: 16 | * description: 17 | */ 18 | @GlideModule 19 | public class AppGlideModule extends com.bumptech.glide.module.AppGlideModule { 20 | @Override 21 | public boolean isManifestParsingEnabled() { 22 | return false; 23 | } 24 | 25 | @Override 26 | public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { 27 | RequestOptions requestOptions = new RequestOptions() 28 | .centerCrop() 29 | .dontAnimate() 30 | .disallowHardwareConfig() 31 | .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)//表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)。 32 | .format(DecodeFormat.PREFER_RGB_565); 33 | builder.setDefaultRequestOptions(requestOptions); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.TextView; 7 | import android.widget.Toast; 8 | 9 | import com.madreain.sku.bean.ProductData; 10 | import com.madreain.sku.bean.Sku; 11 | 12 | import butterknife.ButterKnife; 13 | 14 | /** 15 | * @author madreain 16 | * @date 2019-07-27. 17 | * module: 18 | * description: 19 | */ 20 | public class MainActivity extends AppCompatActivity { 21 | 22 | private TextView tv; 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_main); 27 | tv=findViewById(R.id.tv); 28 | tv.setOnClickListener(new View.OnClickListener() { 29 | @Override 30 | public void onClick(View v) { 31 | showSkuDialog(); 32 | } 33 | }); 34 | } 35 | 36 | //商品sku 37 | private ProductSkuDialog dialog; 38 | 39 | private void showSkuDialog() { 40 | if (dialog == null) { 41 | dialog = new ProductSkuDialog(this); 42 | dialog.setData(ProductData.get(this), new ProductSkuDialog.Callback() { 43 | @Override 44 | public void onAdded(Sku sku, int quantity) { 45 | //立即购买再跳转到 46 | Toast.makeText(MainActivity.this, "添加购物车成功 "+sku.toString(), Toast.LENGTH_SHORT).show(); 47 | } 48 | 49 | @Override 50 | public void onSelect(String selected) { 51 | //默认设置 有选的 52 | tv.setText(selected); 53 | } 54 | 55 | @Override 56 | public void reUnSelect() { 57 | 58 | } 59 | }); 60 | } 61 | dialog.show(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/ProductSkuDialog.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku; 2 | 3 | import android.app.Dialog; 4 | import android.content.Context; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.StyleRes; 7 | import android.text.TextUtils; 8 | import android.view.Gravity; 9 | import android.view.KeyEvent; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.view.Window; 13 | import android.view.WindowManager; 14 | import android.view.inputmethod.EditorInfo; 15 | import android.widget.EditText; 16 | import android.widget.ImageButton; 17 | import android.widget.ImageView; 18 | import android.widget.TextView; 19 | import android.widget.Toast; 20 | 21 | import com.madreain.sku.bean.ProductData; 22 | import com.madreain.sku.bean.Sku; 23 | import com.madreain.sku.bean.SkuAttribute; 24 | import com.madreain.sku.view.OnSkuListener; 25 | import com.madreain.sku.view.SkuSelectScrollView; 26 | 27 | import java.util.List; 28 | 29 | import butterknife.BindView; 30 | import butterknife.ButterKnife; 31 | 32 | /** 33 | * @author madreain 34 | * @date 2019-07-27. 35 | * module: 36 | * description: 37 | */ 38 | public class ProductSkuDialog extends Dialog { 39 | 40 | @BindView(R.id.ib_sku_close) 41 | ImageButton ibSkuClose; 42 | @BindView(R.id.tv_sku_info) 43 | TextView tvSkuInfo; 44 | @BindView(R.id.tv_sku_selling_price) 45 | TextView tvSkuSellingPrice; 46 | @BindView(R.id.scroll_sku_list) 47 | SkuSelectScrollView scrollSkuList; 48 | @BindView(R.id.btn_submit) 49 | TextView btnSubmit; 50 | @BindView(R.id.iv_sku_logo) 51 | ImageView ivSkuLogo; 52 | @BindView(R.id.tv_sku_quantity_label) 53 | TextView tvSkuQuantityLabel; 54 | @BindView(R.id.tv_sku_quantity) 55 | TextView tvSkuQuantity; 56 | @BindView(R.id.btn_sku_quantity_minus) 57 | TextView btnSkuQuantityMinus; 58 | @BindView(R.id.et_sku_quantity_input) 59 | EditText etSkuQuantityInput; 60 | @BindView(R.id.btn_sku_quantity_plus) 61 | TextView btnSkuQuantityPlus; 62 | 63 | 64 | private Context context; 65 | private ProductData product; 66 | private List skuList; 67 | private Callback callback; 68 | private Sku selectedSku; 69 | private String priceFormat; 70 | private String stockQuantityFormat; 71 | 72 | public ProductSkuDialog(@NonNull Context context) { 73 | this(context, R.style.CommonBottomDialogStyle); 74 | } 75 | 76 | public ProductSkuDialog(@NonNull Context context, @StyleRes int themeResId) { 77 | super(context, themeResId); 78 | this.context = context; 79 | initView(); 80 | } 81 | 82 | private void initView() { 83 | View view = View.inflate(context, R.layout.dialog_sku, null); 84 | Window window = this.getWindow(); 85 | window.setContentView(view); 86 | ButterKnife.bind(this, view); 87 | //关闭 88 | ibSkuClose.setOnClickListener(new View.OnClickListener() { 89 | @Override 90 | public void onClick(View v) { 91 | dismiss(); 92 | } 93 | }); 94 | //减少 95 | btnSkuQuantityMinus.setOnClickListener(new View.OnClickListener() { 96 | @Override 97 | public void onClick(View v) { 98 | String quantity = etSkuQuantityInput.getText().toString(); 99 | if (TextUtils.isEmpty(quantity)) { 100 | return; 101 | } 102 | int quantityInt = Integer.parseInt(quantity); 103 | if (quantityInt > 1) { 104 | String newQuantity = String.valueOf(quantityInt - 1); 105 | etSkuQuantityInput.setText(newQuantity); 106 | etSkuQuantityInput.setSelection(newQuantity.length()); 107 | updateQuantityOperator(quantityInt - 1); 108 | } 109 | } 110 | }); 111 | //增加 112 | btnSkuQuantityPlus.setOnClickListener(new View.OnClickListener() { 113 | @Override 114 | public void onClick(View v) { 115 | String quantity = etSkuQuantityInput.getText().toString(); 116 | if (TextUtils.isEmpty(quantity) || selectedSku == null) { 117 | return; 118 | } 119 | int quantityInt = Integer.parseInt(quantity); 120 | if (quantityInt < selectedSku.getStockQuantity()) { 121 | String newQuantity = String.valueOf(quantityInt + 1); 122 | etSkuQuantityInput.setText(newQuantity); 123 | etSkuQuantityInput.setSelection(newQuantity.length()); 124 | updateQuantityOperator(quantityInt + 1); 125 | } 126 | } 127 | }); 128 | //输入 129 | etSkuQuantityInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { 130 | @Override 131 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 132 | if (actionId != EditorInfo.IME_ACTION_DONE || selectedSku == null) { 133 | return false; 134 | } 135 | String quantity = etSkuQuantityInput.getText().toString(); 136 | if (TextUtils.isEmpty(quantity)) { 137 | return false; 138 | } 139 | int quantityInt = Integer.parseInt(quantity); 140 | if (quantityInt <= 1) { 141 | etSkuQuantityInput.setText("1"); 142 | etSkuQuantityInput.setSelection(1); 143 | updateQuantityOperator(1); 144 | } else if (quantityInt >= selectedSku.getStockQuantity()) { 145 | String newQuantity = String.valueOf(selectedSku.getStockQuantity()); 146 | etSkuQuantityInput.setText(newQuantity); 147 | etSkuQuantityInput.setSelection(newQuantity.length()); 148 | updateQuantityOperator(selectedSku.getStockQuantity()); 149 | } else { 150 | etSkuQuantityInput.setSelection(quantity.length()); 151 | updateQuantityOperator(quantityInt); 152 | } 153 | return false; 154 | } 155 | }); 156 | //sku整体的点击事件 157 | scrollSkuList.setOnSkuListener(new OnSkuListener() { 158 | @Override 159 | public void onUnselected(SkuAttribute unselectedAttribute) { 160 | selectedSku = null; 161 | //默认第一张照片 162 | GlideApp.with(context).load(product.getPictureUrl()).into(ivSkuLogo); 163 | tvSkuQuantity.setText(String.format(stockQuantityFormat, product.getStockQuantity())); 164 | String selected = getSelected(); 165 | //设置回掉 166 | callback.onSelect(selected); 167 | //增加前缀 168 | if (isHaveSelect()) { 169 | selected = "已选: " + selected; 170 | } else { 171 | selected = "请选择: " + selected; 172 | tvSkuSellingPrice.setText(String.format(priceFormat, product.getMinPrice())); 173 | } 174 | tvSkuInfo.setText(selected); 175 | btnSubmit.setEnabled(false); 176 | etSkuQuantityInput.setText("1"); 177 | updateQuantityOperator(1); 178 | } 179 | 180 | @Override 181 | public void onSelect(SkuAttribute selectAttribute) { 182 | String selected = getSelected(); 183 | //设置回掉 184 | callback.onSelect(selected); 185 | //增加前缀 186 | if (isHaveSelect()) { 187 | selected = "已选: " + selected; 188 | } else { 189 | selected = "请选择: " + selected; 190 | tvSkuSellingPrice.setText(String.format(priceFormat, product.getMinPrice())); 191 | } 192 | tvSkuInfo.setText(selected); 193 | etSkuQuantityInput.setText("1"); 194 | updateQuantityOperator(1); 195 | } 196 | 197 | @Override 198 | public void onSkuSelected(Sku sku) { 199 | selectedSku = sku; 200 | GlideApp.with(context).load(selectedSku.getPictureUrl()).into(ivSkuLogo); 201 | List attributeList = selectedSku.getAttributes(); 202 | StringBuilder builder = new StringBuilder(); 203 | for (int i = 0; i < attributeList.size(); i++) { 204 | if (i != 0) { 205 | builder.append(" "); 206 | } 207 | SkuAttribute attribute = attributeList.get(i); 208 | builder.append("\"" + attribute.getValue() + "\""); 209 | } 210 | tvSkuInfo.setText("已选:" + builder.toString()); 211 | setHaveSelect(true); 212 | //设置价格 213 | tvSkuSellingPrice.setText(String.format(priceFormat, sku.getPrice())); 214 | callback.onSelect(builder.toString()); 215 | tvSkuQuantity.setText(String.format(stockQuantityFormat, selectedSku.getStockQuantity())); 216 | btnSubmit.setEnabled(true); 217 | if (sku.getStockQuantity() >= 1) { 218 | etSkuQuantityInput.setText("1"); 219 | updateQuantityOperator(1); 220 | } else { 221 | etSkuQuantityInput.setText("0"); 222 | updateQuantityOperator(0); 223 | } 224 | } 225 | }); 226 | //添加 227 | btnSubmit.setOnClickListener(new View.OnClickListener() { 228 | @Override 229 | public void onClick(View v) { 230 | String quantity = etSkuQuantityInput.getText().toString(); 231 | if (TextUtils.isEmpty(quantity)) { 232 | return; 233 | } 234 | int quantityInt = Integer.parseInt(quantity); 235 | if (quantityInt > 0 && quantityInt <= selectedSku.getStockQuantity()) { 236 | callback.onAdded(selectedSku, quantityInt); 237 | dismiss(); 238 | } else { 239 | Toast.makeText(getContext(), "商品数量超出库存,请修改数量", Toast.LENGTH_SHORT).show(); 240 | } 241 | } 242 | }); 243 | } 244 | 245 | /** 246 | * 设置数据 247 | * @param product 248 | * @param callback 249 | */ 250 | public void setData(final ProductData product, Callback callback) { 251 | this.product = product; 252 | this.skuList = product.getSkus(); 253 | this.callback = callback; 254 | priceFormat = context.getString(R.string.comm_price_format); 255 | stockQuantityFormat = context.getString(R.string.sku_stock); 256 | updateSkuData(); 257 | updateQuantityOperator(1); 258 | } 259 | 260 | /** 261 | * 更新数据 262 | */ 263 | private void updateSkuData() { 264 | scrollSkuList.setSkuList(product.getSkus()); 265 | //默认选择第一个 266 | Sku firstSku = product.getSkus().get(0); 267 | if (firstSku.getStockQuantity() > 0) { 268 | selectedSku = firstSku; 269 | // 选中第一个sku 270 | scrollSkuList.setSelectedSku(selectedSku); 271 | GlideApp.with(context).load(product.getPictureUrl()).into(ivSkuLogo); 272 | tvSkuSellingPrice.setText(String.format(priceFormat, selectedSku.getPrice())); 273 | tvSkuQuantity.setText(String.format(stockQuantityFormat, selectedSku.getStockQuantity())); 274 | btnSubmit.setEnabled(selectedSku.getStockQuantity() > 0); 275 | List attributeList = selectedSku.getAttributes(); 276 | StringBuilder builder = new StringBuilder(); 277 | int attributeListSize = attributeList.size(); 278 | for (int i = 0; i < attributeListSize; i++) { 279 | if (i != 0) { 280 | builder.append(" "); 281 | } 282 | SkuAttribute attribute = attributeList.get(i); 283 | builder.append("\"" + attribute.getValue() + "\""); 284 | } 285 | tvSkuInfo.setText("已选:" + builder.toString()); 286 | setHaveSelect(true); 287 | callback.onSelect(builder.toString()); 288 | } else { 289 | GlideApp.with(context).load(product.getPictureUrl()).into(ivSkuLogo); 290 | tvSkuSellingPrice.setText(String.format(priceFormat, product.getMinPrice())); 291 | tvSkuQuantity.setText(String.format(stockQuantityFormat, product.getStockQuantity())); 292 | btnSubmit.setEnabled(false); 293 | tvSkuInfo.setText("请选择:" + skuList.get(0).getAttributes().get(0).getKey()); 294 | setHaveSelect(false); 295 | callback.reUnSelect(); 296 | } 297 | } 298 | 299 | /** 300 | * 获取已选中的 301 | * 302 | * @return 303 | */ 304 | public boolean isHaveSelect; 305 | 306 | public boolean isHaveSelect() { 307 | return isHaveSelect; 308 | } 309 | 310 | public void setHaveSelect(boolean haveSelect) { 311 | isHaveSelect = haveSelect; 312 | } 313 | 314 | /** 315 | * 获取显示的内容 316 | * 317 | * @return 318 | */ 319 | private String getSelected() { 320 | List skuAttributeList = scrollSkuList.getSelectedAttributeList(); 321 | if (skuAttributeList != null) { 322 | //是否有属性是选中的 323 | setHaveSelect(false); 324 | StringBuilder stringBuilder = new StringBuilder(); 325 | int skuAttributeListSize = skuAttributeList.size(); 326 | for (int i = 0; i < skuAttributeListSize; i++) { 327 | SkuAttribute skuAttribute = skuAttributeList.get(i); 328 | if (i != 0) { 329 | stringBuilder.append(" "); 330 | } 331 | if (TextUtils.isEmpty(skuAttribute.getValue())) { 332 | stringBuilder.append(skuAttribute.getKey()); 333 | } else { 334 | stringBuilder.append(skuAttribute.getValue()); 335 | setHaveSelect(true); 336 | } 337 | } 338 | return stringBuilder.toString(); 339 | } else 340 | return null; 341 | } 342 | 343 | private void updateQuantityOperator(int newQuantity) { 344 | if (selectedSku == null) { 345 | btnSkuQuantityMinus.setEnabled(false); 346 | btnSkuQuantityPlus.setEnabled(false); 347 | etSkuQuantityInput.setEnabled(false); 348 | } else { 349 | if (newQuantity <= 1) { 350 | btnSkuQuantityMinus.setEnabled(false); 351 | btnSkuQuantityPlus.setEnabled(true); 352 | } else if (newQuantity >= selectedSku.getStockQuantity()) { 353 | btnSkuQuantityMinus.setEnabled(true); 354 | btnSkuQuantityPlus.setEnabled(false); 355 | } else { 356 | btnSkuQuantityMinus.setEnabled(true); 357 | btnSkuQuantityPlus.setEnabled(true); 358 | } 359 | etSkuQuantityInput.setEnabled(true); 360 | } 361 | 362 | } 363 | 364 | @Override 365 | public void onAttachedToWindow() { 366 | super.onAttachedToWindow(); 367 | // 解决键盘遮挡输入框问题 368 | Window window = getWindow(); 369 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); 370 | window.setGravity(Gravity.BOTTOM); 371 | window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 372 | window.getDecorView().setPadding(0, 0, 0, 0); 373 | } 374 | 375 | public interface Callback { 376 | //添加购物车 377 | void onAdded(Sku sku, int quantity); 378 | 379 | //已选 380 | void onSelect(String selected); 381 | 382 | //恢复默认未选 383 | void reUnSelect(); 384 | } 385 | 386 | } 387 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/bean/ProductData.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.bean; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Keep; 5 | import android.util.Log; 6 | 7 | import com.google.gson.Gson; 8 | import com.google.gson.reflect.TypeToken; 9 | import com.madreain.sku.R; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.IOException; 13 | import java.io.InputStreamReader; 14 | import java.io.UnsupportedEncodingException; 15 | import java.util.List; 16 | 17 | /** 18 | * @author madreain 19 | * @date 2019-07-27. 20 | * module: 21 | * description: 22 | */ 23 | @Keep 24 | public class ProductData { 25 | 26 | //商品id 27 | private long pid; 28 | 29 | //主图url 30 | private String pictureUrl; 31 | 32 | //最高价格 33 | private float maxPrice; 34 | 35 | //最低价格 36 | private float minPrice; 37 | 38 | //sku属性 39 | private List skus; 40 | 41 | //数量 42 | private int stockQuantity; 43 | 44 | public long getPid() { 45 | return pid; 46 | } 47 | 48 | public void setPid(long pid) { 49 | this.pid = pid; 50 | } 51 | 52 | public int getStockQuantity() { 53 | return stockQuantity; 54 | } 55 | 56 | public void setStockQuantity(int stockQuantity) { 57 | this.stockQuantity = stockQuantity; 58 | } 59 | 60 | public static ProductData get(Context context) { 61 | String json = readTextFromSDcard(context); 62 | return new Gson().fromJson(json, new TypeToken() { 63 | }.getType()); 64 | } 65 | 66 | private static String readTextFromSDcard(Context context) { 67 | String resultString = null; 68 | InputStreamReader inputStreamReader; 69 | try { 70 | inputStreamReader = new InputStreamReader(context.getAssets().open("Product.json"), "UTF-8"); 71 | BufferedReader bufferedReader = new BufferedReader( 72 | inputStreamReader); 73 | String line; 74 | StringBuilder stringBuilder = new StringBuilder(); 75 | while ((line = bufferedReader.readLine()) != null) { 76 | stringBuilder.append(line); 77 | } 78 | inputStreamReader.close(); 79 | bufferedReader.close(); 80 | resultString = stringBuilder.toString(); 81 | } catch (UnsupportedEncodingException e) { 82 | e.printStackTrace(); 83 | } catch (IOException e) { 84 | e.printStackTrace(); 85 | } 86 | return resultString; 87 | } 88 | 89 | public String getPictureUrl() { 90 | return pictureUrl; 91 | } 92 | 93 | public void setPictureUrl(String pictureUrl) { 94 | this.pictureUrl = pictureUrl; 95 | } 96 | 97 | public float getMaxPrice() { 98 | return maxPrice; 99 | } 100 | 101 | public void setMaxPrice(float maxPrice) { 102 | this.maxPrice = maxPrice; 103 | } 104 | 105 | public float getMinPrice() { 106 | return minPrice; 107 | } 108 | 109 | public void setMinPrice(float minPrice) { 110 | this.minPrice = minPrice; 111 | } 112 | 113 | public List getSkus() { 114 | return skus; 115 | } 116 | 117 | public void setSkus(List skus) { 118 | this.skus = skus; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/bean/Sku.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.bean; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.support.annotation.Keep; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author madreain 11 | * @date 2019-07-27. 12 | * module: 13 | * description: 14 | */ 15 | @Keep 16 | public class Sku implements Parcelable { 17 | 18 | private long sid; 19 | 20 | private float price; 21 | 22 | private String pictureUrl; 23 | 24 | private List attributes; 25 | 26 | private int stockQuantity; 27 | 28 | public long getSid() { 29 | return sid; 30 | } 31 | 32 | public void setSid(long sid) { 33 | this.sid = sid; 34 | } 35 | 36 | public int getStockQuantity() { 37 | return stockQuantity; 38 | } 39 | 40 | public void setStockQuantity(int stockQuantity) { 41 | this.stockQuantity = stockQuantity; 42 | } 43 | 44 | public float getPrice() { 45 | return price; 46 | } 47 | 48 | public void setPrice(float price) { 49 | this.price = price; 50 | } 51 | 52 | public String getPictureUrl() { 53 | return pictureUrl; 54 | } 55 | 56 | public void setPictureUrl(String pictureUrl) { 57 | this.pictureUrl = pictureUrl; 58 | } 59 | 60 | public List getAttributes() { 61 | return attributes; 62 | } 63 | 64 | public void setAttributes(List attributes) { 65 | this.attributes = attributes; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "Sku{" + 71 | "sid=" + sid + 72 | ", price=" + price + 73 | ", pictureUrl='" + pictureUrl + '\'' + 74 | ", attributes=" + attributes + 75 | ", stockQuantity=" + stockQuantity + 76 | '}'; 77 | } 78 | 79 | public Sku() { 80 | 81 | } 82 | 83 | /** 84 | * 描述 85 | * @return 86 | */ 87 | @Override 88 | public int describeContents() { 89 | return 0; 90 | } 91 | 92 | /** 93 | * 反序列化 94 | * @param dest 95 | * @param flags 96 | */ 97 | @Override 98 | public void writeToParcel(Parcel dest, int flags) { 99 | dest.writeLong(this.sid); 100 | dest.writeFloat(this.price); 101 | dest.writeString(this.pictureUrl); 102 | dest.writeTypedList(this.attributes); 103 | } 104 | 105 | /** 106 | * 序列化 107 | * @param in 108 | */ 109 | protected Sku(Parcel in) { 110 | this.sid = in.readLong(); 111 | this.price = in.readFloat(); 112 | this.pictureUrl = in.readString(); 113 | this.attributes = in.createTypedArrayList(SkuAttribute.CREATOR); 114 | } 115 | 116 | public static final Creator CREATOR = new Creator() { 117 | @Override 118 | public Sku createFromParcel(Parcel source) { 119 | return new Sku(source); 120 | } 121 | 122 | @Override 123 | public Sku[] newArray(int size) { 124 | return new Sku[size]; 125 | } 126 | }; 127 | 128 | 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/bean/SkuAttribute.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.bean; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.support.annotation.Keep; 6 | 7 | /** 8 | * @author madreain 9 | * @date 2019-07-27. 10 | * module: 11 | * description: 12 | */ 13 | @Keep 14 | public class SkuAttribute implements Parcelable { 15 | 16 | private String key; 17 | 18 | private String value; 19 | 20 | public SkuAttribute() { 21 | } 22 | 23 | public SkuAttribute(String key, String value) { 24 | this.key = key; 25 | this.value = value; 26 | } 27 | 28 | public String getValue() { 29 | return value; 30 | } 31 | 32 | public void setValue(String value) { 33 | this.value = value; 34 | } 35 | 36 | public String getKey() { 37 | return key; 38 | } 39 | 40 | public void setKey(String key) { 41 | this.key = key; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "SkuAttribute{" + 47 | "key='" + key + '\'' + 48 | ", value='" + value + '\'' + 49 | '}'; 50 | } 51 | 52 | /** 53 | * 描述 54 | * @return 55 | */ 56 | @Override 57 | public int describeContents() { 58 | return 0; 59 | } 60 | 61 | /** 62 | * 反序列化 63 | * @param dest 64 | * @param flags 65 | */ 66 | @Override 67 | public void writeToParcel(Parcel dest, int flags) { 68 | dest.writeString(this.key); 69 | dest.writeString(this.value); 70 | } 71 | 72 | /** 73 | * 序列化 74 | * @param in 75 | */ 76 | protected SkuAttribute(Parcel in) { 77 | this.key = in.readString(); 78 | this.value = in.readString(); 79 | } 80 | 81 | public static final Creator CREATOR = new Creator() { 82 | @Override 83 | public SkuAttribute createFromParcel(Parcel source) { 84 | return new SkuAttribute(source); 85 | } 86 | 87 | @Override 88 | public SkuAttribute[] newArray(int size) { 89 | return new SkuAttribute[size]; 90 | } 91 | }; 92 | 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/utils/DisplayUtil.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.utils; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * @author madreain 7 | * @date 2019-07-27. 8 | * module: 9 | * description: 10 | */ 11 | public class DisplayUtil { 12 | 13 | public static int dp2px(Context context, float dp) { 14 | final float scale = context.getResources().getDisplayMetrics().density; 15 | return (int) (dp * scale + 0.5f); 16 | } 17 | 18 | public static int px2dp(Context context, float pxValue) { 19 | final float scale = context.getResources().getDisplayMetrics().density; 20 | return (int) (pxValue / scale + 0.5f); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/view/FlowLayout.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.util.TypedValue; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | 11 | import com.madreain.sku.R; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * @author madreain 18 | * @date 2019-07-27. 19 | * module: 20 | * description: 21 | */ 22 | public class FlowLayout extends ViewGroup { 23 | private static final String LOG_TAG = FlowLayout.class.getSimpleName(); 24 | 25 | /** 26 | * Special value for the child view spacing. 27 | * SPACING_AUTO means that the actual spacing is calculated according to the size of the 28 | * container and the number of the child views, so that the child views are placed evenly in 29 | * the container. 30 | */ 31 | public static final int SPACING_AUTO = -65536; 32 | 33 | /** 34 | * Special value for the horizontal spacing of the child views in the last row 35 | * SPACING_ALIGN means that the horizontal spacing of the child views in the last row keeps 36 | * the same with the spacing used in the row above. If there is only one row, this value is 37 | * ignored and the spacing will be calculated according to childSpacing. 38 | */ 39 | public static final int SPACING_ALIGN = -65537; 40 | 41 | private static final int SPACING_UNDEFINED = -65538; 42 | 43 | private static final boolean DEFAULT_FLOW = true; 44 | private static final int DEFAULT_CHILD_SPACING = 0; 45 | private static final int DEFAULT_CHILD_SPACING_FOR_LAST_ROW = SPACING_UNDEFINED; 46 | private static final float DEFAULT_ROW_SPACING = 0; 47 | private static final boolean DEFAULT_RTL = false; 48 | private static final int DEFAULT_MAX_ROWS = Integer.MAX_VALUE; 49 | 50 | private boolean mFlow = DEFAULT_FLOW; 51 | private int mChildSpacing = DEFAULT_CHILD_SPACING; 52 | private int mChildSpacingForLastRow = DEFAULT_CHILD_SPACING_FOR_LAST_ROW; 53 | private float mRowSpacing = DEFAULT_ROW_SPACING; 54 | private float mAdjustedRowSpacing = DEFAULT_ROW_SPACING; 55 | private boolean mRtl = DEFAULT_RTL; 56 | private int mMaxRows = DEFAULT_MAX_ROWS; 57 | 58 | private List mHorizontalSpacingForRow = new ArrayList<>(); 59 | private List mHeightForRow = new ArrayList<>(); 60 | private List mChildNumForRow = new ArrayList<>(); 61 | 62 | public FlowLayout(Context context) { 63 | this(context, null); 64 | } 65 | 66 | public FlowLayout(Context context, AttributeSet attrs) { 67 | super(context, attrs); 68 | 69 | TypedArray a = context.getTheme().obtainStyledAttributes( 70 | attrs, R.styleable.FlowLayout, 0, 0); 71 | try { 72 | mFlow = a.getBoolean(R.styleable.FlowLayout_flow, DEFAULT_FLOW); 73 | try { 74 | mChildSpacing = a.getInt(R.styleable.FlowLayout_childSpacing, DEFAULT_CHILD_SPACING); 75 | } catch (NumberFormatException e) { 76 | mChildSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_childSpacing, (int) dpToPx(DEFAULT_CHILD_SPACING)); 77 | } 78 | try { 79 | mChildSpacingForLastRow = a.getInt(R.styleable.FlowLayout_childSpacingForLastRow, SPACING_UNDEFINED); 80 | } catch (NumberFormatException e) { 81 | mChildSpacingForLastRow = a.getDimensionPixelSize(R.styleable.FlowLayout_childSpacingForLastRow, (int) dpToPx(DEFAULT_CHILD_SPACING)); 82 | } 83 | try { 84 | mRowSpacing = a.getInt(R.styleable.FlowLayout_rowSpacing, 0); 85 | } catch (NumberFormatException e) { 86 | mRowSpacing = a.getDimension(R.styleable.FlowLayout_rowSpacing, dpToPx(DEFAULT_ROW_SPACING)); 87 | } 88 | mMaxRows = a.getInt(R.styleable.FlowLayout_maxRows, DEFAULT_MAX_ROWS); 89 | mRtl = a.getBoolean(R.styleable.FlowLayout_rtl, DEFAULT_RTL); 90 | } finally { 91 | a.recycle(); 92 | } 93 | } 94 | 95 | @Override 96 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 97 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 98 | 99 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 100 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 101 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 102 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 103 | 104 | mHorizontalSpacingForRow.clear(); 105 | mChildNumForRow.clear(); 106 | mHeightForRow.clear(); 107 | 108 | int measuredHeight = 0, measuredWidth = 0, childCount = getChildCount(); 109 | int rowWidth = 0, maxChildHeightInRow = 0, childNumInRow = 0; 110 | int rowSize = widthSize - getPaddingLeft() - getPaddingRight(); 111 | boolean allowFlow = widthMode != MeasureSpec.UNSPECIFIED && mFlow; 112 | int childSpacing = mChildSpacing == SPACING_AUTO && widthMode == MeasureSpec.UNSPECIFIED 113 | ? 0 : mChildSpacing; 114 | float tmpSpacing = childSpacing == SPACING_AUTO ? 0 : childSpacing; 115 | 116 | for (int i = 0; i < childCount; i++) { 117 | View child = getChildAt(i); 118 | if (child.getVisibility() == GONE) { 119 | continue; 120 | } 121 | 122 | LayoutParams childParams = child.getLayoutParams(); 123 | int horizontalMargin = 0, verticalMargin = 0; 124 | if (childParams instanceof MarginLayoutParams) { 125 | measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, measuredHeight); 126 | MarginLayoutParams marginParams = (MarginLayoutParams) childParams; 127 | horizontalMargin = marginParams.leftMargin + marginParams.rightMargin; 128 | verticalMargin = marginParams.topMargin + marginParams.bottomMargin; 129 | } else { 130 | measureChild(child, widthMeasureSpec, heightMeasureSpec); 131 | } 132 | 133 | int childWidth = child.getMeasuredWidth() + horizontalMargin; 134 | int childHeight = child.getMeasuredHeight() + verticalMargin; 135 | if (allowFlow && rowWidth + childWidth > rowSize) { // Need flow to next row 136 | // Save parameters for current row 137 | mHorizontalSpacingForRow.add( 138 | getSpacingForRow(childSpacing, rowSize, rowWidth, childNumInRow)); 139 | mChildNumForRow.add(childNumInRow); 140 | mHeightForRow.add(maxChildHeightInRow); 141 | if (mHorizontalSpacingForRow.size() <= mMaxRows) { 142 | measuredHeight += maxChildHeightInRow; 143 | } 144 | measuredWidth = Math.max(measuredWidth, rowWidth); 145 | 146 | // Place the child view to next row 147 | childNumInRow = 1; 148 | rowWidth = childWidth + (int) tmpSpacing; 149 | maxChildHeightInRow = childHeight; 150 | } else { 151 | childNumInRow++; 152 | rowWidth += childWidth + tmpSpacing; 153 | maxChildHeightInRow = Math.max(maxChildHeightInRow, childHeight); 154 | } 155 | } 156 | 157 | // Measure remaining child views in the last row 158 | if (mChildSpacingForLastRow == SPACING_ALIGN) { 159 | // For SPACING_ALIGN, use the same spacing from the row above if there is more than one 160 | // row. 161 | if (mHorizontalSpacingForRow.size() >= 1) { 162 | mHorizontalSpacingForRow.add( 163 | mHorizontalSpacingForRow.get(mHorizontalSpacingForRow.size() - 1)); 164 | } else { 165 | mHorizontalSpacingForRow.add( 166 | getSpacingForRow(childSpacing, rowSize, rowWidth, childNumInRow)); 167 | } 168 | } else if (mChildSpacingForLastRow != SPACING_UNDEFINED) { 169 | // For SPACING_AUTO and specific DP values, apply them to the spacing strategy. 170 | mHorizontalSpacingForRow.add( 171 | getSpacingForRow(mChildSpacingForLastRow, rowSize, rowWidth, childNumInRow)); 172 | } else { 173 | // For SPACING_UNDEFINED, apply childSpacing to the spacing strategy for the last row. 174 | mHorizontalSpacingForRow.add( 175 | getSpacingForRow(childSpacing, rowSize, rowWidth, childNumInRow)); 176 | } 177 | 178 | mChildNumForRow.add(childNumInRow); 179 | mHeightForRow.add(maxChildHeightInRow); 180 | if (mHorizontalSpacingForRow.size() <= mMaxRows) { 181 | measuredHeight += maxChildHeightInRow; 182 | } 183 | measuredWidth = Math.max(measuredWidth, rowWidth); 184 | 185 | if (childSpacing == SPACING_AUTO) { 186 | measuredWidth = widthSize; 187 | } else if (widthMode == MeasureSpec.UNSPECIFIED) { 188 | measuredWidth = measuredWidth + getPaddingLeft() + getPaddingRight(); 189 | } else { 190 | measuredWidth = Math.min(measuredWidth + getPaddingLeft() + getPaddingRight(), widthSize); 191 | } 192 | 193 | measuredHeight += getPaddingTop() + getPaddingBottom(); 194 | int rowNum = Math.min(mHorizontalSpacingForRow.size(), mMaxRows); 195 | float rowSpacing = mRowSpacing == SPACING_AUTO && heightMode == MeasureSpec.UNSPECIFIED 196 | ? 0 : mRowSpacing; 197 | if (rowSpacing == SPACING_AUTO) { 198 | if (rowNum > 1) { 199 | mAdjustedRowSpacing = (heightSize - measuredHeight) / (rowNum - 1); 200 | } else { 201 | mAdjustedRowSpacing = 0; 202 | } 203 | measuredHeight = heightSize; 204 | } else { 205 | mAdjustedRowSpacing = rowSpacing; 206 | if (rowNum > 1) { 207 | measuredHeight = heightMode == MeasureSpec.UNSPECIFIED 208 | ? ((int) (measuredHeight + mAdjustedRowSpacing * (rowNum - 1))) 209 | : (Math.min((int) (measuredHeight + mAdjustedRowSpacing * (rowNum - 1)), 210 | heightSize)); 211 | } 212 | } 213 | 214 | measuredWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : measuredWidth; 215 | measuredHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : measuredHeight; 216 | setMeasuredDimension(measuredWidth, measuredHeight); 217 | } 218 | 219 | @Override 220 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 221 | int paddingLeft = getPaddingLeft(); 222 | int paddingRight = getPaddingRight(); 223 | int paddingTop = getPaddingTop(); 224 | int x = mRtl ? (getWidth() - paddingRight) : paddingLeft; 225 | int y = paddingTop; 226 | 227 | int rowCount = mChildNumForRow.size(), childIdx = 0; 228 | for (int row = 0; row < rowCount; row++) { 229 | int childNum = mChildNumForRow.get(row); 230 | int rowHeight = mHeightForRow.get(row); 231 | float spacing = mHorizontalSpacingForRow.get(row); 232 | for (int i = 0; i < childNum && childIdx < getChildCount(); ) { 233 | View child = getChildAt(childIdx++); 234 | if (child.getVisibility() == GONE) { 235 | continue; 236 | } else { 237 | i++; 238 | } 239 | 240 | LayoutParams childParams = child.getLayoutParams(); 241 | int marginLeft = 0, marginTop = 0, marginRight = 0; 242 | if (childParams instanceof MarginLayoutParams) { 243 | MarginLayoutParams marginParams = (MarginLayoutParams) childParams; 244 | marginLeft = marginParams.leftMargin; 245 | marginRight = marginParams.rightMargin; 246 | marginTop = marginParams.topMargin; 247 | } 248 | 249 | int childWidth = child.getMeasuredWidth(); 250 | int childHeight = child.getMeasuredHeight(); 251 | if (mRtl) { 252 | child.layout(x - marginRight - childWidth, y + marginTop, 253 | x - marginRight, y + marginTop + childHeight); 254 | x -= childWidth + spacing + marginLeft + marginRight; 255 | } else { 256 | child.layout(x + marginLeft, y + marginTop, 257 | x + marginLeft + childWidth, y + marginTop + childHeight); 258 | x += childWidth + spacing + marginLeft + marginRight; 259 | } 260 | } 261 | x = mRtl ? (getWidth() - paddingRight) : paddingLeft; 262 | y += rowHeight + mAdjustedRowSpacing; 263 | } 264 | } 265 | 266 | @Override 267 | protected LayoutParams generateLayoutParams(LayoutParams p) { 268 | return new MarginLayoutParams(p); 269 | } 270 | 271 | @Override 272 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 273 | return new MarginLayoutParams(getContext(), attrs); 274 | } 275 | 276 | /** 277 | * Returns whether to allow child views flow to next row when there is no enough space. 278 | * 279 | * @return Whether to flow child views to next row when there is no enough space. 280 | */ 281 | public boolean isFlow() { 282 | return mFlow; 283 | } 284 | 285 | /** 286 | * Sets whether to allow child views flow to next row when there is no enough space. 287 | * 288 | * @param flow true to allow flow. false to restrict all child views in one row. 289 | */ 290 | public void setFlow(boolean flow) { 291 | mFlow = flow; 292 | requestLayout(); 293 | } 294 | 295 | /** 296 | * Returns the horizontal spacing between child views. 297 | * 298 | * @return The spacing, either {@link FlowLayout#SPACING_AUTO}, or a fixed size in pixels. 299 | */ 300 | public int getChildSpacing() { 301 | return mChildSpacing; 302 | } 303 | 304 | /** 305 | * Sets the horizontal spacing between child views. 306 | * 307 | * @param childSpacing The spacing, either {@link FlowLayout#SPACING_AUTO}, or a fixed size in 308 | * pixels. 309 | */ 310 | public void setChildSpacing(int childSpacing) { 311 | mChildSpacing = childSpacing; 312 | requestLayout(); 313 | } 314 | 315 | /** 316 | * Returns the horizontal spacing between child views of the last row. 317 | * 318 | * @return The spacing, either {@link FlowLayout#SPACING_AUTO}, 319 | * {@link FlowLayout#SPACING_ALIGN}, or a fixed size in pixels 320 | */ 321 | public int getChildSpacingForLastRow() { 322 | return mChildSpacingForLastRow; 323 | } 324 | 325 | /** 326 | * Sets the horizontal spacing between child views of the last row. 327 | * 328 | * @param childSpacingForLastRow The spacing, either {@link FlowLayout#SPACING_AUTO}, 329 | * {@link FlowLayout#SPACING_ALIGN}, or a fixed size in pixels 330 | */ 331 | public void setChildSpacingForLastRow(int childSpacingForLastRow) { 332 | mChildSpacingForLastRow = childSpacingForLastRow; 333 | requestLayout(); 334 | } 335 | 336 | /** 337 | * Returns the vertical spacing between rows. 338 | * 339 | * @return The spacing, either {@link FlowLayout#SPACING_AUTO}, or a fixed size in pixels. 340 | */ 341 | public float getRowSpacing() { 342 | return mRowSpacing; 343 | } 344 | 345 | /** 346 | * Sets the vertical spacing between rows in pixels. Use SPACING_AUTO to evenly place all rows 347 | * in vertical. 348 | * 349 | * @param rowSpacing The spacing, either {@link FlowLayout#SPACING_AUTO}, or a fixed size in 350 | * pixels. 351 | */ 352 | public void setRowSpacing(float rowSpacing) { 353 | mRowSpacing = rowSpacing; 354 | requestLayout(); 355 | } 356 | 357 | /** 358 | * Returns the maximum number of rows of the FlowLayout. 359 | * 360 | * @return The maximum number of rows. 361 | */ 362 | public int getMaxRows() { 363 | return mMaxRows; 364 | } 365 | 366 | /** 367 | * Sets the number of row of the FlowLayout to be at most maxRows size. 368 | * 369 | * @param maxRows The maximum number of rows. 370 | */ 371 | public void setMaxRows(int maxRows) { 372 | mMaxRows = maxRows; 373 | requestLayout(); 374 | } 375 | 376 | private float getSpacingForRow(int spacingAttribute, int rowSize, int usedSize, int childNum) { 377 | float spacing; 378 | if (spacingAttribute == SPACING_AUTO) { 379 | if (childNum > 1) { 380 | spacing = (rowSize - usedSize) / (childNum - 1); 381 | } else { 382 | spacing = 0; 383 | } 384 | } else { 385 | spacing = spacingAttribute; 386 | } 387 | return spacing; 388 | } 389 | 390 | private float dpToPx(float dp) { 391 | return TypedValue.applyDimension( 392 | TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); 393 | } 394 | } -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/view/OnSkuListener.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.view; 2 | 3 | 4 | import com.madreain.sku.bean.Sku; 5 | import com.madreain.sku.bean.SkuAttribute; 6 | 7 | /** 8 | * @author madreain 9 | * @date 2019-07-27. 10 | * module: 11 | * description: 12 | */ 13 | public interface OnSkuListener { 14 | /** 15 | * 属性取消选中 16 | * 17 | * @param unselectedAttribute 18 | */ 19 | void onUnselected(SkuAttribute unselectedAttribute); 20 | 21 | /** 22 | * 属性选中 23 | * 24 | * @param selectAttribute 25 | */ 26 | void onSelect(SkuAttribute selectAttribute); 27 | 28 | /** 29 | * sku选中 30 | * 31 | * @param sku 32 | */ 33 | void onSkuSelected(Sku sku); 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/view/SkuItemLayout.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.text.TextPaint; 6 | import android.util.AttributeSet; 7 | import android.util.TypedValue; 8 | import android.view.View; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | 12 | 13 | import com.madreain.sku.R; 14 | import com.madreain.sku.bean.SkuAttribute; 15 | import com.madreain.sku.utils.DisplayUtil; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * @author madreain 21 | * @date 2019-07-27. 22 | * module: 23 | * description: 24 | */ 25 | 26 | public class SkuItemLayout extends LinearLayout { 27 | private TextView attributeNameTv; 28 | private FlowLayout attributeValueLayout; 29 | private OnSkuItemSelectListener listener; 30 | 31 | public SkuItemLayout(Context context) { 32 | super(context); 33 | init(context); 34 | } 35 | 36 | public SkuItemLayout(Context context, @Nullable AttributeSet attrs) { 37 | super(context, attrs); 38 | init(context); 39 | } 40 | 41 | public SkuItemLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 42 | super(context, attrs, defStyleAttr); 43 | init(context); 44 | } 45 | 46 | private void init(Context context) { 47 | setOrientation(VERTICAL); 48 | //类目的展示 49 | attributeNameTv = new TextView(context); 50 | attributeNameTv.setTextColor(context.getResources().getColor(R.color.m2F2F39)); 51 | attributeNameTv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); 52 | attributeNameTv.setIncludeFontPadding(false); 53 | //字体加粗 54 | TextPaint tp = attributeNameTv.getPaint(); 55 | tp.setFakeBoldText(true); 56 | LayoutParams attributeNameParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 57 | attributeNameParams.leftMargin = DisplayUtil.dp2px(context, 16); 58 | attributeNameParams.topMargin = DisplayUtil.dp2px(context, 24); 59 | attributeNameTv.setLayoutParams(attributeNameParams); 60 | addView(attributeNameTv); 61 | //sku属性展示 62 | attributeValueLayout = new FlowLayout(context); 63 | attributeValueLayout.setMinimumHeight(DisplayUtil.dp2px(context, 25)); 64 | attributeValueLayout.setChildSpacing(DisplayUtil.dp2px(context, 15)); 65 | attributeValueLayout.setRowSpacing(DisplayUtil.dp2px(context, 15)); 66 | LayoutParams attributeValueParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 67 | attributeValueParams.leftMargin = DisplayUtil.dp2px(context, 15); 68 | attributeValueParams.rightMargin = DisplayUtil.dp2px(context, 15); 69 | attributeValueParams.topMargin = DisplayUtil.dp2px(context, 15); 70 | attributeValueParams.bottomMargin = DisplayUtil.dp2px(context, 10); 71 | attributeValueLayout.setLayoutParams(attributeValueParams); 72 | addView(attributeValueLayout); 73 | //横线的展示 74 | View line = new View(context); 75 | line.setBackgroundResource(R.color.m202F2F39); 76 | LayoutParams lineParams = new LayoutParams(LayoutParams.MATCH_PARENT, 1); 77 | lineParams.leftMargin = DisplayUtil.dp2px(context, 15); 78 | lineParams.rightMargin = DisplayUtil.dp2px(context, 15); 79 | lineParams.topMargin = DisplayUtil.dp2px(context, 5); 80 | line.setLayoutParams(lineParams); 81 | addView(line); 82 | } 83 | 84 | /** 85 | * sku属性展示 86 | * @param position 87 | * @param attributeName 88 | * @param attributeValueList 89 | */ 90 | public void buildItemLayout(int position, String attributeName, List attributeValueList) { 91 | attributeNameTv.setText(attributeName); 92 | attributeValueLayout.removeAllViewsInLayout(); 93 | for (String attributeValue : attributeValueList) { 94 | SkuItemView itemView = new SkuItemView(getContext()); 95 | itemView.setAttributeValue(attributeValue); 96 | itemView.setOnClickListener(new ItemClickListener(position, itemView)); 97 | itemView.setLayoutParams(new FlowLayout.LayoutParams( 98 | FlowLayout.LayoutParams.WRAP_CONTENT, 99 | DisplayUtil.dp2px(getContext(), 36))); 100 | attributeValueLayout.addView(itemView); 101 | } 102 | } 103 | 104 | /** 105 | * 清空是否可点击,选中状态 106 | */ 107 | public void clearItemViewStatus() { 108 | int attributeValueLayoutChildCount = attributeValueLayout.getChildCount(); 109 | for (int i = 0; i < attributeValueLayoutChildCount; i++) { 110 | SkuItemView itemView = (SkuItemView) attributeValueLayout.getChildAt(i); 111 | itemView.setSelected(false); 112 | itemView.setEnabled(false); 113 | } 114 | } 115 | 116 | /** 117 | * 设置指定属性为可点击状态 118 | * 119 | * @param attributeValue 120 | */ 121 | public void optionItemViewEnableStatus(String attributeValue) { 122 | int attributeValueLayoutChildCount = attributeValueLayout.getChildCount(); 123 | for (int i = 0; i < attributeValueLayoutChildCount; i++) { 124 | SkuItemView itemView = (SkuItemView) attributeValueLayout.getChildAt(i); 125 | if (attributeValue.equals(itemView.getAttributeValue())) { 126 | itemView.setEnabled(true); 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * 设置指定属性为选中状态 133 | * 134 | * @param selectValue 135 | */ 136 | public void optionItemViewSelectStatus(SkuAttribute selectValue) { 137 | int attributeValueLayoutChildCount = attributeValueLayout.getChildCount(); 138 | for (int i = 0; i < attributeValueLayoutChildCount; i++) { 139 | SkuItemView itemView = (SkuItemView) attributeValueLayout.getChildAt(i); 140 | if (selectValue.getValue().equals(itemView.getAttributeValue())) { 141 | itemView.setEnabled(true); 142 | itemView.setSelected(true); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * 当前属性集合是否有选中项 149 | * 150 | * @return 151 | */ 152 | public boolean isSelected() { 153 | int attributeValueLayoutChildCount = attributeValueLayout.getChildCount(); 154 | for (int i = 0; i < attributeValueLayoutChildCount; i++) { 155 | SkuItemView itemView = (SkuItemView) attributeValueLayout.getChildAt(i); 156 | if (itemView.isSelected()) { 157 | return true; 158 | } 159 | } 160 | return false; 161 | } 162 | 163 | /** 164 | * 获取属性名称 165 | * 166 | * @return 167 | */ 168 | public String getAttributeName() { 169 | return attributeNameTv.getText().toString(); 170 | } 171 | 172 | /** 173 | * sku点击 174 | * @param position 175 | * @param view 176 | */ 177 | private void onSkuItemClicked(int position, SkuItemView view) { 178 | boolean selected = !view.isSelected(); 179 | SkuAttribute attribute = new SkuAttribute(); 180 | attribute.setKey(attributeNameTv.getText().toString()); 181 | attribute.setValue(view.getAttributeValue()); 182 | listener.onSelect(position, selected, attribute); 183 | } 184 | 185 | /** 186 | * 点击事件 187 | */ 188 | private class ItemClickListener implements OnClickListener { 189 | private int position; 190 | private SkuItemView view; 191 | 192 | ItemClickListener(int position, SkuItemView view) { 193 | this.position = position; 194 | this.view = view; 195 | } 196 | 197 | @Override 198 | public void onClick(View v) { 199 | onSkuItemClicked(position, view); 200 | } 201 | } 202 | 203 | public void setOnSkuItemSelectListener(OnSkuItemSelectListener listener) { 204 | this.listener = listener; 205 | } 206 | 207 | interface OnSkuItemSelectListener { 208 | void onSelect(int position, boolean select, SkuAttribute attribute); 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/view/SkuItemView.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.support.annotation.Nullable; 6 | import android.util.AttributeSet; 7 | import android.util.TypedValue; 8 | import android.view.Gravity; 9 | import android.widget.TextView; 10 | 11 | import com.madreain.sku.R; 12 | import com.madreain.sku.utils.DisplayUtil; 13 | 14 | 15 | /** 16 | * @author madreain 17 | * @date 2019-07-27. 18 | * module: 19 | * description: 20 | */ 21 | 22 | @SuppressLint("AppCompatCustomView") 23 | public class SkuItemView extends TextView { 24 | private String attributeValue; 25 | 26 | public SkuItemView(Context context) { 27 | super(context); 28 | init(context); 29 | } 30 | 31 | public SkuItemView(Context context, @Nullable AttributeSet attrs) { 32 | super(context, attrs); 33 | init(context); 34 | } 35 | 36 | public SkuItemView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 37 | super(context, attrs, defStyleAttr); 38 | init(context); 39 | } 40 | 41 | private void init(Context context) { 42 | setBackgroundResource(R.drawable.sku_item_selector); 43 | setTextColor(getResources().getColorStateList(R.color.sku_item_text_selector)); 44 | setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); 45 | setSingleLine(); 46 | setGravity(Gravity.CENTER); 47 | setPadding(DisplayUtil.dp2px(context, 10), 0, DisplayUtil.dp2px(context, 10), 0); 48 | setMinWidth(DisplayUtil.dp2px(context, 45)); 49 | setMaxWidth(DisplayUtil.dp2px(context, 200)); 50 | } 51 | 52 | public String getAttributeValue() { 53 | return attributeValue; 54 | } 55 | 56 | public void setAttributeValue(String attributeValue) { 57 | this.attributeValue = attributeValue; 58 | setText(attributeValue); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/view/SkuMaxHeightScrollView.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | import android.widget.ScrollView; 8 | 9 | import com.madreain.sku.R; 10 | import com.madreain.sku.utils.DisplayUtil; 11 | 12 | /** 13 | * @author madreain 14 | * @date 2019-07-27. 15 | * module: 16 | * description:解决Sku过多时,选择界面铺满全屏的问题 17 | */ 18 | public class SkuMaxHeightScrollView extends ScrollView { 19 | 20 | private int maxHeight; 21 | private int minHeight; 22 | 23 | public SkuMaxHeightScrollView(Context context) { 24 | this(context, null); 25 | } 26 | 27 | public SkuMaxHeightScrollView(Context context, AttributeSet attrs) { 28 | super(context, attrs); 29 | TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SkuMaxHeightScrollView, 0, 0); 30 | try { 31 | maxHeight = typedArray.getInt(R.styleable.SkuMaxHeightScrollView_maxSkuHeight, 220); 32 | minHeight = typedArray.getInt(R.styleable.SkuMaxHeightScrollView_minSkuHeight, 75); 33 | } finally { 34 | typedArray.recycle(); 35 | } 36 | } 37 | 38 | @Override 39 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 40 | int width = MeasureSpec.getSize(widthMeasureSpec); 41 | int height = 0; 42 | for (int i = 0; i < getChildCount(); i++) { 43 | View child = getChildAt(i); 44 | child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 45 | int h = child.getMeasuredHeight(); 46 | if (h > height) 47 | height = h; 48 | } 49 | float heightDp = DisplayUtil.px2dp(getContext(), height); 50 | int skumaxHeight = DisplayUtil.dp2px(getContext(), maxHeight); 51 | int skuminHeight = DisplayUtil.dp2px(getContext(), minHeight); 52 | if (heightDp > skumaxHeight) { 53 | setMeasuredDimension(width, skumaxHeight); 54 | } else if (heightDp < skuminHeight) { 55 | setMeasuredDimension(width, skuminHeight); 56 | } else { 57 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/madreain/sku/view/SkuSelectScrollView.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku.view; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.util.AttributeSet; 6 | import android.widget.FrameLayout; 7 | import android.widget.LinearLayout; 8 | 9 | 10 | import com.madreain.sku.bean.Sku; 11 | import com.madreain.sku.bean.SkuAttribute; 12 | 13 | import java.util.Iterator; 14 | import java.util.LinkedHashMap; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author madreain 21 | * @date 2019-07-27. 22 | * module: 23 | * description: 24 | */ 25 | public class SkuSelectScrollView extends SkuMaxHeightScrollView implements SkuItemLayout.OnSkuItemSelectListener { 26 | private LinearLayout skuContainerLayout; 27 | private List skuList; 28 | private List selectedAttributeList; // 存放当前属性选中信息 29 | private OnSkuListener listener; // sku选中状态回调接口 30 | 31 | public SkuSelectScrollView(Context context) { 32 | super(context); 33 | init(context, null); 34 | } 35 | 36 | public SkuSelectScrollView(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | init(context, attrs); 39 | } 40 | 41 | private void init(Context context, AttributeSet attrs) { 42 | setFillViewport(true); 43 | setOverScrollMode(OVER_SCROLL_NEVER); 44 | skuContainerLayout = new LinearLayout(context, attrs); 45 | skuContainerLayout.setOrientation(LinearLayout.VERTICAL); 46 | skuContainerLayout.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); 47 | addView(skuContainerLayout); 48 | } 49 | 50 | /** 51 | * 设置监听接口 52 | * 53 | * @param listener {@link OnSkuListener} 54 | */ 55 | public void setOnSkuListener(OnSkuListener listener) { 56 | this.listener = listener; 57 | } 58 | 59 | /** 60 | * 绑定sku数据 61 | * 62 | * @param skuList 63 | */ 64 | public void setSkuList(List skuList) { 65 | this.skuList = skuList; 66 | // 清空sku视图 67 | skuContainerLayout.removeAllViews(); 68 | 69 | // 获取分组的sku集合 70 | Map> dataMap = getSkuGroupByName(skuList); 71 | selectedAttributeList = new LinkedList<>(); 72 | int index = 0; 73 | for (Iterator>> it = dataMap.entrySet().iterator(); it.hasNext(); ) { 74 | Map.Entry> entry = it.next(); 75 | 76 | // 构建sku视图 77 | SkuItemLayout itemLayout = new SkuItemLayout(getContext()); 78 | itemLayout.setLayoutParams(new LinearLayout.LayoutParams( 79 | LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT 80 | )); 81 | itemLayout.buildItemLayout(index++, entry.getKey(), entry.getValue()); 82 | itemLayout.setOnSkuItemSelectListener(this); 83 | skuContainerLayout.addView(itemLayout); 84 | // 初始状态下,所有属性信息设置为空 85 | selectedAttributeList.add(new SkuAttribute(entry.getKey(), "")); 86 | } 87 | // 一个sku时,默认选中 88 | if (skuList.size() == 1) { 89 | selectedAttributeList.clear(); 90 | for (SkuAttribute attribute : this.skuList.get(0).getAttributes()) { 91 | selectedAttributeList.add(new SkuAttribute(attribute.getKey(), attribute.getValue())); 92 | } 93 | } 94 | // 清除所有选中状态 95 | clearAllLayoutStatus(); 96 | // 设置是否可点击 97 | optionLayoutEnableStatus(); 98 | // 设置选中状态 99 | optionLayoutSelectStatus(); 100 | } 101 | 102 | /** 103 | * 将sku根据属性名进行分组 104 | * 105 | * @param list 106 | * @return 如{ "颜色": {"白色", "红色", "黑色"}, "尺寸": {"XS","S","M", "L", "XL", "XXL"}} 107 | */ 108 | private Map> getSkuGroupByName(List list) { 109 | Map> dataMap = new LinkedHashMap<>(); 110 | for (Sku sku : list) { 111 | for (SkuAttribute attribute : sku.getAttributes()) { 112 | String attributeName = attribute.getKey(); 113 | String attributeValue = attribute.getValue(); 114 | if (!dataMap.containsKey(attributeName)) { 115 | dataMap.put(attributeName, new LinkedList()); 116 | } 117 | List valueList = dataMap.get(attributeName); 118 | if (!valueList.contains(attributeValue)) { 119 | dataMap.get(attributeName).add(attributeValue); 120 | } 121 | } 122 | } 123 | return dataMap; 124 | } 125 | 126 | /** 127 | * 重置所有属性的选中状态 128 | */ 129 | private void clearAllLayoutStatus() { 130 | int skuContainerLayoutChildCount = skuContainerLayout.getChildCount(); 131 | for (int i = 0; i < skuContainerLayoutChildCount; i++) { 132 | SkuItemLayout itemLayout = (SkuItemLayout) skuContainerLayout.getChildAt(i); 133 | itemLayout.clearItemViewStatus(); 134 | } 135 | } 136 | 137 | /** 138 | * 设置所有属性的Enable状态,即是否可点击 139 | */ 140 | private void optionLayoutEnableStatus() { 141 | int childCount = skuContainerLayout.getChildCount(); 142 | if (childCount <= 1) { 143 | optionLayoutEnableStatusSingleProperty(); 144 | } else { 145 | optionLayoutEnableStatusMultipleProperties(); 146 | } 147 | } 148 | 149 | private void optionLayoutEnableStatusSingleProperty() { 150 | SkuItemLayout itemLayout = (SkuItemLayout) skuContainerLayout.getChildAt(0); 151 | // 遍历sku列表 152 | for (Sku sku : skuList) { 153 | // 属性值是否可点击flag 154 | List attributeBeanList = sku.getAttributes(); 155 | //库存判断 156 | if (sku.getStockQuantity() > 0) { 157 | String attributeValue = attributeBeanList.get(0).getValue(); 158 | itemLayout.optionItemViewEnableStatus(attributeValue); 159 | } 160 | } 161 | } 162 | 163 | private void optionLayoutEnableStatusMultipleProperties() { 164 | int skuContainerLayoutSize = skuContainerLayout.getChildCount(); 165 | for (int i = 0; i < skuContainerLayoutSize; i++) { 166 | SkuItemLayout itemLayout = (SkuItemLayout) skuContainerLayout.getChildAt(i); 167 | // 遍历sku列表 168 | int skuListSize = skuList.size(); 169 | for (int j = 0; j < skuListSize; j++) { 170 | // 属性值是否可点击flag 171 | boolean flag = false; 172 | Sku sku = skuList.get(j); 173 | List attributeBeanList = sku.getAttributes(); 174 | // 遍历选中信息列表 175 | int selectedAttributeListSize = selectedAttributeList.size(); 176 | for (int k = 0; k < selectedAttributeListSize; k++) { 177 | // i = k,跳过当前属性,避免多次设置是否可点击 178 | if (i == k) continue; 179 | // 选中信息为空,则说明未选中,无法判断是否有不可点击的情形,跳过 180 | if ("".equals(selectedAttributeList.get(k).getValue())) continue; 181 | // 选中信息列表中不包含当前sku的属性,则sku组合不存在,设置为不可点击 182 | // 库存为0,设置为不可点击 183 | if (!selectedAttributeList.get(k).getValue().equals(attributeBeanList.get(k).getValue()) 184 | //库存没有 185 | || sku.getStockQuantity() == 0 186 | ) { 187 | flag = true; 188 | break; 189 | } 190 | } 191 | // flag 为false时,可点击 192 | if (!flag) { 193 | String attributeValue = attributeBeanList.get(i).getValue(); 194 | itemLayout.optionItemViewEnableStatus(attributeValue); 195 | } 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * 设置所有属性的选中状态 202 | */ 203 | private void optionLayoutSelectStatus() { 204 | int skuContainerLayoutSize = skuContainerLayout.getChildCount(); 205 | for (int i = 0; i < skuContainerLayoutSize; i++) { 206 | SkuItemLayout itemLayout = (SkuItemLayout) skuContainerLayout.getChildAt(i); 207 | itemLayout.optionItemViewSelectStatus(selectedAttributeList.get(i)); 208 | } 209 | } 210 | 211 | /** 212 | * 是否有sku选中 213 | * 214 | * @return 215 | */ 216 | private boolean isSkuSelected() { 217 | for (SkuAttribute attribute : selectedAttributeList) { 218 | if (TextUtils.isEmpty(attribute.getValue())) { 219 | return false; 220 | } 221 | } 222 | return true; 223 | } 224 | 225 | /** 226 | * 获取第一个未选中的属性名 227 | * 228 | * @return 229 | */ 230 | public String getFirstUnelectedAttributeName() { 231 | int skuContainerLayoutSize = skuContainerLayout.getChildCount(); 232 | for (int i = 0; i < skuContainerLayoutSize; i++) { 233 | SkuItemLayout itemLayout = (SkuItemLayout) skuContainerLayout.getChildAt(i); 234 | if (!itemLayout.isSelected()) { 235 | return itemLayout.getAttributeName(); 236 | } 237 | } 238 | return ""; 239 | } 240 | 241 | /** 242 | * 获取选中的Sku 243 | * 244 | * @return 245 | */ 246 | public Sku getSelectedSku() { 247 | // 判断是否有选中的sku 248 | if (!isSkuSelected()) { 249 | return null; 250 | } 251 | for (Sku sku : skuList) { 252 | List attributeList = sku.getAttributes(); 253 | // 将sku的属性列表与selectedAttributeList匹配,完全匹配则为已选中sku 254 | boolean flag = true; 255 | int attributeListSize = attributeList.size(); 256 | for (int i = 0; i < attributeListSize; i++) { 257 | if (!isSameSkuAttribute(attributeList.get(i), selectedAttributeList.get(i))) { 258 | flag = false; 259 | } 260 | } 261 | if (flag) { 262 | return sku; 263 | } 264 | } 265 | return null; 266 | } 267 | 268 | /** 269 | * 设置选中的sku 270 | * 271 | * @param sku 272 | */ 273 | public void setSelectedSku(Sku sku) { 274 | selectedAttributeList.clear(); 275 | for (SkuAttribute attribute : sku.getAttributes()) { 276 | selectedAttributeList.add(new SkuAttribute(attribute.getKey(), attribute.getValue())); 277 | } 278 | // 清除所有选中状态 279 | clearAllLayoutStatus(); 280 | // 设置是否可点击 281 | optionLayoutEnableStatus(); 282 | // 设置选中状态 283 | optionLayoutSelectStatus(); 284 | } 285 | 286 | /** 287 | * 是否为同一个SkuAttribute 288 | * 289 | * @param previousAttribute 290 | * @param nextAttribute 291 | * @return 292 | */ 293 | private boolean isSameSkuAttribute(SkuAttribute previousAttribute, SkuAttribute nextAttribute) { 294 | return previousAttribute.getKey().equals(nextAttribute.getKey()) 295 | && previousAttribute.getValue().equals(nextAttribute.getValue()); 296 | } 297 | 298 | @Override 299 | public void onSelect(int position, boolean selected, SkuAttribute attribute) { 300 | if (selected) { 301 | // 选中,保存选中信息 302 | selectedAttributeList.set(position, attribute); 303 | } else { 304 | // 取消选中,清空保存的选中信息 305 | selectedAttributeList.get(position).setValue(""); 306 | } 307 | clearAllLayoutStatus(); 308 | // 设置是否可点击 309 | optionLayoutEnableStatus(); 310 | // 设置选中状态 311 | optionLayoutSelectStatus(); 312 | // 回调接口 313 | if (isSkuSelected()) { 314 | listener.onSkuSelected(getSelectedSku()); 315 | } else if (selected) { 316 | listener.onSelect(attribute); 317 | } else { 318 | listener.onUnselected(attribute); 319 | } 320 | } 321 | 322 | /** 323 | * 获取当前选择的属性信息 324 | * 325 | * @return 326 | */ 327 | public List getSelectedAttributeList() { 328 | return selectedAttributeList; 329 | } 330 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_to_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/color/quantity_operator_text_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/sku_item_text_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_mfe5002.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sku_item_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sku_quantity_input_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sku_quantity_minus_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sku_quantity_plus_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_sku.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 24 | 25 | 28 | 29 | 30 | 39 | 40 | 41 | 54 | 55 | 56 | 66 | 67 | 68 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 99 | 100 | 104 | 105 | 116 | 117 | 128 | 129 | 139 | 140 | 151 | 152 | 171 | 172 | 184 | 185 | 186 | 187 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/comm_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xhdpi/comm_close.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/comm_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xxhdpi/comm_close.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | #2f2f39 7 | #332F2F39 8 | #0A2F2F39 9 | #FE5002 10 | #66FE5002 11 | #14FE5002 12 | #bbbbbb 13 | #666666 14 | #333333 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 1dp 5 | 152dp 6 | 120dp 7 | 16dp 8 | 24dp 9 | 8dp 10 | 20sp 11 | 14sp 12 | 16sp 13 | 40dp 14 | 6dp 15 | 26dp 16 | 10dp 17 | 12sp 18 | 18sp 19 | 28dp 20 | 2dp 21 | 100dp 22 | 21dp 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SKU 3 | ¥ %s 4 | 确认购买 5 | 购买数量 6 | (库存:%1$s) 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/test/java/com/madreain/sku/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.madreain.sku; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.4.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /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 | 15 | 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /sku/sku.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madreain/SKU/f01420e51194f4778cb7b0808b6487b857e9367a/sku/sku.gif --------------------------------------------------------------------------------