11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # android-pile-layout
2 | An abnormal horizontal ListView-like pile layout.
3 |
4 | ### captured images
5 | The following pictures were captured earlier. Since the source code and the outputted-apk have changed some params, you will have a different UI when you directly run the code or install the apk file. Hope there has no confusions later.
6 |
7 |
8 | ### design
9 | Recently I have seen this kind of UI design, and at the first sight I was trying to implement it by using RecyclerView's LayoutManager. Unfortunately, I am unable to contrust a clear Math model while sliding the PileView. After several tries, I gave up LayoutManager, and find another way for this implementation. If you make LayoutManager works well for this UI design, please tell me later.
10 |
11 | ### how to use
12 | 1. declare PileLayout in your xml file
13 | ```xml
14 |
24 | ```
25 | Meanwhile, pileLayout is able to be customized by these 4 params:
26 |
27 | |name|format|description|
28 | |:---:|:---:|:---:|
29 | | interval | dimension |items-margin each other
30 | | sizeRatio | float |each item's height/witdth
31 | | scaleStep | float |size scale step when needed
32 | | displayCount | float |number of items that may display
33 |
34 | 2. in Java files:
35 | ```java
36 | pileLayout = (PileLayout) findViewById(R.id.pileLayout);
37 | pileLayout.setAdapter(new PileLayout.Adapter() {
38 | @Override
39 | public int getLayoutId() {
40 | // item's layout resource id
41 | return R.layout.item_layout;
42 | }
43 |
44 | @Override
45 | public void bindView(View view, int position) {
46 | ViewHolder viewHolder = (ViewHolder) view.getTag();
47 | if (viewHolder == null) {
48 | viewHolder = new ViewHolder();
49 | viewHolder.imageView = (ImageView) view.findViewById(R.id.imageView);
50 | view.setTag(viewHolder);
51 | }
52 | // recycled view bind new position
53 | }
54 |
55 | @Override
56 | public int getItemCount() {
57 | // item count
58 | return dataList.size();
59 | }
60 |
61 | @Override
62 | public void displaying(int position) {
63 | // right displaying the left biggest itemView's position
64 | }
65 |
66 | @Override
67 | public void onItemClick(View view, int position) {
68 | // on item click
69 | }
70 | });
71 | ```
72 |
73 | ### demo apk
74 | [download](capture/app-debug.apk)
75 |
76 | ## License
77 |
78 | Copyright 2017, xmuSistone
79 |
80 | Licensed under the Apache License, Version 2.0 (the "License");
81 | you may not use this file except in compliance with the License.
82 | You may obtain a copy of the License at
83 |
84 | http://www.apache.org/licenses/LICENSE-2.0
85 |
86 | Unless required by applicable law or agreed to in writing, software
87 | distributed under the License is distributed on an "AS IS" BASIS,
88 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
89 | See the License for the specific language governing permissions and
90 | limitations under the License.
91 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion "25.0.2"
6 | defaultConfig {
7 | applicationId "com.stone.pile"
8 | minSdkVersion 14
9 | targetSdkVersion 25
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(include: ['*.jar'], dir: 'libs')
24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
25 | exclude group: 'com.android.support', module: 'support-annotations'
26 | })
27 | compile 'com.android.support:appcompat-v7:25.3.1'
28 | testCompile 'junit:junit:4.12'
29 | compile 'com.github.bumptech.glide:glide:3.8.0'
30 | compile 'com.makeramen:roundedimageview:2.3.0'
31 | compile project(':library')
32 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\adt-bundle-windows-x86-20130917\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/stone/pile/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile;
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 | * Instrumentation 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() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.stone.pile", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/assets/preset.config:
--------------------------------------------------------------------------------
1 | {
2 | "code": 200,
3 | "msg": "ok",
4 | "result": [
5 | {
6 | "country": "Maldives",
7 | "temperature": "25-39℃",
8 | "coverImageUrl": "http://img.hb.aicdn.com/10dd7b6eb9ca02a55e915a068924058e72f7b3353a40d-ZkO3ko_fw658",
9 | "address": "Arabian Sea, Indian Ocean",
10 | "description": "Visitors to the Maldives do not need to apply for a visa pre-arrival, regardless of their country of origin, provided they have a valid passport, proof of onward travel, and the money to be self-sufficient while in the country.",
11 | "time": "6.10~3.31 8:00-20:00",
12 | "mapImageUrl": "http://img.hb.aicdn.com/3f04db36f22e2bf56d252a3bc1eacdd2a0416d75221a7c-rpihP1_fw658"
13 | },
14 | {
15 | "country": "USA",
16 | "temperature": "16-29℃",
17 | "coverImageUrl": "http://img.hb.aicdn.com/a3a995b26bd7d58ccc164eafc6ab902601984728a3101-S2H0lQ_fw658",
18 | "address": "Niagara Falls",
19 | "description": "The cities of Niagara Falls, Ontario, Canada, and Niagara Falls, New York, United States, are connected by two international bridges. The Rainbow Bridge, just downriver from the falls, affords the closest view.",
20 | "time": "3.25~10.21 9:00-17:00",
21 | "mapImageUrl": "http://img.hb.aicdn.com/09302f8c939c76bb920af0aa8ea0a7ea8ae286b8a6de-1Teqka_fw658"
22 | },
23 | {
24 | "country": "China",
25 | "temperature": "18-37℃",
26 | "coverImageUrl": "http://pic4.nipic.com/20091124/3789537_153149003980_2.jpg",
27 | "address": "Hunan Zhangjiajie",
28 | "description": "Zhangjiajie is a prefecture-level city in the northwestern part of Hunan province, People's Republic of China. It comprises the district of Yongding and counties of Cili and Sangzhi.We made all sail,and bore up for Hawaii. The beauty of the scenery bedazed all of us.",
29 | "time": "4.1~4.31 7:00-18:00",
30 | "mapImageUrl": "http://img.hb.aicdn.com/5342e50c6b6876b7cfde7dcdc3f89b290997624f37c56-qoOZli_fw658"
31 | },
32 | {
33 | "country": "Australia",
34 | "temperature": "18-31℃",
35 | "coverImageUrl": "http://img.hb.aicdn.com/4ba573e93c6fe178db6730ba05f0176466056dbe14905-ly0Z43_fw658",
36 | "address": "Fraser Island",
37 | "description": "Fraser Island is a heritage-listed island located along the south-eastern coast of the state of Queensland, Australia. It is approximately 250 kilometres (160 mi) north of the state capital, Brisbane.",
38 | "time": "5.21~8.15 6:00-24:00",
39 | "mapImageUrl": "http://img.hb.aicdn.com/9a592a3bb4235eb2a9efe2061b7962b91fda51da146a9b-9MnyBd_fw658"
40 | },
41 | {
42 | "country": "China",
43 | "temperature": "12-26℃",
44 | "coverImageUrl": "http://img.hb.aicdn.com/4bc60d00aa3184f1f98e418df6fb6abc447dc814226ef-ZtS8hB_fw658",
45 | "address": "Sichuan Jiuzhaigou Valley",
46 | "description": "Jiuzhaigou Valley is part of the Min Mountains on the edge of the Tibetan Plateau and stretches over 72,000 hectares. It is known for its many multi-level waterfalls, colorful lakes, and snow-capped peaks.",
47 | "time": "6.10~11.22 9:00-19:00",
48 | "mapImageUrl": "http://img.hb.aicdn.com/04e84f35444738307f4acf872138daca92e4c4bb2abe0-KB7srY_fw658"
49 | },
50 | {
51 | "country": "New Zealand",
52 | "temperature": "12-33℃",
53 | "coverImageUrl": "http://img.hb.aicdn.com/d9a48c272914c5253eceac26c51a56a26f4e50d048ba7-IJsbou_fw658",
54 | "address": "South Pacific Ocean",
55 | "description": "New Zealand has a strong presence among the Pacific Island countries. A large proportion of New Zealand's aid goes to these countries and many Pacific people migrate to New Zealand for employment.",
56 | "time": "5.9~12.11 5:00-22:00",
57 | "mapImageUrl": "http://img.hb.aicdn.com/bfd4cae9a22214d0c8ff812c72df072f5192081a4bc7f-a8bMYr_fw658"
58 | },
59 | {
60 | "country": "Fiji",
61 | "temperature": "18-39℃",
62 | "coverImageUrl": "http://img.hb.aicdn.com/03d474bbe20efb7df9aed4541ace70b53b53c70bdfe3-8djYVv_fw658",
63 | "address": "Koro Sea",
64 | "description": "For a country of its size, Fiji has fairly large armed forces, and has been a major contributor to UN peacekeeping missions in various parts of the world.For a country of its size, Fiji has fairly large armed forces, and has been a major contributor to UN peacekeeping missions in various parts of the world.",
65 | "time": "6.1~12.12 6:00-21:00",
66 | "mapImageUrl": "http://img.hb.aicdn.com/92cd31abfec5cc1701066f7a77fbeebc730e39b150e61-X6vafO_fw658"
67 | },
68 | {
69 | "country": "England",
70 | "temperature": "8-23℃",
71 | "coverImageUrl": "http://img.hb.aicdn.com/004cddd40519846281526b4b25fbdea36b31d01e190dd-7zlmuG_fw658",
72 | "address": "The White Cliffs of Dover",
73 | "description": "The White Cliffs of Dover are cliffs that form part of the English coastline facing the Strait of Dover and France. The cliffs are part of the North Downs formation.",
74 | "time": "6.5~12.12 9:00-14:00",
75 | "mapImageUrl": "http://img.hb.aicdn.com/de1eb2f982c640ca0581bb2ccb3f620340757ba527ffca-DqKBOn_fw658"
76 | },
77 | {
78 | "country": "Indonesia",
79 | "temperature": "21-39℃",
80 | "coverImageUrl": "http://img.hb.aicdn.com/a58eda8a9a2a3f30f0a694c2702e1aba71d97d616d34f-rqv6FA_fw658",
81 | "address": "Bali Island",
82 | "description": "The tourism industry is primarily focused in the south, while significant in the other parts of the island as well.",
83 | "time": "8.10~12.29 7:00-23:00",
84 | "mapImageUrl": "http://img.hb.aicdn.com/434b325a54214319d0cc37ac090f3eaf78db8a4c1f6cf-n7Lxkv_fw658"
85 | },
86 | {
87 | "country": "Norway",
88 | "temperature": "6-18℃",
89 | "coverImageUrl": "http://img.hb.aicdn.com/41ff5110b4ecdec24e14f767e83c1659c2e8a180f3df-QqUAgk_fw658",
90 | "address": "Scandinavia",
91 | "description": "Norway's territory comprises the western portion of the Scandinavian Peninsula plus the island Jan Mayen and the archipelago of Svalbard.",
92 | "time": "9.12~11.11 6:00-15:00",
93 | "mapImageUrl": "http://img.hb.aicdn.com/3c515a7e4fac799d7f62bf6be045d8a287cb16f214b91-Y8N5wB_fw658"
94 | },
95 | {
96 | "country": "USA",
97 | "temperature": "0-11℃",
98 | "coverImageUrl": "http://img.hb.aicdn.com/80006ed344ed8dee7ad8142b3c4dc1b51cbf207c3097a-SGiu5P_fw658",
99 | "address": "Alaska",
100 | "description": "Cities not served by road, sea, or river can be reached only by air, foot, dogsled, or snowmachine, accounting for Alaska's extremely well developed bush air services—an Alaskan novelty.",
101 | "time": "7.22~11.18 9:00-14:00",
102 | "mapImageUrl": "http://img.hb.aicdn.com/8341ed2db4c5c386e20eddb33c7c7369ace3ed6c1ddfc-Ur1z3Y_fw658"
103 | },
104 | {
105 | "country": "Brazil",
106 | "temperature": "26-39℃",
107 | "coverImageUrl": "http://img.hb.aicdn.com/ade29e312dc44eb63aa6fd258cabcb4a217d88fb4959e-9qPoRi_fw658",
108 | "address": "Amazon River",
109 | "description": "Many branches begin flooding in November and may continue to rise until June. The rise of the Rio Negro starts in February or March and begins to recede in June.",
110 | "time": "9.9~11.4 14:00-18:00",
111 | "mapImageUrl": "http://img.hb.aicdn.com/2a81f586b936bb570c88d93681ea9ef089235772a5a8a-KVhjzP_fw658"
112 | },
113 | {
114 | "country": "Malaysia",
115 | "temperature": "17-36℃",
116 | "coverImageUrl": "http://img.hb.aicdn.com/d812c602d55419c278d826591d4996e744c629d6fb61-01BfFo_fw658",
117 | "address": "Redang Island",
118 | "description": "In contrast to the neighbouring Perhentian Islands, Redang has a more upmarket image, as almost all accommodation on the island is resort-based.",
119 | "time": "6.26~10.24 15:00-23:00",
120 | "mapImageUrl": "http://img.hb.aicdn.com/226ce6243ae187c107e1a22cfa7cc0763fe2bda9908c4-8foxjv_fw658"
121 | },
122 | {
123 | "country": "Philippine",
124 | "temperature": "21-39℃",
125 | "coverImageUrl": "http://img.hb.aicdn.com/1ce7724403f4613566d9d51ad5ead3a58f93220521c3f-fNA43p_fw658",
126 | "address": "Boracay Island",
127 | "description": "White Beach, the main tourism beach, is about four kilometers long and is lined with resorts, hotels, lodging houses, restaurants, and other tourism-related businesses.",
128 | "time": "2.1~12.21 10:00-20:00",
129 | "mapImageUrl": "http://img.hb.aicdn.com/cc9b7be6e08eaaea49a1bf45c7253fc498c166c525de1-pyRJON_fw658"
130 | },
131 | {
132 | "country": "Japan",
133 | "temperature": "13-30℃",
134 | "coverImageUrl": "http://img.hb.aicdn.com/e7d7f513744141140f0a34eb3f24540c96ea2b6050ad8-E44srp_fw658",
135 | "address": "Hokkaido",
136 | "description": "Unlike the other major islands of Japan, Hokkaido is normally not affected by the June–July rainy season and the relative lack of humidity and typically warm, rather than hot, summer weather makes its climate an attraction for tourists from other parts of Japan.",
137 | "time": "3.1~6.31 5:00-23:00",
138 | "mapImageUrl": "http://attachments.gfan.com/forum/attachments2/day_110422/1104222243bffe391469c3bee4.jpg"
139 | },
140 | {
141 | "country": "Russia",
142 | "temperature": "0-6℃",
143 | "coverImageUrl": "http://img.hb.aicdn.com/04beba3c6f48abc7d8cea6304f8673cfc3de8637322d2-WDz41G_fw658",
144 | "address": "Lake Baikal",
145 | "description": "Lake Baikal is the largest freshwater lake by volume in the world, and it is home to thousands of species of plants and animals, many of which exist nowhere else in the world.",
146 | "time": "6.21~9.5 6:00-18:00",
147 | "mapImageUrl": "http://img.hb.aicdn.com/607ea895fc7604744afb00e44caf14e711f764e9301c8-mpanyM_fw658"
148 | }
149 | ]
150 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/activity/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.activity;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ObjectAnimator;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.support.v7.app.AppCompatActivity;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.view.WindowManager;
11 | import android.widget.ImageView;
12 | import android.widget.TextView;
13 |
14 | import com.bumptech.glide.Glide;
15 | import com.stone.pile.R;
16 | import com.stone.pile.entity.ItemEntity;
17 | import com.stone.pile.libs.PileLayout;
18 | import com.stone.pile.util.Utils;
19 | import com.stone.pile.widget.FadeTransitionImageView;
20 | import com.stone.pile.widget.HorizontalTransitionLayout;
21 | import com.stone.pile.widget.VerticalTransitionLayout;
22 |
23 | import org.json.JSONArray;
24 | import org.json.JSONObject;
25 |
26 | import java.io.InputStream;
27 | import java.util.ArrayList;
28 | import java.util.List;
29 |
30 | /**
31 | * Created by xmuSistone on 2017/5/12.
32 | */
33 | public class MainActivity extends AppCompatActivity {
34 |
35 | private View positionView;
36 | private PileLayout pileLayout;
37 | private List dataList;
38 |
39 | private int lastDisplay = -1;
40 |
41 | private ObjectAnimator transitionAnimator;
42 | private float transitionValue;
43 | private HorizontalTransitionLayout countryView, temperatureView;
44 | private VerticalTransitionLayout addressView, timeView;
45 | private FadeTransitionImageView bottomView;
46 | private Animator.AnimatorListener animatorListener;
47 | private TextView descriptionView;
48 |
49 | @Override
50 | protected void onCreate(Bundle savedInstanceState) {
51 | super.onCreate(savedInstanceState);
52 | setContentView(R.layout.activity_main);
53 |
54 | positionView = findViewById(R.id.positionView);
55 | countryView = (HorizontalTransitionLayout) findViewById(R.id.countryView);
56 | temperatureView = (HorizontalTransitionLayout) findViewById(R.id.temperatureView);
57 | pileLayout = (PileLayout) findViewById(R.id.pileLayout);
58 | addressView = (VerticalTransitionLayout) findViewById(R.id.addressView);
59 | descriptionView = (TextView) findViewById(R.id.descriptionView);
60 | timeView = (VerticalTransitionLayout) findViewById(R.id.timeView);
61 | bottomView = (FadeTransitionImageView) findViewById(R.id.bottomImageView);
62 |
63 | // 1. 状态栏侵入
64 | boolean adjustStatusHeight = false;
65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
66 | adjustStatusHeight = true;
67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
68 | getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
69 | } else {
70 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
71 | }
72 | }
73 |
74 | // 2. 状态栏占位View的高度调整
75 | String brand = Build.BRAND;
76 | if (brand.contains("Xiaomi")) {
77 | Utils.setXiaomiDarkMode(this);
78 | } else if (brand.contains("Meizu")) {
79 | Utils.setMeizuDarkMode(this);
80 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
81 | View decor = getWindow().getDecorView();
82 | decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
83 | adjustStatusHeight = false;
84 | }
85 | if (adjustStatusHeight) {
86 | adjustStatusBarHeight(); // 调整状态栏高度
87 | }
88 |
89 | animatorListener = new Animator.AnimatorListener() {
90 | @Override
91 | public void onAnimationStart(Animator animation) {
92 |
93 | }
94 |
95 | @Override
96 | public void onAnimationEnd(Animator animation) {
97 | countryView.onAnimationEnd();
98 | temperatureView.onAnimationEnd();
99 | addressView.onAnimationEnd();
100 | bottomView.onAnimationEnd();
101 | timeView.onAnimationEnd();
102 | }
103 |
104 | @Override
105 | public void onAnimationCancel(Animator animation) {
106 |
107 | }
108 |
109 | @Override
110 | public void onAnimationRepeat(Animator animation) {
111 |
112 | }
113 | };
114 |
115 |
116 | // 3. PileLayout绑定Adapter
117 | initDataList();
118 | pileLayout.setAdapter(new PileLayout.Adapter() {
119 | @Override
120 | public int getLayoutId() {
121 | return R.layout.item_layout;
122 | }
123 |
124 | @Override
125 | public void bindView(View view, int position) {
126 | ViewHolder viewHolder = (ViewHolder) view.getTag();
127 | if (viewHolder == null) {
128 | viewHolder = new ViewHolder();
129 | viewHolder.imageView = (ImageView) view.findViewById(R.id.imageView);
130 | view.setTag(viewHolder);
131 | }
132 |
133 | Glide.with(MainActivity.this).load(dataList.get(position).getCoverImageUrl()).into(viewHolder.imageView);
134 | }
135 |
136 | @Override
137 | public int getItemCount() {
138 | return dataList.size();
139 | }
140 |
141 | @Override
142 | public void displaying(int position) {
143 | descriptionView.setText(dataList.get(position).getDescription() + " Since the world is so beautiful, You have to believe me, and this index is " + position);
144 | if (lastDisplay < 0) {
145 | initSecene(position);
146 | lastDisplay = 0;
147 | } else if (lastDisplay != position) {
148 | transitionSecene(position);
149 | lastDisplay = position;
150 | }
151 | }
152 |
153 | @Override
154 | public void onItemClick(View view, int position) {
155 | super.onItemClick(view, position);
156 | }
157 | });
158 | }
159 |
160 | private void initSecene(int position) {
161 | countryView.firstInit(dataList.get(position).getCountry());
162 | temperatureView.firstInit(dataList.get(position).getTemperature());
163 | addressView.firstInit(dataList.get(position).getAddress());
164 | bottomView.firstInit(dataList.get(position).getMapImageUrl());
165 | timeView.firstInit(dataList.get(position).getTime());
166 | }
167 |
168 | private void transitionSecene(int position) {
169 | if (transitionAnimator != null) {
170 | transitionAnimator.cancel();
171 | }
172 |
173 | countryView.saveNextPosition(position, dataList.get(position).getCountry() + "-" + position);
174 | temperatureView.saveNextPosition(position, dataList.get(position).getTemperature());
175 | addressView.saveNextPosition(position, dataList.get(position).getAddress());
176 | bottomView.saveNextPosition(position, dataList.get(position).getMapImageUrl());
177 | timeView.saveNextPosition(position, dataList.get(position).getTime());
178 |
179 | transitionAnimator = ObjectAnimator.ofFloat(this, "transitionValue", 0.0f, 1.0f);
180 | transitionAnimator.setDuration(300);
181 | transitionAnimator.start();
182 | transitionAnimator.addListener(animatorListener);
183 |
184 | }
185 |
186 | /**
187 | * 调整沉浸状态栏
188 | */
189 | private void adjustStatusBarHeight() {
190 | int statusBarHeight = Utils.getStatusBarHeight(this);
191 | ViewGroup.LayoutParams lp = positionView.getLayoutParams();
192 | lp.height = statusBarHeight;
193 | positionView.setLayoutParams(lp);
194 | }
195 |
196 |
197 | /**
198 | * 从asset读取文件json数据
199 | */
200 | private void initDataList() {
201 | dataList = new ArrayList<>();
202 | try {
203 | InputStream in = getAssets().open("preset.config");
204 | int size = in.available();
205 | byte[] buffer = new byte[size];
206 | in.read(buffer);
207 | String jsonStr = new String(buffer, "UTF-8");
208 | JSONObject jsonObject = new JSONObject(jsonStr);
209 | JSONArray jsonArray = jsonObject.optJSONArray("result");
210 | if (null != jsonArray) {
211 | int len = jsonArray.length();
212 | for (int j = 0; j < 3; j++) {
213 | for (int i = 0; i < len; i++) {
214 | JSONObject itemJsonObject = jsonArray.getJSONObject(i);
215 | ItemEntity itemEntity = new ItemEntity(itemJsonObject);
216 | dataList.add(itemEntity);
217 | }
218 | }
219 | }
220 | } catch (Exception e) {
221 | e.printStackTrace();
222 | }
223 | }
224 |
225 | /**
226 | * 属性动画
227 | */
228 | public void setTransitionValue(float transitionValue) {
229 | this.transitionValue = transitionValue;
230 | countryView.duringAnimation(transitionValue);
231 | temperatureView.duringAnimation(transitionValue);
232 | addressView.duringAnimation(transitionValue);
233 | bottomView.duringAnimation(transitionValue);
234 | timeView.duringAnimation(transitionValue);
235 | }
236 |
237 | public float getTransitionValue() {
238 | return transitionValue;
239 | }
240 |
241 | class ViewHolder {
242 | ImageView imageView;
243 | }
244 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/entity/ItemEntity.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.entity;
2 |
3 | import org.json.JSONObject;
4 |
5 | /**
6 | * Created by xmuSistone on 2017/5/12.
7 | */
8 |
9 | public class ItemEntity {
10 |
11 | private String country;
12 | private String temperature;
13 | private String coverImageUrl;
14 | private String address;
15 | private String description;
16 | private String time;
17 | private String mapImageUrl;
18 |
19 | public ItemEntity(JSONObject jsonObject) {
20 | this.country = jsonObject.optString("country");
21 | this.temperature = jsonObject.optString("temperature");
22 | this.coverImageUrl = jsonObject.optString("coverImageUrl");
23 | this.address = jsonObject.optString("address");
24 | this.description = jsonObject.optString("description");
25 | this.time = jsonObject.optString("time");
26 | this.mapImageUrl = jsonObject.optString("mapImageUrl");
27 | }
28 |
29 | public String getCountry() {
30 | return country;
31 | }
32 |
33 | public void setCountry(String country) {
34 | this.country = country;
35 | }
36 |
37 | public String getTemperature() {
38 | return temperature;
39 | }
40 |
41 | public void setTemperature(String temperature) {
42 | this.temperature = temperature;
43 | }
44 |
45 | public String getCoverImageUrl() {
46 | return coverImageUrl;
47 | }
48 |
49 | public void setCoverImageUrl(String coverImageUrl) {
50 | this.coverImageUrl = coverImageUrl;
51 | }
52 |
53 | public String getAddress() {
54 | return address;
55 | }
56 |
57 | public void setAddress(String address) {
58 | this.address = address;
59 | }
60 |
61 | public String getDescription() {
62 | return description;
63 | }
64 |
65 | public void setDescription(String description) {
66 | this.description = description;
67 | }
68 |
69 | public String getTime() {
70 | return time;
71 | }
72 |
73 | public void setTime(String time) {
74 | this.time = time;
75 | }
76 |
77 | public String getMapImageUrl() {
78 | return mapImageUrl;
79 | }
80 |
81 | public void setMapImageUrl(String mapImageUrl) {
82 | this.mapImageUrl = mapImageUrl;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/util/TransitionHelper.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.util;
2 |
3 | /**
4 | * Created by admin on 2017/5/12.
5 | */
6 |
7 | public class TransitionHelper {
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/util/Utils.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.util;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.view.Window;
6 | import android.view.WindowManager;
7 |
8 | import java.lang.reflect.Field;
9 | import java.lang.reflect.Method;
10 |
11 | /**
12 | * Created by xmuSistone on 2017/5/12.
13 | */
14 |
15 | public class Utils {
16 |
17 | /**
18 | * 获取状态栏高度
19 | */
20 | public static int getStatusBarHeight(Context context) {
21 | Class> c = null;
22 | Object obj = null;
23 | Field field = null;
24 | int x = 0, statusBarHeight = 0;
25 | try {
26 | c = Class.forName("com.android.internal.R$dimen");
27 | obj = c.newInstance();
28 | field = c.getField("status_bar_height");
29 | x = Integer.parseInt(field.get(obj).toString());
30 | statusBarHeight = context.getResources().getDimensionPixelSize(x);
31 | } catch (Exception e1) {
32 | e1.printStackTrace();
33 | }
34 | return statusBarHeight;
35 | }
36 |
37 | /**
38 | * 小米手机设置darkMode
39 | */
40 | public static boolean setXiaomiDarkMode(Activity activity) {
41 | Class extends Window> clazz = activity.getWindow().getClass();
42 | try {
43 | int darkModeFlag = 0;
44 | Class> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
45 | Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
46 | darkModeFlag = field.getInt(layoutParams);
47 | Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
48 | extraFlagField.invoke(activity.getWindow(), darkModeFlag, darkModeFlag);
49 | return true;
50 | } catch (Exception e) {
51 | e.printStackTrace();
52 | }
53 | return false;
54 | }
55 |
56 | /**
57 | * 魅族手机设置darkMode
58 | */
59 | public static boolean setMeizuDarkMode(Activity activity) {
60 | boolean result = false;
61 | try {
62 | WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
63 | Field darkFlag = WindowManager.LayoutParams.class
64 | .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
65 | Field meizuFlags = WindowManager.LayoutParams.class
66 | .getDeclaredField("meizuFlags");
67 | darkFlag.setAccessible(true);
68 | meizuFlags.setAccessible(true);
69 | int bit = darkFlag.getInt(null);
70 | int value = meizuFlags.getInt(lp);
71 | value |= bit;
72 | meizuFlags.setInt(lp, value);
73 | activity.getWindow().setAttributes(lp);
74 | result = true;
75 | } catch (Exception e) {
76 | }
77 | return result;
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/widget/BaseTransitionLayout.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.widget;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.widget.FrameLayout;
6 |
7 | /**
8 | * Created by xmuSistone on 2017/5/12.
9 | */
10 |
11 | public abstract class BaseTransitionLayout extends FrameLayout {
12 |
13 | public BaseTransitionLayout(Context context) {
14 | this(context, null);
15 | }
16 |
17 | public BaseTransitionLayout(Context context, AttributeSet attrs) {
18 | this(context, attrs, 0);
19 | }
20 |
21 | public BaseTransitionLayout(Context context, AttributeSet attrs, int defStyleAttr) {
22 | super(context, attrs, defStyleAttr);
23 | }
24 |
25 | @Override
26 | protected void onFinishInflate() {
27 | super.onFinishInflate();
28 | addViewWhenFinishInflate();
29 | }
30 |
31 | public abstract void addViewWhenFinishInflate();
32 |
33 | public abstract void firstInit(String info);
34 |
35 | public abstract void onAnimationEnd();
36 |
37 | public abstract void duringAnimation(float rate);
38 |
39 | public abstract void saveNextPosition(int position, String info);
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/widget/FadeTransitionImageView.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.widget;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.util.AttributeSet;
6 | import android.view.ViewGroup;
7 | import android.widget.ImageView;
8 |
9 | import com.bumptech.glide.Glide;
10 | import com.stone.pile.R;
11 |
12 | /**
13 | * Created by xmuSistone on 2017/5/12.
14 | */
15 |
16 | public class FadeTransitionImageView extends BaseTransitionLayout {
17 |
18 | private ImageView imageView1, imageView2;
19 |
20 | protected int currentPosition = -1;
21 | protected int nextPosition = -1;
22 |
23 | public FadeTransitionImageView(Context context) {
24 | this(context, null);
25 | }
26 |
27 | public FadeTransitionImageView(Context context, AttributeSet attrs) {
28 | this(context, attrs, 0);
29 | }
30 |
31 | public FadeTransitionImageView(Context context, AttributeSet attrs, int defStyleAttr) {
32 | super(context, attrs, defStyleAttr);
33 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.scene);
34 | a.recycle();
35 | }
36 |
37 | @Override
38 | public void addViewWhenFinishInflate() {
39 | imageView1 = new ImageView(getContext());
40 | ViewGroup.LayoutParams lp1 = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
41 | imageView1.setScaleType(ImageView.ScaleType.CENTER_CROP);
42 | addView(imageView1, lp1);
43 |
44 | imageView2 = new ImageView(getContext());
45 | LayoutParams lp2 = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
46 | imageView2.setScaleType(ImageView.ScaleType.CENTER_CROP);
47 | addView(imageView2, lp2);
48 | }
49 |
50 |
51 | @Override
52 | public void firstInit(String url) {
53 | Glide.with(getContext()).load(url).into(imageView1);
54 | currentPosition = 0;
55 | }
56 |
57 | @Override
58 | public void onAnimationEnd() {
59 | currentPosition = nextPosition;
60 | ImageView tmp = imageView1;
61 | imageView1 = imageView2;
62 | imageView2 = tmp;
63 | }
64 |
65 | /**
66 | * rate从零到1
67 | */
68 | @Override
69 | public void duringAnimation(float rate) {
70 | imageView1.setAlpha(1 - rate);
71 | imageView2.setAlpha(rate);
72 | }
73 |
74 | @Override
75 | public void saveNextPosition(int position, String url) {
76 | this.nextPosition = position;
77 | Glide.with(getContext()).load(url).into(imageView2);
78 | }
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/widget/HorizontalTransitionLayout.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.widget;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Color;
6 | import android.util.AttributeSet;
7 | import android.view.Gravity;
8 | import android.widget.FrameLayout;
9 | import android.widget.TextView;
10 |
11 | import com.stone.pile.R;
12 |
13 | /**
14 | * Created by xmuSistone on 2017/5/12.
15 | */
16 | public class HorizontalTransitionLayout extends BaseTransitionLayout {
17 |
18 | private TextView textView1, textView2;
19 |
20 | protected int currentPosition = -1;
21 | protected int nextPosition = -1;
22 |
23 | private int leftMargin = 50;
24 | private float textSize = 22;
25 | private int textColor = Color.BLACK;
26 | private int leftDistance = 50;
27 | private int rightDistance = 450;
28 |
29 | public HorizontalTransitionLayout(Context context) {
30 | this(context, null);
31 | }
32 |
33 | public HorizontalTransitionLayout(Context context, AttributeSet attrs) {
34 | this(context, attrs, 0);
35 | }
36 |
37 | public HorizontalTransitionLayout(Context context, AttributeSet attrs, int defStyleAttr) {
38 | super(context, attrs, defStyleAttr);
39 |
40 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.scene);
41 | leftMargin = a.getDimensionPixelSize(R.styleable.scene_leftMargin, leftMargin);
42 | textSize = a.getFloat(R.styleable.scene_textSize, textSize);
43 | textColor = a.getColor(R.styleable.scene_textColor, textColor);
44 | leftDistance = a.getDimensionPixelSize(R.styleable.scene_leftDistance, leftDistance);
45 | rightDistance = a.getDimensionPixelSize(R.styleable.scene_rightDistance, rightDistance);
46 | a.recycle();
47 | }
48 |
49 | @Override
50 | public void addViewWhenFinishInflate() {
51 | textView1 = new TextView(getContext());
52 | textView1.setGravity(Gravity.CENTER_VERTICAL);
53 | textView1.setTextSize(textSize);
54 | textView1.setTextColor(textColor);
55 | FrameLayout.LayoutParams lp1 = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
56 | lp1.setMargins(leftMargin, 0, 0, 0);
57 | addView(textView1, lp1);
58 |
59 | textView2 = new TextView(getContext());
60 | textView2.setGravity(Gravity.CENTER_VERTICAL);
61 | textView2.setTextSize(textSize);
62 | textView2.setTextColor(textColor);
63 | FrameLayout.LayoutParams lp2 = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
64 | lp2.setMargins(leftMargin, 0, 0, 0);
65 | addView(textView2, lp2);
66 | }
67 |
68 | @Override
69 | public void firstInit(String text) {
70 | this.textView1.setText(text);
71 | currentPosition = 0;
72 | }
73 |
74 | @Override
75 | public void onAnimationEnd() {
76 | currentPosition = nextPosition;
77 | TextView tmp = textView1;
78 | textView1 = textView2;
79 | textView2 = tmp;
80 | }
81 |
82 | /**
83 | * rate从零到1
84 | */
85 | @Override
86 | public void duringAnimation(float rate) {
87 | textView1.setAlpha(1 - rate);
88 | textView2.setAlpha(rate);
89 |
90 | if (nextPosition > currentPosition) {
91 | textView1.offsetLeftAndRight((int) (leftMargin - leftDistance * rate - textView1.getLeft()));
92 | textView2.offsetLeftAndRight((int) (leftMargin + rightDistance * (1 - rate) - textView2.getLeft()));
93 | } else {
94 | textView1.offsetLeftAndRight((int) (leftMargin + rightDistance * rate - textView1.getLeft()));
95 | textView2.offsetLeftAndRight((int) (leftMargin * rate - textView2.getLeft()));
96 | }
97 | }
98 |
99 | @Override
100 | public void saveNextPosition(int position, String text) {
101 | this.nextPosition = position;
102 | this.textView2.setText(text);
103 | this.textView2.setAlpha(0);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stone/pile/widget/VerticalTransitionLayout.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.widget;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Color;
6 | import android.util.AttributeSet;
7 | import android.util.Log;
8 | import android.view.Gravity;
9 | import android.widget.FrameLayout;
10 | import android.widget.TextView;
11 |
12 | import com.stone.pile.R;
13 |
14 | /**
15 | * Created by xmuSistone on 2017/5/12.
16 | */
17 | public class VerticalTransitionLayout extends BaseTransitionLayout {
18 |
19 | private TextView textView1, textView2;
20 |
21 | protected int currentPosition = -1;
22 | protected int nextPosition = -1;
23 |
24 | private float textSize = 22;
25 | private int textColor = Color.BLACK;
26 | private int verticalDistance = 50;
27 |
28 | public VerticalTransitionLayout(Context context) {
29 | this(context, null);
30 | }
31 |
32 | public VerticalTransitionLayout(Context context, AttributeSet attrs) {
33 | this(context, attrs, 0);
34 | }
35 |
36 | public VerticalTransitionLayout(Context context, AttributeSet attrs, int defStyleAttr) {
37 | super(context, attrs, defStyleAttr);
38 |
39 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.scene);
40 | textSize = a.getFloat(R.styleable.scene_textSize, textSize);
41 | textColor = a.getColor(R.styleable.scene_textColor, textColor);
42 | verticalDistance = a.getDimensionPixelSize(R.styleable.scene_verticalDistance, verticalDistance);
43 | a.recycle();
44 | }
45 |
46 | @Override
47 | public void addViewWhenFinishInflate() {
48 | textView1 = new TextView(getContext());
49 | textView1.setGravity(Gravity.CENTER_VERTICAL);
50 | textView1.setTextSize(textSize);
51 | textView1.setTextColor(textColor);
52 | LayoutParams lp1 = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
53 | addView(textView1, lp1);
54 |
55 | textView2 = new TextView(getContext());
56 | textView2.setGravity(Gravity.CENTER_VERTICAL);
57 | textView2.setTextSize(textSize);
58 | textView2.setTextColor(textColor);
59 | LayoutParams lp2 = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
60 | addView(textView2, lp2);
61 | }
62 |
63 | @Override
64 | public void firstInit(String text) {
65 | this.textView1.setText(text);
66 | currentPosition = 0;
67 | }
68 |
69 | @Override
70 | public void onAnimationEnd() {
71 | currentPosition = nextPosition;
72 | TextView tmp = textView1;
73 | textView1 = textView2;
74 | textView2 = tmp;
75 | }
76 |
77 | /**
78 | * rate从零到1
79 | */
80 | @Override
81 | public void duringAnimation(float rate) {
82 | textView1.setAlpha(1 - rate);
83 | textView2.setAlpha(rate);
84 |
85 | if (nextPosition > currentPosition) {
86 | textView1.offsetTopAndBottom((int) (0 - verticalDistance * rate - textView1.getTop()));
87 | textView2.offsetTopAndBottom((int) (0 + verticalDistance * (1 - rate) - textView2.getTop()));
88 | } else {
89 | textView1.offsetTopAndBottom((int) (0 + verticalDistance * rate - textView1.getTop()));
90 | textView2.offsetTopAndBottom((int) (0 - verticalDistance * (1 - rate) - textView2.getTop()));
91 | }
92 | }
93 |
94 | @Override
95 | public void saveNextPosition(int position, String text) {
96 | this.nextPosition = position;
97 | this.textView2.setText(text);
98 | this.textView2.setAlpha(0);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/drawable/clock.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/default_back.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/drawable/play.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/drawable/right.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/drawable/star.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/white_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
20 |
21 |
31 |
32 |
33 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
65 |
66 |
72 |
73 |
77 |
78 |
86 |
87 |
88 |
89 |
101 |
102 |
107 |
108 |
111 |
112 |
116 |
117 |
121 |
122 |
123 |
130 |
131 |
135 |
136 |
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
15 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v23/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ffffff
4 | #dddddd
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PileLayout
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/stone/pile/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile;
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() throws Exception {
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 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/capture/app-debug.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/capture/app-debug.apk
--------------------------------------------------------------------------------
/capture/capture1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/capture/capture1.gif
--------------------------------------------------------------------------------
/capture/capture2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/capture/capture2.gif
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xmuSistone/AndroidPileLayout/76434cad0af3a12ca4ce18e54117c3091374425f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion "25.0.2"
6 |
7 | defaultConfig {
8 | minSdkVersion 14
9 | targetSdkVersion 25
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 |
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | }
23 |
24 | dependencies {
25 | compile fileTree(dir: 'libs', include: ['*.jar'])
26 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
27 | exclude group: 'com.android.support', module: 'support-annotations'
28 | })
29 | compile 'com.android.support:appcompat-v7:25.3.1'
30 | testCompile 'junit:junit:4.12'
31 | }
32 |
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\adt-bundle-windows-x86-20130917\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/library/src/androidTest/java/com/stone/pile/libs/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.libs;
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 | * Instrumentation 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() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.stone.pile.libs.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/java/com/stone/pile/libs/PileLayout.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.libs;
2 |
3 | import android.animation.ObjectAnimator;
4 | import android.content.Context;
5 | import android.content.res.TypedArray;
6 | import android.util.AttributeSet;
7 | import android.view.LayoutInflater;
8 | import android.view.MotionEvent;
9 | import android.view.VelocityTracker;
10 | import android.view.View;
11 | import android.view.ViewConfiguration;
12 | import android.view.ViewGroup;
13 | import android.view.ViewTreeObserver;
14 | import android.view.animation.DecelerateInterpolator;
15 | import android.view.animation.Interpolator;
16 | import android.widget.FrameLayout;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | /**
22 | * Created by xmuSistone on 2017/5/12.
23 | */
24 |
25 | public class PileLayout extends ViewGroup {
26 |
27 | private final int mMaximumVelocity;
28 | private OnClickListener onClickListener;
29 |
30 | // 以下三个参数,可通过属性定制
31 | private int interval = 30; // view之间的间隔
32 | private float sizeRatio = 1.1f;
33 | private float scaleStep = 0.36f;
34 |
35 | private int everyWidth;
36 | private int everyHeight;
37 | private int scrollDistanceMax; // 滑动参考值
38 | private List originX = new ArrayList<>(); // 存放的是最初的七个View的位置
39 |
40 | // 拖拽相关
41 | private static final int MODE_IDLE = 0;
42 | private static final int MODE_HORIZONTAL = 1;
43 | private static final int MODE_VERTICAL = 2;
44 | private static final int VELOCITY_THRESHOLD = 200;
45 | private int scrollMode;
46 | private int downX, downY;
47 | private float lastX;
48 | private final int mTouchSlop; // 判定为滑动的阈值,单位是像素
49 |
50 | private float animateValue;
51 | private ObjectAnimator animator;
52 | private Interpolator interpolator = new DecelerateInterpolator(1.6f);
53 | private Adapter adapter;
54 | private boolean hasSetAdapter = false;
55 | private float displayCount = 1.6f;
56 | private FrameLayout animatingView;
57 | private VelocityTracker mVelocityTracker;
58 |
59 | public PileLayout(Context context) {
60 | this(context, null);
61 | }
62 |
63 | public PileLayout(Context context, AttributeSet attrs) {
64 | this(context, attrs, 0);
65 | }
66 |
67 | public PileLayout(Context context, AttributeSet attrs, int defStyleAttr) {
68 | super(context, attrs, defStyleAttr);
69 |
70 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.pile);
71 | interval = (int) a.getDimension(R.styleable.pile_interval, interval);
72 | sizeRatio = a.getFloat(R.styleable.pile_sizeRatio, sizeRatio);
73 | scaleStep = a.getFloat(R.styleable.pile_scaleStep, scaleStep);
74 | displayCount = a.getFloat(R.styleable.pile_displayCount, displayCount);
75 | a.recycle();
76 |
77 | ViewConfiguration configuration = ViewConfiguration.get(getContext());
78 | mTouchSlop = configuration.getScaledTouchSlop();
79 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
80 |
81 | onClickListener = new OnClickListener() {
82 | @Override
83 | public void onClick(View v) {
84 | if (null != adapter) {
85 | int position = Integer.parseInt(v.getTag().toString());
86 | if (position >= 0 && position < adapter.getItemCount()) {
87 | adapter.onItemClick(((FrameLayout) v).getChildAt(0), position);
88 | }
89 | }
90 | }
91 | };
92 |
93 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
94 | @Override
95 | public void onGlobalLayout() {
96 | if (getHeight() > 0 && null != adapter && !hasSetAdapter) {
97 | setAdapter(adapter);
98 | }
99 | }
100 | });
101 | }
102 |
103 | @Override
104 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
105 | int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
106 | everyWidth = (int) ((width - getPaddingLeft() - getPaddingRight() - interval * 8) / displayCount);
107 | everyHeight = (int) (everyWidth * sizeRatio);
108 | setMeasuredDimension(width, (int) (everyHeight * (1 + scaleStep) + getPaddingTop() + getPaddingBottom()));
109 |
110 | // 把每个View的初始位置坐标都计算好
111 | if (originX.size() == 0) {
112 | int position0 = 0;
113 | originX.add(position0);
114 |
115 | int position1 = interval;
116 | originX.add(position1);
117 |
118 | int position2 = interval * 2;
119 | originX.add(position2);
120 |
121 | int position3 = interval * 3;
122 | originX.add(position3);
123 |
124 | int position4 = (int) (position3 + everyWidth * (1 + scaleStep) + interval);
125 | originX.add(position4);
126 |
127 | int position5 = position4 + everyWidth + interval;
128 | originX.add(position5);
129 |
130 | int position6 = position5 + everyWidth + interval;
131 | originX.add(position6);
132 |
133 | int position7 = position6 + everyWidth + interval;
134 | originX.add(position7);
135 |
136 | int position8 = position7 + everyWidth + interval;
137 | originX.add(position8);
138 |
139 | scrollDistanceMax = position4 - position3;
140 | }
141 | }
142 |
143 | @Override
144 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
145 | int num = getChildCount();
146 | for (int i = 0; i < num; i++) {
147 | View itemView = getChildAt(i);
148 | int left = originX.get(i);
149 | int top = (getMeasuredHeight() - everyHeight) / 2;
150 | int right = left + everyWidth;
151 | int bottom = top + everyHeight;
152 | itemView.layout(left, top, right, bottom);
153 | itemView.setPivotX(0);
154 | itemView.setPivotY(everyHeight / 2);
155 | adjustScale(itemView);
156 | adjustAlpha(itemView);
157 | }
158 | }
159 |
160 | /**
161 | * 根据X坐标位置调整ImageView的透明度
162 | *
163 | * @param itemView 需要调整的imageView
164 | */
165 | private void adjustAlpha(View itemView) {
166 | int position2 = originX.get(2);
167 | if (itemView.getLeft() >= position2) {
168 | itemView.setAlpha(1);
169 | } else {
170 | int position0 = originX.get(0);
171 | float alpha = (float) (itemView.getLeft()) / (position2 - position0);
172 | itemView.setAlpha(alpha);
173 | }
174 | }
175 |
176 | private void adjustScale(View itemView) {
177 | float scale = 1.0f;
178 | int position4 = originX.get(4);
179 | int thisLeft = itemView.getLeft();
180 | if (thisLeft < position4) {
181 | int position3 = originX.get(3);
182 | if (thisLeft > position3) {
183 | scale = 1 + scaleStep - scaleStep * (thisLeft - position3) / (position4 - position3);
184 | } else {
185 | int position2 = originX.get(2);
186 | scale = 1 + (thisLeft - position2) * scaleStep / interval;
187 | }
188 | }
189 | itemView.setScaleX(scale);
190 | itemView.setScaleY(scale);
191 | }
192 |
193 | @Override
194 | public boolean onInterceptTouchEvent(MotionEvent event) {
195 | // 决策是否需要拦截
196 | int action = event.getActionMasked();
197 | switch (action) {
198 | case MotionEvent.ACTION_DOWN:
199 | downX = (int) event.getX();
200 | downY = (int) event.getY();
201 | lastX = event.getX();
202 | scrollMode = MODE_IDLE;
203 | if (null != animator) {
204 | animator.cancel();
205 | }
206 |
207 | initVelocityTrackerIfNotExists();
208 | mVelocityTracker.addMovement(event);
209 | animatingView = null;
210 |
211 | break;
212 |
213 | case MotionEvent.ACTION_MOVE:
214 | if (scrollMode == MODE_IDLE) {
215 | float xDistance = Math.abs(downX - event.getX());
216 | float yDistance = Math.abs(downY - event.getY());
217 | if (xDistance > yDistance && xDistance > mTouchSlop) {
218 | // 水平滑动,需要拦截
219 | scrollMode = MODE_HORIZONTAL;
220 | return true;
221 | } else if (yDistance > xDistance && yDistance > mTouchSlop) {
222 | // 垂直滑动
223 | scrollMode = MODE_VERTICAL;
224 | }
225 | }
226 | break;
227 |
228 | case MotionEvent.ACTION_CANCEL:
229 | case MotionEvent.ACTION_UP:
230 | recycleVelocityTracker();
231 | // ACTION_UP还能拦截,说明手指没滑动,只是一个click事件,同样需要snap到特定位置
232 | onRelease(event.getX(), 0);
233 | break;
234 | }
235 | return false; // 默认都是不拦截的
236 | }
237 |
238 | @Override
239 | public boolean onTouchEvent(MotionEvent event) {
240 | mVelocityTracker.addMovement(event);
241 | int action = event.getActionMasked();
242 | switch (action) {
243 | case MotionEvent.ACTION_DOWN:
244 | // 此处说明底层没有子View愿意消费Touch事件
245 | break;
246 |
247 | case MotionEvent.ACTION_MOVE:
248 | int currentX = (int) event.getX();
249 | int dx = (int) (currentX - lastX);
250 | requireScrollChange(dx);
251 | lastX = currentX;
252 | break;
253 |
254 | case MotionEvent.ACTION_UP:
255 | case MotionEvent.ACTION_CANCEL:
256 | final VelocityTracker velocityTracker = mVelocityTracker;
257 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
258 | int velocity = (int) velocityTracker.getXVelocity();
259 | recycleVelocityTracker();
260 |
261 | onRelease(event.getX(), velocity);
262 | break;
263 | }
264 | return true;
265 | }
266 |
267 | private void onRelease(float eventX, int velocityX) {
268 | animatingView = (FrameLayout) getChildAt(3);
269 | animateValue = animatingView.getLeft();
270 | int tag = Integer.parseInt(animatingView.getTag().toString());
271 |
272 | // 计算目标位置
273 | int destX = originX.get(3);
274 | if (velocityX > VELOCITY_THRESHOLD || (animatingView.getLeft() > originX.get(3) + scrollDistanceMax / 2 && velocityX > -VELOCITY_THRESHOLD)) {
275 | destX = originX.get(4);
276 | tag--;
277 | }
278 | if (tag < 0 || tag >= adapter.getItemCount()) {
279 | return;
280 | }
281 |
282 | if (Math.abs(animatingView.getLeft() - destX) < mTouchSlop && Math.abs(eventX - downX) < mTouchSlop) {
283 | return;
284 | }
285 |
286 | adapter.displaying(tag);
287 | animator = ObjectAnimator.ofFloat(this, "animateValue", animatingView.getLeft(), destX);
288 | animator.setInterpolator(interpolator);
289 | animator.setDuration(360).start();
290 | }
291 |
292 | private void requireScrollChange(int dx) {
293 | if (dx == 0) {
294 | return;
295 | }
296 |
297 | int currentPosition = Integer.parseInt(getChildAt(3).getTag().toString());
298 | if (dx < 0 && currentPosition >= adapter.getItemCount()) {
299 | return;
300 | } else if (dx > 0) {
301 | if (currentPosition <= 0) {
302 | return;
303 | } else if (currentPosition == 1) {
304 | if (getChildAt(3).getLeft() + dx >= originX.get(4)) {
305 | dx = originX.get(4) - getChildAt(3).getLeft();
306 | }
307 | }
308 | }
309 |
310 |
311 | int num = getChildCount();
312 |
313 | // 1. View循环复用
314 | FrameLayout firstView = (FrameLayout) getChildAt(0);
315 | if (dx > 0 && firstView.getLeft() >= originX.get(1)) {
316 | // 向右滑动,从左边把View补上
317 | FrameLayout lastView = (FrameLayout) getChildAt(getChildCount() - 1);
318 |
319 | LayoutParams lp = lastView.getLayoutParams();
320 | removeViewInLayout(lastView);
321 | addViewInLayout(lastView, 0, lp);
322 |
323 | int tag = Integer.parseInt(lastView.getTag().toString());
324 | tag -= num;
325 | lastView.setTag(tag);
326 | if (tag < 0) {
327 | lastView.setVisibility(View.INVISIBLE);
328 | } else {
329 | lastView.setVisibility(View.VISIBLE);
330 | adapter.bindView(lastView.getChildAt(0), tag);
331 | }
332 | } else if (dx < 0 && firstView.getLeft() <= originX.get(0)) {
333 | // 向左滑动,从右边把View补上
334 | LayoutParams lp = firstView.getLayoutParams();
335 | removeViewInLayout(firstView);
336 | addViewInLayout(firstView, -1, lp);
337 |
338 | int tag = Integer.parseInt(firstView.getTag().toString());
339 | tag += num;
340 | firstView.setTag(tag);
341 | if (tag >= adapter.getItemCount()) {
342 | firstView.setVisibility(View.INVISIBLE);
343 | } else {
344 | firstView.setVisibility(View.VISIBLE);
345 | adapter.bindView(firstView.getChildAt(0), tag);
346 | }
347 | }
348 |
349 | // 2. 位置修正
350 | View view3 = getChildAt(3);
351 | float rate = (float) ((view3.getLeft() + dx) - originX.get(3)) / scrollDistanceMax;
352 | if (rate < 0) {
353 | rate = 0;
354 | }
355 | int position1 = Math.round(rate * (originX.get(2) - originX.get(1))) + originX.get(1);
356 | boolean endAnim = false;
357 | if (position1 >= originX.get(2) && null != animatingView) {
358 | animator.cancel();
359 | endAnim = true;
360 | }
361 | for (int i = 0; i < num; i++) {
362 | View itemView = getChildAt(i);
363 | if (endAnim) {
364 | itemView.offsetLeftAndRight(originX.get(i + 1) - itemView.getLeft());
365 | } else if (itemView == animatingView) {
366 | itemView.offsetLeftAndRight(dx);
367 | } else {
368 | int position = Math.round(rate * (originX.get(i + 1) - originX.get(i))) + originX.get(i);
369 | if (i + 1 < originX.size() && position >= originX.get(i + 1)) {
370 | position = originX.get(i + 1);
371 | }
372 | itemView.offsetLeftAndRight(position - itemView.getLeft());
373 | }
374 | adjustAlpha(itemView); // 调整透明度
375 | adjustScale(itemView); // 调整缩放
376 | }
377 | }
378 |
379 | /**
380 | * 绑定Adapter
381 | */
382 | public void setAdapter(Adapter adapter) {
383 | this.adapter = adapter;
384 |
385 | // ViewdoBindAdapter尚未渲染出来的时候,不做适配
386 | if (everyWidth > 0 && everyHeight > 0) {
387 | doBindAdapter();
388 | }
389 | }
390 |
391 |
392 | /**
393 | * 真正绑定Adapter
394 | */
395 | private void doBindAdapter() {
396 | if (adapter == null) {
397 | return;
398 | }
399 | if (hasSetAdapter) {
400 | throw new RuntimeException("PileLayout can only hold one adapter.");
401 | }
402 | hasSetAdapter = true;
403 | if (getChildCount() == 0) {
404 | LayoutInflater inflater = LayoutInflater.from(getContext());
405 | for (int i = 0; i < 6; i++) {
406 | FrameLayout frameLayout = new FrameLayout(getContext());
407 | View view = inflater.inflate(adapter.getLayoutId(), null);
408 | FrameLayout.LayoutParams lp1 = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
409 | lp1.width = everyWidth;
410 | lp1.height = everyHeight;
411 | frameLayout.addView(view, lp1);
412 | LayoutParams lp2 = new LayoutParams(everyWidth, everyHeight);
413 | lp2.width = everyWidth;
414 | lp2.height = everyHeight;
415 | frameLayout.setLayoutParams(lp2);
416 | frameLayout.setOnClickListener(onClickListener);
417 | addView(frameLayout);
418 | frameLayout.setTag(i - 3); // 这个tag主要是对应于在dataList中的数据index
419 | frameLayout.measure(everyWidth, everyHeight);
420 | }
421 | }
422 |
423 | int num = getChildCount();
424 | for (int i = 0; i < num; i++) {
425 | if (i < 3) {
426 | getChildAt(i).setVisibility(View.INVISIBLE);
427 | } else {
428 | FrameLayout frameLayout = (FrameLayout) getChildAt(i);
429 | if (i - 3 < adapter.getItemCount()) {
430 | frameLayout.setVisibility(View.VISIBLE);
431 | adapter.bindView(frameLayout.getChildAt(0), i - 3);
432 | } else {
433 | frameLayout.setVisibility(View.INVISIBLE);
434 | }
435 | }
436 | }
437 |
438 | if (adapter.getItemCount() > 0) {
439 | adapter.displaying(0);
440 | }
441 | }
442 |
443 | /**
444 | * 数据更新通知
445 | */
446 | public void notifyDataSetChanged() {
447 | int num = getChildCount();
448 | for (int i = 0; i < num; i++) {
449 | FrameLayout frameLayout = (FrameLayout) getChildAt(i);
450 | int tag = Integer.parseInt(frameLayout.getTag().toString());
451 | if (tag > 0 && tag < adapter.getItemCount()) {
452 | frameLayout.setVisibility(View.VISIBLE);
453 | adapter.bindView(frameLayout.getChildAt(0), tag);
454 | } else {
455 | frameLayout.setVisibility(View.INVISIBLE);
456 | }
457 |
458 | if (i == 3 && tag == 0) {
459 | adapter.displaying(0);
460 | }
461 | }
462 | }
463 |
464 | private void initVelocityTrackerIfNotExists() {
465 | if (mVelocityTracker == null) {
466 | mVelocityTracker = VelocityTracker.obtain();
467 | }
468 | }
469 |
470 | private void recycleVelocityTracker() {
471 | if (mVelocityTracker != null) {
472 | mVelocityTracker.recycle();
473 | mVelocityTracker = null;
474 | }
475 | }
476 |
477 | @Override
478 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
479 | if (disallowIntercept) {
480 | recycleVelocityTracker();
481 | }
482 | super.requestDisallowInterceptTouchEvent(disallowIntercept);
483 | }
484 |
485 |
486 | /**
487 | * 属性动画,请勿删除
488 | */
489 | public void setAnimateValue(float animateValue) {
490 | this.animateValue = animateValue; // 当前应该在的位置
491 | int dx = Math.round(animateValue - animatingView.getLeft());
492 | requireScrollChange(dx);
493 | }
494 |
495 | public float getAnimateValue() {
496 | return animateValue;
497 | }
498 |
499 | /**
500 | * 适配器
501 | */
502 | public static abstract class Adapter {
503 |
504 | /**
505 | * layout文件ID,调用者必须实现
506 | */
507 | public abstract int getLayoutId();
508 |
509 | /**
510 | * item数量,调用者必须实现
511 | */
512 | public abstract int getItemCount();
513 |
514 | /**
515 | * View与数据绑定回调,可重载
516 | */
517 | public void bindView(View view, int index) {
518 | }
519 |
520 | /**
521 | * 正在展示的回调,可重载
522 | */
523 | public void displaying(int position) {
524 | }
525 |
526 | /**
527 | * item点击,可重载
528 | */
529 | public void onItemClick(View view, int position) {
530 | }
531 | }
532 | }
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PileLibrary
3 |
4 |
--------------------------------------------------------------------------------
/library/src/test/java/com/stone/pile/libs/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.stone.pile.libs;
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() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------