├── .gitignore
├── README.md
├── app
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── dolphinwang
│ │ └── imagecoverflow
│ │ ├── BitmapUtils.java
│ │ ├── CoverFlowAdapter.java
│ │ ├── CoverFlowCell.java
│ │ └── CoverFlowView.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ └── ic_launcher.png
│ ├── values-sw600dp
│ └── dimens.xml
│ ├── values-sw720dp-land
│ └── dimens.xml
│ ├── values-v11
│ └── styles.xml
│ ├── values-v14
│ └── styles.xml
│ └── values
│ └── attrs.xml
├── build.gradle
├── coverflowsample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── mogujie
│ │ └── coverflowsample
│ │ ├── MyActivity.java
│ │ └── MyCoverFlowAdapter.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ ├── footprint_header_bg1.png
│ └── ic_launcher.png
│ ├── layout
│ └── activity_main.xml
│ └── values
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── imagecoverflow_screenshot.png
├── import-summary.txt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | .DS_Store
3 |
4 | #proguard
5 | #mymapping.txt
6 | myusage.txt
7 | # library
8 | library/
9 |
10 | # android generated
11 | bin/
12 | gen/
13 |
14 | # eclipse
15 | .classpath
16 | .settings
17 | .project
18 |
19 | local.properties
20 | project.properties
21 |
22 | # IDEA
23 | .idea/
24 | *.iml
25 | out/
26 |
27 | # gradle
28 | build/
29 | .gradle/
30 | gradle/
31 | gradlew
32 | gradlew.bat
33 | gradle.properties
34 |
35 | # build
36 | lint.xml
37 | lint.html
38 | proguard-rules.txt
39 |
40 | run.sh
41 |
42 | # test
43 | test.html
44 | ajcore.*
45 |
46 | #unit test
47 | androidTest/
48 |
49 | # Sonar 静态代码检查
50 | .sonar/
51 | .gradletasknamecache
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ImageCoverFlow
2 |
3 | #### To show Cover Flow effect on Android
4 |
5 | ImageCoverFlow is an open source Android library that allows developers to easily create applications with a cover flow effect to show images. This library does not extend Gallery. Feel free to use it all you want in your Android apps provided that you cite this project and include the license in your app.
6 |
7 | 
8 |
9 | #### ImageCoverFlow is currently used in some published Android apps:
10 |
11 | 1. [ICardEnglish](https://play.google.com/store/apps/details?id=com.cn.icardenglish&hl=zh_CN)
12 | 2. [PNP iSerbis](https://play.google.com/store/apps/details?id=ph.gov.pnp.itms.itmspnpiserbis)
13 |
14 | ---
15 |
16 | # How to Use:
17 |
18 | #### Step One: Add `CoverFlowView` to your project
19 |
20 | 1. Via XML:
21 |
22 | ```xml
23 |
35 | ```
36 |
37 | 2. Programatically (via Java):
38 |
39 | ```java
40 | CoverFlowView mCoverFlowView =
41 | (CoverFlowView) findViewById(R.id.coverflow);
42 |
43 | mCoverFlowView.setCoverFlowGravity(CoverFlowGravity.CENTER_VERTICAL);
44 | mCoverFlowView.setCoverFlowLayoutMode(CoverFlowLayoutMode.WRAP_CONTENT);
45 | mCoverFlowView.setReflectionHeight(30);
46 | mCoverFlowView.setReflectionGap(20);
47 | mCoverFlowView.setVisibleImage(5);
48 | ```
49 |
50 | ---
51 |
52 | #### Step Two: Set an adapter, which extends `CoverFlowAdapter`:
53 |
54 | ```java
55 | MyCoverFlowAdapter adapter = new MyCoverFlowAdapter(this);
56 | mCoverFlowView.setAdapter(adapter);
57 | ```
58 |
59 | **TIPS**:
60 | * Method `setAdapter()` should be called after all properties of CoverFlow are settled.
61 | * If you want to load image dynamically, you can call method `notifyDataSetChanged()` when bitmaps are loaded.
62 |
63 | #### Step Three: if you want to listen for the click event of the top image, you can set a `StateListener` to it:
64 |
65 | ```java
66 | mCoverFlowView.setStateListener(new CoverFlowView.StateListener() {
67 | @Override
68 | public void imageOnTop(CoverFlowView view, int position,
69 | float left, float top, float right,float bottom) {
70 | // TODO
71 | }
72 |
73 | @Override
74 | public void invalidationCompleted(CoverFlowView view) {
75 | // TODO
76 | }
77 | });
78 | ```
79 |
80 | if you want to listen for click events of showing images, you can set a `ImageClickListener` to it:
81 |
82 | ```java
83 | mCoverFlowView.setImageClickListener(new CoverFlowView.ImageClickListener() {
84 | @Override
85 | public void onClick(CoverFlowView coverFlowView, int position) {
86 | // TODO
87 | }
88 | });
89 | ```
90 |
91 |
92 | If you want to listen for long click events of the top image, you can set a `ImageLongClickListener` to it:
93 |
94 | ```java
95 | mCoverFlowView
96 | .setImageLongClickListener(new CoverFlowView.ImageLongClickListener() {
97 | @Override
98 | public void onLongClick(CoverFlowView view, int position) {
99 | // TODO
100 | }
101 | });
102 | ```
103 |
104 | Users can use method `setSelection(int position)` to show a specific position at the top.
105 |
106 | ---
107 |
108 | #### If you want to subclass `CoverFlowView`
109 |
110 | 1. You can override method `getCustomTransformMatrix()` to make more transformations for images (there is some annotated code which shows how to make image y-axis rotation).
111 | 2. You should never override method `onLayout()` to layout any of `CoverFlowView`’s children, because all of image will draw on the canvas directly.
112 |
113 | ---
114 |
115 | #### Developed By:
116 |
117 | Roy Wang (dolphinwang@foxmail.com)
118 |
119 | If you use this library, please let me know.
120 |
121 | ---
122 |
123 | #### License:
124 |
125 | Copyright 2013 Roy Wang
126 |
127 | Licensed under the Apache License, Version 2.0 (the "License");
128 | you may not use this file except in compliance with the License.
129 | You may obtain a copy of the License at
130 |
131 | http://www.apache.org/licenses/LICENSE-2.0
132 |
133 | Unless required by applicable law or agreed to in writing, software
134 | distributed under the License is distributed on an "AS IS" BASIS,
135 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
136 | See the License for the specific language governing permissions and
137 | limitations under the License.
138 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion '25.0.2'
6 | defaultConfig {
7 | minSdkVersion 8
8 | targetSdkVersion 23
9 | }
10 |
11 | lintOptions {
12 | abortOnError false
13 | }
14 | compileOptions {
15 | sourceCompatibility JavaVersion.VERSION_1_7
16 | targetCompatibility JavaVersion.VERSION_1_7
17 | }
18 | }
19 |
20 | dependencies {
21 | compile 'com.android.support:support-v4:18.0.0'
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dolphinwang/imagecoverflow/BitmapUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 Roy Wang
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.dolphinwang.imagecoverflow;
17 |
18 | import android.graphics.Bitmap;
19 | import android.graphics.Canvas;
20 | import android.graphics.LinearGradient;
21 | import android.graphics.Matrix;
22 | import android.graphics.Paint;
23 | import android.graphics.PorterDuffXfermode;
24 | import android.graphics.Shader.TileMode;
25 |
26 | public class BitmapUtils {
27 | public static Bitmap createReflectedBitmap(Bitmap srcBitmap,
28 | float reflectHeight) {
29 | if (null == srcBitmap) {
30 | return null;
31 | }
32 |
33 | int srcWidth = srcBitmap.getWidth();
34 | int srcHeight = srcBitmap.getHeight();
35 | int reflectionWidth = srcBitmap.getWidth();
36 | int reflectionHeight = reflectHeight == 0 ? srcHeight / 3
37 | : (int) (reflectHeight * srcHeight);
38 |
39 | if (0 == srcWidth || srcHeight == 0) {
40 | return null;
41 | }
42 |
43 | // The matrix
44 | Matrix matrix = new Matrix();
45 | matrix.preScale(1, -1);
46 |
47 | try {
48 | // The reflection bitmap, width is same with original's
49 | Bitmap reflectionBitmap = Bitmap.createBitmap(srcBitmap, 0,
50 | srcHeight - reflectionHeight, reflectionWidth,
51 | reflectionHeight, matrix, false);
52 |
53 | if (null == reflectionBitmap) {
54 | return null;
55 | }
56 |
57 | Canvas canvas = new Canvas(reflectionBitmap);
58 |
59 | Paint paint = new Paint();
60 | paint.setAntiAlias(true);
61 |
62 | LinearGradient shader = new LinearGradient(0, 0, 0,
63 | reflectionBitmap.getHeight(), 0x70FFFFFF, 0x00FFFFFF,
64 | TileMode.MIRROR);
65 | paint.setShader(shader);
66 |
67 | paint.setXfermode(new PorterDuffXfermode(
68 | android.graphics.PorterDuff.Mode.DST_IN));
69 |
70 | // Draw the linear shader.
71 | canvas.drawRect(0, 0, reflectionBitmap.getWidth(),
72 | reflectionBitmap.getHeight(), paint);
73 |
74 | return reflectionBitmap;
75 | } catch (Exception e) {
76 | e.printStackTrace();
77 | }
78 |
79 | return null;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dolphinwang/imagecoverflow/CoverFlowAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 Roy Wang
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.dolphinwang.imagecoverflow;
17 |
18 | import android.database.DataSetObservable;
19 | import android.database.DataSetObserver;
20 | import android.graphics.Bitmap;
21 |
22 | public abstract class CoverFlowAdapter {
23 | private final DataSetObservable mDataSetObservable = new DataSetObservable();
24 |
25 | public void registerDataSetObserver(DataSetObserver observer) {
26 | mDataSetObservable.registerObserver(observer);
27 | }
28 |
29 | public void unregisterDataSetObserver(DataSetObserver observer) {
30 | mDataSetObservable.unregisterObserver(observer);
31 | }
32 |
33 | public void notifyDataSetChanged() {
34 | mDataSetObservable.notifyChanged();
35 | }
36 |
37 | public void notifyDataSetInvalidated() {
38 | mDataSetObservable.notifyInvalidated();
39 | }
40 |
41 | public int getItemViewType(int position) {
42 | return 0;
43 | }
44 |
45 | public int getViewTypeCount() {
46 | return 1;
47 | }
48 |
49 | public abstract int getCount();
50 |
51 | public abstract Bitmap getImage(int position);
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dolphinwang/imagecoverflow/CoverFlowCell.java:
--------------------------------------------------------------------------------
1 | package com.dolphinwang.imagecoverflow;
2 |
3 | import android.graphics.Matrix;
4 | import android.graphics.RectF;
5 |
6 | /**
7 | * Created by dolphinWang on 2018/3/5.
8 | */
9 |
10 | public class CoverFlowCell {
11 |
12 | public int width;
13 | public int height;
14 |
15 | public boolean isOnTop;
16 |
17 | public int showingPosition;
18 | public int index;
19 |
20 | public RectF showingRect;
21 |
22 | public CoverFlowCell() {
23 | showingRect = new RectF();
24 | }
25 |
26 | public boolean mapTransform(Matrix transformer) {
27 | if (height == 0 || width == 0) {
28 | return false;
29 | }
30 |
31 | showingRect.top = 0;
32 | showingRect.left = 0;
33 | showingRect.right = width;
34 | showingRect.bottom = height;
35 |
36 | return transformer.mapRect(showingRect);
37 | }
38 |
39 | public boolean inTouchArea(float x, float y) {
40 | return showingRect.contains(x, y);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dolphinwang/imagecoverflow/CoverFlowView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 Roy Wang
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.dolphinwang.imagecoverflow;
17 |
18 | import android.app.ActivityManager;
19 | import android.content.Context;
20 | import android.content.res.TypedArray;
21 | import android.database.DataSetObserver;
22 | import android.graphics.Bitmap;
23 | import android.graphics.Canvas;
24 | import android.graphics.Matrix;
25 | import android.graphics.Paint;
26 | import android.graphics.PaintFlagsDrawFilter;
27 | import android.graphics.Rect;
28 | import android.os.Build;
29 | import android.support.v4.util.LruCache;
30 | import android.util.AttributeSet;
31 | import android.util.Log;
32 | import android.util.SparseArray;
33 | import android.view.MotionEvent;
34 | import android.view.VelocityTracker;
35 | import android.view.View;
36 | import android.view.ViewConfiguration;
37 | import android.view.animation.AccelerateDecelerateInterpolator;
38 | import android.view.animation.AnimationUtils;
39 | import android.widget.Scroller;
40 |
41 | /**
42 | * @author dolphinWang
43 | * @time 2013-11-29
44 | */
45 | public class CoverFlowView extends View {
46 |
47 | public enum CoverFlowGravity {
48 | TOP, BOTTOM, CENTER_VERTICAL
49 | }
50 |
51 | public enum CoverFlowLayoutMode {
52 | MATCH_PARENT, WRAP_CONTENT
53 | }
54 |
55 | /****
56 | * static field
57 | ****/
58 | private static final String VIEW_LOG_TAG = "CoverFlowView";
59 |
60 | private static final int DURATION = 200;
61 |
62 | protected final int INVALID_INDEX = -1;
63 |
64 | protected static final int DEFAULT_VISIBLE_IMAGES = 3;
65 |
66 | // Used to indicate a no preference for a position type.
67 | static final int NO_POSITION = -1;
68 |
69 | // the visible views left and right
70 | protected int mVisibleImages = DEFAULT_VISIBLE_IMAGES;
71 |
72 | // space between each two of children
73 | protected final int CHILD_SPACING = -200;
74 |
75 | // 基础alphaֵ
76 | private final int ALPHA_DATUM = 76;
77 | private int STANDARD_ALPHA;
78 | // 基础缩放值
79 | private static final float CARD_SCALE = 0.15f;
80 | private static float MOVE_POS_MULTIPLE = 3.0f;
81 | private static final int TOUCH_MINIMUM_MOVE = 5;
82 | private static final float MOVE_SPEED_MULTIPLE = 1;
83 | private static final float MAX_SPEED = 6.0f;
84 | private static final float FRICTION = 10.0f;
85 |
86 | private static final int LONG_CLICK_DELAY = ViewConfiguration
87 | .getLongPressTimeout();
88 | /****
89 | * static field
90 | ****/
91 |
92 | private RecycleBin mRecycler;
93 | protected int mCoverFlowCenter;
94 | private T mAdapter;
95 |
96 | private int mVisibleImageCount;
97 | private int mImageCount;
98 |
99 | /**
100 | * True if the data has changed since the last layout
101 | */
102 | boolean mDataSetChanged;
103 |
104 | protected CoverFlowGravity mGravity;
105 |
106 | protected CoverFlowLayoutMode mLayoutMode;
107 |
108 | private Rect mCoverFlowPadding;
109 |
110 | private PaintFlagsDrawFilter mDrawFilter;
111 |
112 | private Matrix mImageTransformer;
113 | private Matrix mReflectionTransformer;
114 |
115 | private Paint mDrawImagePaint;
116 |
117 | private SparseArray mImageCells;
118 |
119 | private int mWidth;
120 | private boolean mTouchMoved;
121 | private float mTouchStartPos;
122 | private float mTouchStartX;
123 | private float mTouchStartY;
124 |
125 | private float mOffset;
126 |
127 | private float mStartOffset;
128 | private long mStartTime;
129 |
130 | private float mStartSpeed;
131 | private float mDuration;
132 | private Runnable mAnimationRunnable;
133 | private VelocityTracker mVelocity;
134 |
135 | private int mCellHeight;
136 | private int mChildTranslateY;
137 | private int mReflectionTranslateY;
138 |
139 | private float reflectHeightFraction;
140 | private int reflectGap;
141 |
142 | private boolean imageClickEnable = true;
143 | private boolean imageLongClickEnable = true;
144 |
145 | private StateListener mStateListener;
146 |
147 | private ImageLongClickListener mLongClickListener;
148 | private LongClickRunnable mLongClickRunnable;
149 | private boolean mLongClickTriggled;
150 |
151 | private ImageClickListener mClickListener;
152 |
153 | private int mTopImageIndex;
154 |
155 | private Scroller mScroller;
156 |
157 | private DataSetObserver mDataSetObserver = new DataSetObserver() {
158 |
159 | @Override
160 | public void onChanged() {
161 | final int nextImageCount = mAdapter.getCount();
162 |
163 | // If current index of top image will larger than total count in future,
164 | // locate it to new mid.
165 | if (mTopImageIndex % mImageCount > nextImageCount - 1) {
166 | mOffset = nextImageCount - mVisibleImages - 1;
167 | } else { // If current index top top image will less than total count in future,
168 | // change mOffset to current state in first loop
169 | mOffset += mVisibleImages;
170 | while (mOffset < 0 || mOffset >= mImageCount) {
171 | if (mOffset < 0) {
172 | mOffset += mImageCount;
173 | } else if (mOffset >= mImageCount) {
174 | mOffset -= mImageCount;
175 | }
176 | }
177 | mOffset -= mVisibleImages;
178 | }
179 |
180 | mImageCount = nextImageCount;
181 | resetCoverFlow();
182 |
183 | requestLayout();
184 | invalidate();
185 | super.onChanged();
186 | }
187 |
188 | @Override
189 | public void onInvalidated() {
190 | super.onInvalidated();
191 | }
192 |
193 | };
194 |
195 |
196 | public CoverFlowView(Context context) {
197 | super(context);
198 | init();
199 | }
200 |
201 | public CoverFlowView(Context context, AttributeSet attrs) {
202 | super(context, attrs);
203 | initAttributes(context, attrs);
204 | init();
205 | }
206 |
207 | public CoverFlowView(Context context, AttributeSet attrs, int defStyle) {
208 | super(context, attrs, defStyle);
209 | initAttributes(context, attrs);
210 | init();
211 | }
212 |
213 | private void initAttributes(Context context, AttributeSet attrs) {
214 | TypedArray a = context.obtainStyledAttributes(attrs,
215 | R.styleable.ImageCoverFlowView);
216 |
217 | int totalVisibleChildren = a.getInt(
218 | R.styleable.ImageCoverFlowView_visibleImage, 3);
219 | setVisibleImage(totalVisibleChildren);
220 |
221 | reflectHeightFraction = a.getFraction(
222 | R.styleable.ImageCoverFlowView_reflectionHeight, 100, 0, 0.0f);
223 | if (reflectHeightFraction > 100) {
224 | reflectHeightFraction = 100;
225 | }
226 | reflectHeightFraction /= 100;
227 | reflectGap = a.getDimensionPixelSize(
228 | R.styleable.ImageCoverFlowView_reflectionGap, 0);
229 |
230 | imageClickEnable = a.getBoolean(R.styleable.ImageCoverFlowView_imageClickEnable, true);
231 | imageLongClickEnable = a.getBoolean(R.styleable.ImageCoverFlowView_imageLongClickEnable, true);
232 |
233 | mGravity = CoverFlowGravity.values()[a.getInt(
234 | R.styleable.ImageCoverFlowView_coverflowGravity,
235 | CoverFlowGravity.CENTER_VERTICAL.ordinal())];
236 |
237 | mLayoutMode = CoverFlowLayoutMode.values()[a.getInt(
238 | R.styleable.ImageCoverFlowView_coverflowLayoutMode,
239 | CoverFlowLayoutMode.WRAP_CONTENT.ordinal())];
240 |
241 | a.recycle();
242 | }
243 |
244 | private void init() {
245 | setWillNotDraw(false);
246 | setClickable(true);
247 |
248 | mImageTransformer = new Matrix();
249 | mReflectionTransformer = new Matrix();
250 |
251 | mDrawImagePaint = new Paint();
252 | mDrawImagePaint.setAntiAlias(true);
253 | mDrawImagePaint.setFlags(Paint.ANTI_ALIAS_FLAG);
254 |
255 | mCoverFlowPadding = new Rect();
256 |
257 | mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
258 | | Paint.FILTER_BITMAP_FLAG);
259 |
260 | mScroller = new Scroller(getContext(),
261 | new AccelerateDecelerateInterpolator());
262 | }
263 |
264 | /**
265 | * if subclass override this method, should call super method.
266 | *
267 | * @param adapter extends CoverFlowAdapter
268 | */
269 | public void setAdapter(T adapter) {
270 |
271 | if (mAdapter != null) {
272 | mAdapter.unregisterDataSetObserver(mDataSetObserver);
273 | }
274 |
275 | mAdapter = adapter;
276 |
277 | if (mAdapter != null) {
278 | mAdapter.registerDataSetObserver(mDataSetObserver);
279 |
280 | mImageCount = mAdapter.getCount();
281 |
282 | if (mRecycler != null) {
283 | mRecycler.clear();
284 | } else {
285 | mRecycler = new RecycleBin();
286 | }
287 | }
288 |
289 | mOffset = 0;
290 |
291 | resetCoverFlow();
292 |
293 | requestLayout();
294 | }
295 |
296 | public T getAdapter() {
297 | return mAdapter;
298 | }
299 |
300 | public void setStateListener(StateListener l) {
301 | mStateListener = l;
302 | }
303 |
304 | private void resetCoverFlow() {
305 |
306 | if (mImageCount < 3) {
307 | throw new IllegalArgumentException(
308 | "total count in adapter must larger than 3!");
309 | }
310 |
311 | final int totalVisible = mVisibleImages * 2 + 1;
312 | if (mImageCount < totalVisible) {
313 | mVisibleImages = (mImageCount - 1) / 2;
314 | }
315 |
316 | mCellHeight = 0;
317 |
318 | STANDARD_ALPHA = (255 - ALPHA_DATUM) / mVisibleImages;
319 |
320 | if (mGravity == null) {
321 | mGravity = CoverFlowGravity.CENTER_VERTICAL;
322 | }
323 |
324 | if (mLayoutMode == null) {
325 | mLayoutMode = CoverFlowLayoutMode.WRAP_CONTENT;
326 | }
327 |
328 | mImageCells = new SparseArray<>(mImageCount);
329 |
330 | mTopImageIndex = INVALID_INDEX;
331 | mDataSetChanged = true;
332 | }
333 |
334 | @Override
335 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
336 | super.onSizeChanged(w, h, oldw, oldh);
337 | }
338 |
339 | /**
340 | * 这个函数除了计算父控件的宽高之外,最重要的是计算出出现在屏幕上的图片的宽高
341 | */
342 | @Override
343 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
344 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
345 |
346 | if (mAdapter == null) {
347 | return;
348 | }
349 |
350 | if (!mDataSetChanged) {
351 | return;
352 | }
353 |
354 | mCoverFlowPadding.left = getPaddingLeft();
355 | mCoverFlowPadding.right = getPaddingRight();
356 | mCoverFlowPadding.top = getPaddingTop();
357 | mCoverFlowPadding.bottom = getPaddingBottom();
358 |
359 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
360 | int widthSize = MeasureSpec.getSize(widthMeasureSpec);
361 | int heightSize = MeasureSpec.getSize(heightMeasureSpec);
362 |
363 | int availableHeight = heightSize - mCoverFlowPadding.top
364 | - mCoverFlowPadding.bottom;
365 |
366 | int maxChildTotalHeight = 0;
367 |
368 | int visibleCount = (mVisibleImages << 1) + 1;
369 | int mid = (int) Math.floor(mOffset + 0.5);
370 | int leftChild = visibleCount >> 1;
371 | final int startPos = getIndex(mid - leftChild);
372 |
373 | for (int i = startPos; i < visibleCount + startPos; ++i) {
374 | Bitmap child = mAdapter.getImage(i);
375 | final int childHeight = child.getHeight();
376 | final int childTotalHeight = (int) (childHeight + childHeight
377 | * reflectHeightFraction + reflectGap);
378 |
379 | maxChildTotalHeight = (maxChildTotalHeight < childTotalHeight) ? childTotalHeight
380 | : maxChildTotalHeight;
381 | }
382 |
383 | if (heightMode == MeasureSpec.EXACTLY
384 | || heightMode == MeasureSpec.AT_MOST) {
385 | // if height which parent provided is less than child need, scale
386 | // child height to parent provide
387 | if (availableHeight < maxChildTotalHeight) {
388 | mCellHeight = availableHeight;
389 | } else {
390 | // if larger than, depends on layout mode
391 | // if layout mode is match_parent, scale child height to parent
392 | // provide
393 | if (mLayoutMode == CoverFlowLayoutMode.MATCH_PARENT) {
394 | mCellHeight = availableHeight;
395 | // if layout mode is wrap_content, keep child's original
396 | // height
397 | } else if (mLayoutMode == CoverFlowLayoutMode.WRAP_CONTENT) {
398 | mCellHeight = maxChildTotalHeight;
399 |
400 | // adjust parent's height
401 | if (heightMode == MeasureSpec.AT_MOST) {
402 | heightSize = mCellHeight + mCoverFlowPadding.top
403 | + mCoverFlowPadding.bottom;
404 | }
405 | }
406 | }
407 | } else {
408 | // height mode is unspecified
409 | if (mLayoutMode == CoverFlowLayoutMode.MATCH_PARENT) {
410 | mCellHeight = availableHeight;
411 | } else if (mLayoutMode == CoverFlowLayoutMode.WRAP_CONTENT) {
412 | mCellHeight = maxChildTotalHeight;
413 |
414 | // adjust parent's height
415 | heightSize = mCellHeight + mCoverFlowPadding.top
416 | + mCoverFlowPadding.bottom;
417 | }
418 | }
419 |
420 | // Adjust movement in y-axis according to gravity
421 | if (mGravity == CoverFlowGravity.CENTER_VERTICAL) {
422 | mChildTranslateY = (heightSize >> 1) - (mCellHeight >> 1);
423 | } else if (mGravity == CoverFlowGravity.TOP) {
424 | mChildTranslateY = mCoverFlowPadding.top;
425 | } else if (mGravity == CoverFlowGravity.BOTTOM) {
426 | mChildTranslateY = heightSize - mCoverFlowPadding.bottom
427 | - mCellHeight;
428 | }
429 | mReflectionTranslateY = (int) (mChildTranslateY + mCellHeight - mCellHeight
430 | * reflectHeightFraction);
431 |
432 | setMeasuredDimension(widthSize, heightSize);
433 | mVisibleImageCount = visibleCount;
434 | mWidth = widthSize;
435 | }
436 |
437 | /**
438 | * subclass should never override this method, because all of child will
439 | * draw on the canvas directly
440 | */
441 | @Override
442 | protected void onLayout(boolean changed, int left, int top, int right,
443 | int bottom) {
444 | }
445 |
446 | @Override
447 | protected void onDraw(Canvas canvas) {
448 |
449 | if (mAdapter == null) {
450 | super.onDraw(canvas);
451 | return;
452 | }
453 |
454 | canvas.setDrawFilter(mDrawFilter);
455 |
456 | Log.e(VIEW_LOG_TAG, "mOffset==>" + mOffset);
457 |
458 | final float offset = mOffset;
459 | int i = 0;
460 | int mid = (int) Math.floor(offset + 0.5);
461 |
462 | int rightChild, leftChild;
463 | rightChild = leftChild = mVisibleImageCount / 2;
464 |
465 | // draw the left children
466 | int startPos = mid - leftChild;
467 | for (i = startPos; i < mid; ++i) {
468 | drawChild(canvas, mid, i, i - offset);
469 | }
470 |
471 | // draw the right children
472 | int endPos = mid + rightChild;
473 | for (i = endPos; i >= mid; --i) {
474 | drawChild(canvas, mid, i, i - offset);
475 | }
476 |
477 | // call imageOnTop
478 | if ((offset - (int) offset) == 0.0f) {
479 | final int topImageIndex = getIndex((int) offset);
480 |
481 | if (mTopImageIndex != topImageIndex
482 | && mImageCells.get(mTopImageIndex) != null) {
483 | mImageCells.get(mTopImageIndex).isOnTop = false;
484 | }
485 | mTopImageIndex = topImageIndex;
486 | final CoverFlowCell cell = mImageCells.get(topImageIndex);
487 | cell.isOnTop = true;
488 |
489 | if (mStateListener != null) {
490 | mStateListener.imageOnTop(this, topImageIndex
491 | , cell.showingRect.left, cell.showingRect.top,
492 | cell.showingRect.right, cell.showingRect.bottom);
493 | }
494 | }
495 |
496 | super.onDraw(canvas);
497 |
498 | if (mStateListener != null) {
499 | mStateListener.invalidationCompleted(this);
500 | }
501 | }
502 |
503 | protected final void drawChild(Canvas canvas, int mid, int position, float offset) {
504 |
505 | int index = getIndex(position);
506 |
507 | final Bitmap child = mAdapter.getImage(index);
508 | final Bitmap reflection = obtainReflection(child);
509 |
510 | CoverFlowCell cell = mImageCells.get(index);
511 | if (cell == null) {
512 | cell = new CoverFlowCell();
513 | cell.index = index;
514 | mImageCells.put(index, cell);
515 | }
516 | cell.showingPosition = position;
517 | cell.width = child.getWidth();
518 | cell.height = child.getHeight();
519 |
520 | if (child != null && !child.isRecycled() && canvas != null) {
521 | makeChildTransformer(child, mid, position, offset);
522 |
523 | // record cell data, only when images in "non-intermediate state"
524 | if ((offset - (int) offset) == 0.0f) {
525 | cell.mapTransform(mImageTransformer);
526 | }
527 |
528 | canvas.drawBitmap(child, mImageTransformer, mDrawImagePaint);
529 |
530 | if (reflection != null) {
531 | canvas.drawBitmap(reflection, mReflectionTransformer,
532 | mDrawImagePaint);
533 | }
534 | }
535 | }
536 |
537 | /**
538 | *
539 | * - 对bitmap进行伪3d变换
540 | *
541 | *
542 | * @param child
543 | * @param position
544 | * @param offset
545 | */
546 | private void makeChildTransformer(Bitmap child, int mid, int position, float offset) {
547 | mImageTransformer.reset();
548 | mReflectionTransformer.reset();
549 |
550 | float spreadScale = 0;
551 | //this scale make sure that each image will be smaller than the
552 | //previous one
553 | if (position != mid) {
554 | spreadScale = 1 - Math.abs(offset) * 0.25f;
555 | } else {
556 | spreadScale = 1 - Math.abs(offset) * CARD_SCALE;
557 | }
558 |
559 | // 延x轴移动的距离应该根据center图片决定
560 | float translateX = 0;
561 |
562 | final int imageHeight = (int) (mCellHeight - mCellHeight
563 | * reflectHeightFraction - reflectGap);
564 | final int cellHeight = (int) (child.getHeight()
565 | + child.getHeight() * reflectHeightFraction + reflectGap);
566 |
567 | final float imageScale = (float) imageHeight / child.getHeight();
568 | final float totallyScale = imageScale * spreadScale;
569 |
570 | final int showingWidth = (int) (child.getWidth() * totallyScale);
571 | final int centerShowingWidth = (int) (child.getWidth() * imageScale);
572 | int leftSpace = ((mWidth / 2) - mCoverFlowPadding.left)
573 | - (centerShowingWidth / 2);
574 | int rightSpace = (((mWidth / 2) - mCoverFlowPadding.right) - (centerShowingWidth / 2));
575 |
576 | if (offset <= 0) {
577 | translateX = ((float) leftSpace / mVisibleImages)
578 | * (mVisibleImages + offset) + mCoverFlowPadding.left;
579 | } else {
580 | translateX = mWidth - ((float) rightSpace / mVisibleImages)
581 | * (mVisibleImages - offset) - showingWidth
582 | - mCoverFlowPadding.right;
583 | }
584 |
585 | float alpha = (float) 254 - Math.abs(offset) * STANDARD_ALPHA;
586 |
587 | if (alpha < 0) {
588 | alpha = 0;
589 | } else if (alpha > 254) {
590 | alpha = 254;
591 | }
592 |
593 | mDrawImagePaint.setAlpha((int) alpha);
594 |
595 | mImageTransformer.preTranslate(0, -(cellHeight / 2));
596 | // matrix中的postxxx为顺序执行,相反prexxx为倒叙执行
597 | mImageTransformer.postScale(totallyScale, totallyScale);
598 |
599 | if ((offset - (int) offset) == 0.0f) {
600 | Log.e(VIEW_LOG_TAG, "offset=>" + offset + " scale=>" + totallyScale);
601 | }
602 |
603 | // if actually child height is larger or smaller than original child height
604 | // need to change translate distance of y-axis
605 | float adjustedChildTranslateY = 0;
606 | if (totallyScale != 1) {
607 | adjustedChildTranslateY = (mCellHeight - cellHeight) / 2;
608 | }
609 |
610 | mImageTransformer.postTranslate(translateX, mChildTranslateY
611 | + adjustedChildTranslateY);
612 |
613 | // Log.d(VIEW_LOG_TAG, "position= " + position + " mChildTranslateY= "
614 | // + mChildTranslateY + adjustedChildTranslateY);
615 |
616 | getCustomTransformMatrix(mImageTransformer, mDrawImagePaint, child,
617 | position, offset);
618 |
619 | mImageTransformer.postTranslate(0, (cellHeight / 2));
620 |
621 | mReflectionTransformer.preTranslate(0, -(cellHeight / 2));
622 | mReflectionTransformer.postScale(totallyScale, totallyScale);
623 | mReflectionTransformer.postTranslate(translateX, mReflectionTranslateY
624 | * spreadScale + adjustedChildTranslateY);
625 | getCustomTransformMatrix(mReflectionTransformer, mDrawImagePaint,
626 | child, position, offset);
627 | mReflectionTransformer.postTranslate(0, (cellHeight / 2));
628 | }
629 |
630 | /**
631 | *
632 | * - This is an empty method.
633 | * - Giving user a chance to make more transform base on standard.
634 | *
635 | *
636 | * @param mDrawChildPaint paint, user can set alpha
637 | * @param child bitmap to draw
638 | * @param position
639 | * @param offset offset to center(zero)
640 | */
641 | protected void getCustomTransformMatrix(Matrix transfromer,
642 | Paint mDrawChildPaint, Bitmap child, int position, float offset) {
643 |
644 | /** example code to make image y-axis rotation **/
645 | // Camera c = new Camera();
646 | // c.save();
647 | // Matrix m = new Matrix();
648 | // c.rotateY(10 * (-offset));
649 | // c.getMatrix(m);
650 | // c.restore();
651 | // m.preTranslate(-(child.getWidth() >> 1), -(child.getHeight() >> 1));
652 | // m.postTranslate(child.getWidth() >> 1, child.getHeight() >> 1);
653 | // mChildTransfromMatrix.preConcat(m);
654 | }
655 |
656 | @Override
657 | public boolean onTouchEvent(MotionEvent event) {
658 | if (getParent() != null) {
659 | getParent().requestDisallowInterceptTouchEvent(true);
660 | }
661 |
662 | int action = event.getAction();
663 | switch (action) {
664 | case MotionEvent.ACTION_DOWN:
665 | if (mScroller.computeScrollOffset()) {
666 | mScroller.abortAnimation();
667 | invalidate();
668 | }
669 | touchBegan(event);
670 | return true;
671 | case MotionEvent.ACTION_MOVE:
672 | touchMoved(event);
673 | return true;
674 | case MotionEvent.ACTION_UP:
675 | touchEnded(event);
676 | return true;
677 | }
678 |
679 | return false;
680 | }
681 |
682 | private void startLongClick(float x, float y) {
683 | if (imageLongClickable()) {
684 | final int touchedImageIndex = findTouchedImage(x, y, mTopImageIndex);
685 | if (touchedImageIndex != INVALID_INDEX) {
686 | mLongClickRunnable.setPosition(touchedImageIndex);
687 | postDelayed(mLongClickRunnable, LONG_CLICK_DELAY);
688 | }
689 | }
690 | }
691 |
692 | private void resetLongClick() {
693 | if (mLongClickRunnable != null) {
694 | removeCallbacks(mLongClickRunnable);
695 | }
696 | mLongClickTriggled = false;
697 | }
698 |
699 | private int findTouchedImage(float x, float y, int mid) {
700 | final int leftStart = mid - (mVisibleImageCount / 2);
701 | for (int i = mid; i >= leftStart; i--) {
702 | if (i >= mImageCount) {
703 | i -= mImageCount;
704 | } else if (i < 0) {
705 | i += mImageCount;
706 | }
707 |
708 | CoverFlowCell cell = mImageCells.get(i);
709 | if (cell.inTouchArea(x, y)) {
710 | return i;
711 | }
712 | }
713 |
714 | final int rightStart = mid + 1;
715 | for (int i = rightStart; i <= mid + (mVisibleImageCount / 2); i++) {
716 | if (i >= mImageCount) {
717 | i -= mImageCount;
718 | } else if (i < 0) {
719 | i += mImageCount;
720 | }
721 |
722 | CoverFlowCell cell = mImageCells.get(i);
723 | if (cell.inTouchArea(x, y)) {
724 | return i;
725 | }
726 | }
727 |
728 | return INVALID_INDEX;
729 | }
730 |
731 | private boolean imageLongClickable() {
732 | return (mLongClickListener != null && imageLongClickEnable);
733 | }
734 |
735 | private boolean imageClickable() {
736 | return (mClickListener != null && imageClickEnable);
737 | }
738 |
739 | private void touchBegan(MotionEvent event) {
740 | endAnimation();
741 |
742 | float x = event.getX();
743 | mTouchStartX = x;
744 | mTouchStartY = event.getY();
745 | mStartTime = AnimationUtils.currentAnimationTimeMillis();
746 | mStartOffset = mOffset;
747 |
748 | mTouchMoved = false;
749 |
750 | mTouchStartPos = (x / mWidth) * MOVE_POS_MULTIPLE - 5;
751 | mTouchStartPos /= 2;
752 |
753 | mVelocity = VelocityTracker.obtain();
754 | mVelocity.addMovement(event);
755 |
756 | // reset first
757 | resetLongClick();
758 | // than post an runnable to start lone clock event monitor
759 | startLongClick(mTouchStartX, mTouchStartY);
760 | }
761 |
762 | private void touchMoved(MotionEvent event) {
763 | float pos = (event.getX() / mWidth) * MOVE_POS_MULTIPLE - 5;
764 | pos /= 2;
765 |
766 | if (!mTouchMoved) {
767 | float dx = Math.abs(event.getX() - mTouchStartX);
768 | float dy = Math.abs(event.getY() - mTouchStartY);
769 |
770 | if (dx < TOUCH_MINIMUM_MOVE && dy < TOUCH_MINIMUM_MOVE)
771 | return;
772 |
773 | mTouchMoved = true;
774 |
775 | resetLongClick();
776 | }
777 |
778 | mOffset = mStartOffset + mTouchStartPos - pos;
779 |
780 | invalidate();
781 | mVelocity.addMovement(event);
782 | }
783 |
784 | private void touchEnded(MotionEvent event) {
785 | float pos = (event.getX() / mWidth) * MOVE_POS_MULTIPLE - 5;
786 | pos /= 2;
787 |
788 | if (mTouchMoved || (mOffset - Math.floor(mOffset)) != 0) {
789 | mStartOffset += mTouchStartPos - pos;
790 | mOffset = mStartOffset;
791 |
792 | mVelocity.addMovement(event);
793 |
794 | mVelocity.computeCurrentVelocity(1000);
795 | double speed = mVelocity.getXVelocity();
796 |
797 | speed = (speed / mWidth) * MOVE_SPEED_MULTIPLE;
798 | if (speed > MAX_SPEED)
799 | speed = MAX_SPEED;
800 | else if (speed < -MAX_SPEED)
801 | speed = -MAX_SPEED;
802 |
803 | startAnimation(-speed);
804 | } else {
805 | final float x = event.getX();
806 | final float y = event.getY();
807 | Log.e(VIEW_LOG_TAG,
808 | " touch ==>" + x + " , " + y);
809 |
810 | if (imageClickable() && !mLongClickTriggled) {
811 | final int touchedImageIndex = findTouchedImage(x, y, mTopImageIndex);
812 | if (touchedImageIndex != INVALID_INDEX) {
813 | mClickListener.onClick(this, touchedImageIndex);
814 | }
815 | }
816 | }
817 |
818 | mVelocity.clear();
819 | mVelocity.recycle();
820 |
821 | resetLongClick();
822 | }
823 |
824 | private void startAnimation(double speed) {
825 | if (mAnimationRunnable != null)
826 | return;
827 |
828 | double delta = speed * speed / (FRICTION * 2);
829 | if (speed < 0)
830 | delta = -delta;
831 |
832 | double nearest = mStartOffset + delta;
833 | nearest = Math.floor(nearest + 0.5);
834 |
835 | mStartSpeed = (float) Math.sqrt(Math.abs(nearest - mStartOffset)
836 | * FRICTION * 2);
837 | if (nearest < mStartOffset)
838 | mStartSpeed = -mStartSpeed;
839 |
840 | mDuration = Math.abs(mStartSpeed / FRICTION);
841 | mStartTime = AnimationUtils.currentAnimationTimeMillis();
842 |
843 | mAnimationRunnable = new Runnable() {
844 | @Override
845 | public void run() {
846 | driveAnimation();
847 | }
848 | };
849 | post(mAnimationRunnable);
850 | }
851 |
852 | private void driveAnimation() {
853 | float elapsed = (AnimationUtils.currentAnimationTimeMillis() - mStartTime) / 1000.0f;
854 | if (elapsed >= mDuration)
855 | endAnimation();
856 | else {
857 | updateAnimationAtElapsed(elapsed);
858 | post(mAnimationRunnable);
859 | }
860 | }
861 |
862 | private void endAnimation() {
863 | if (mAnimationRunnable != null) {
864 | mOffset = (float) Math.floor(mOffset + 0.5);
865 |
866 | invalidate();
867 |
868 | removeCallbacks(mAnimationRunnable);
869 | mAnimationRunnable = null;
870 | }
871 | }
872 |
873 | private void updateAnimationAtElapsed(float elapsed) {
874 | if (elapsed > mDuration)
875 | elapsed = mDuration;
876 |
877 | float delta = Math.abs(mStartSpeed) * elapsed - FRICTION * elapsed
878 | * elapsed / 2;
879 | if (mStartSpeed < 0)
880 | delta = -delta;
881 |
882 | mOffset = mStartOffset + delta;
883 | invalidate();
884 | }
885 |
886 | /**
887 | * Convert showing position to index in adapter
888 | *
889 | * @param showingPosition position to draw
890 | * @return
891 | */
892 | private int getIndex(int showingPosition) {
893 | if (mAdapter == null) {
894 | return INVALID_INDEX;
895 | }
896 |
897 | int max = mAdapter.getCount();
898 |
899 | showingPosition += mVisibleImages;
900 | while (showingPosition < 0 || showingPosition >= max) {
901 | if (showingPosition < 0) {
902 | showingPosition += max;
903 | } else if (showingPosition >= max) {
904 | showingPosition -= max;
905 | }
906 | }
907 |
908 | return showingPosition;
909 | }
910 |
911 | private Bitmap obtainReflection(Bitmap src) {
912 | if (reflectHeightFraction <= 0) {
913 | return null;
914 | }
915 |
916 | Bitmap reflection = mRecycler.getCachedReflectiuon(src);
917 |
918 | if (reflection == null || reflection.isRecycled()) {
919 | mRecycler.removeReflectionCache(src);
920 |
921 | reflection = BitmapUtils.createReflectedBitmap(src,
922 | reflectHeightFraction);
923 |
924 | if (reflection != null) {
925 | mRecycler.buildReflectionCache(src, reflection);
926 |
927 | return reflection;
928 | }
929 | }
930 |
931 | return reflection;
932 | }
933 |
934 | public void setVisibleImage(int count) {
935 | if (count % 2 == 0) {
936 | throw new IllegalArgumentException(
937 | "visible image must be an odd number");
938 | }
939 |
940 | if (count < 3) {
941 | throw new IllegalArgumentException(
942 | "visible image must larger than 3");
943 | }
944 |
945 | mVisibleImages = count / 2;
946 | STANDARD_ALPHA = (255 - ALPHA_DATUM) / mVisibleImages;
947 | }
948 |
949 | public void setCoverFlowGravity(CoverFlowGravity gravity) {
950 | mGravity = gravity;
951 | }
952 |
953 | public void setCoverFlowLayoutMode(CoverFlowLayoutMode mode) {
954 | mLayoutMode = mode;
955 | }
956 |
957 | public void setReflectionHeight(int fraction) {
958 | if (fraction < 0)
959 | fraction = 0;
960 | else if (fraction > 100)
961 | fraction = 100;
962 |
963 | reflectHeightFraction = fraction;
964 | }
965 |
966 | public void setReflectionGap(int gap) {
967 | if (gap < 0)
968 | gap = 0;
969 |
970 | reflectGap = gap;
971 | }
972 |
973 | public void disableImageClick() {
974 | imageClickEnable = false;
975 | }
976 |
977 | public void enableImageClick() {
978 | imageClickEnable = true;
979 | }
980 |
981 | public void disableImageLongClick() {
982 | imageLongClickEnable = false;
983 | }
984 |
985 | public void enableImageLongClick() {
986 | imageLongClickEnable = true;
987 | }
988 |
989 | public void setSelection(int position) {
990 | final int max = mAdapter.getCount();
991 | if (position < 0 || position >= max) {
992 | throw new IllegalArgumentException(
993 | "Position want to select can not less than 0 or larger than max of adapter provide!");
994 | }
995 |
996 | if (mTopImageIndex != position) {
997 | if (mScroller.computeScrollOffset()) {
998 | mScroller.abortAnimation();
999 | }
1000 |
1001 | final int fromX = (int) (mOffset * 100);
1002 |
1003 | final CoverFlowCell destCell = mImageCells.get(position);
1004 | final CoverFlowCell midCell = mImageCells.get(mTopImageIndex);
1005 |
1006 | if (destCell.showingRect.right > midCell.showingRect.right
1007 | && position < mTopImageIndex) {
1008 | position += mImageCount;
1009 | } else if (destCell.showingRect.left < midCell.showingRect.left
1010 | && position > mTopImageIndex) {
1011 | position -= mImageCount;
1012 | }
1013 |
1014 | final int indexGap = position - mTopImageIndex;
1015 | final int disX = indexGap * 100;
1016 |
1017 | mScroller.startScroll(
1018 | fromX,
1019 | 0,
1020 | disX,
1021 | 0,
1022 | DURATION * Math.abs(indexGap));
1023 |
1024 | invalidate();
1025 | }
1026 | }
1027 |
1028 | @Override
1029 | public void computeScroll() {
1030 | super.computeScroll();
1031 |
1032 | if (mScroller.computeScrollOffset()) {
1033 | final int currX = mScroller.getCurrX();
1034 |
1035 | mOffset = (float) currX / 100;
1036 |
1037 | invalidate();
1038 | }
1039 | }
1040 |
1041 | public void setImageLongClickListener(ImageLongClickListener listener) {
1042 | mLongClickListener = listener;
1043 |
1044 | if (listener == null) {
1045 | mLongClickRunnable = null;
1046 | } else {
1047 | if (mLongClickRunnable == null) {
1048 | mLongClickRunnable = new LongClickRunnable();
1049 | }
1050 | }
1051 | }
1052 |
1053 | public void setImageClickListener(ImageClickListener listener) {
1054 | mClickListener = listener;
1055 | }
1056 |
1057 | public int getTopImageIndex() {
1058 | if (mTopImageIndex == INVALID_INDEX) {
1059 | return -1;
1060 | }
1061 |
1062 | return mTopImageIndex;
1063 | }
1064 |
1065 | private class LongClickRunnable implements Runnable {
1066 | private int position;
1067 |
1068 | public void setPosition(int position) {
1069 | this.position = position;
1070 | }
1071 |
1072 | @Override
1073 | public void run() {
1074 | if (mLongClickListener != null) {
1075 | mLongClickListener.onLongClick(CoverFlowView.this, position);
1076 | mLongClickTriggled = true;
1077 | }
1078 | }
1079 | }
1080 |
1081 | class RecycleBin {
1082 |
1083 | final LruCache bitmapCache = new LruCache(
1084 | getCacheSize(getContext())) {
1085 | @Override
1086 | protected int sizeOf(Integer key, Bitmap bitmap) {
1087 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) {
1088 | return bitmap.getRowBytes() * bitmap.getHeight();
1089 | } else {
1090 | return bitmap.getByteCount();
1091 | }
1092 | }
1093 |
1094 | @Override
1095 | protected void entryRemoved(boolean evicted, Integer key,
1096 | Bitmap oldValue, Bitmap newValue) {
1097 | if (evicted && oldValue != null && !oldValue.isRecycled()) {
1098 | oldValue.recycle();
1099 | oldValue = null;
1100 | }
1101 | }
1102 | };
1103 |
1104 | public Bitmap getCachedReflectiuon(Bitmap origin) {
1105 | return bitmapCache.get(origin.hashCode());
1106 | }
1107 |
1108 | public void buildReflectionCache(Bitmap origin, Bitmap b) {
1109 | bitmapCache.put(origin.hashCode(), b);
1110 | Runtime.getRuntime().gc();
1111 | }
1112 |
1113 | public Bitmap removeReflectionCache(Bitmap origin) {
1114 | if (origin == null) {
1115 | return null;
1116 | }
1117 |
1118 | return bitmapCache.remove(origin.hashCode());
1119 | }
1120 |
1121 | public void clear() {
1122 | bitmapCache.evictAll();
1123 | }
1124 |
1125 | private int getCacheSize(Context context) {
1126 | final ActivityManager am = (ActivityManager) context
1127 | .getSystemService(Context.ACTIVITY_SERVICE);
1128 | final int memClass = am.getMemoryClass();
1129 | // Target ~5% of the available heap.
1130 | int cacheSize = 1024 * 1024 * memClass / 21;
1131 |
1132 | Log.e(VIEW_LOG_TAG, "cacheSize == " + cacheSize);
1133 | return cacheSize;
1134 | }
1135 | }
1136 |
1137 | public interface ImageLongClickListener {
1138 | void onLongClick(CoverFlowView coverFlowView, int position);
1139 | }
1140 |
1141 | public interface ImageClickListener {
1142 | void onClick(CoverFlowView coverFlowView, int position);
1143 | }
1144 |
1145 | public interface StateListener {
1146 | void imageOnTop(CoverFlowView coverFlowView,
1147 | int position, float left, float top, float right, float bottom);
1148 |
1149 | void invalidationCompleted(CoverFlowView coverFlowView);
1150 | }
1151 | }
1152 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-sw600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-sw720dp-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 128dp
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | jcenter()
5 | }
6 | dependencies {
7 | classpath 'com.android.tools.build:gradle:2.3.3'
8 | }
9 | }
10 |
11 | allprojects {
12 | repositories {
13 | jcenter()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/coverflowsample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/coverflowsample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion '25.0.2'
6 | defaultConfig {
7 | applicationId "com.mogujie.coverflow"
8 | minSdkVersion 8
9 | targetSdkVersion 23
10 | }
11 |
12 | lintOptions {
13 | abortOnError false
14 | }
15 | compileOptions {
16 | sourceCompatibility JavaVersion.VERSION_1_7
17 | targetCompatibility JavaVersion.VERSION_1_7
18 | }
19 | }
20 |
21 | dependencies {
22 | compile fileTree(dir: 'libs', include: ['*.jar'])
23 | compile 'com.android.support:appcompat-v7:22.2.0'
24 | compile project(':app')
25 | }
26 |
--------------------------------------------------------------------------------
/coverflowsample/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 /Users/dolphinWang/Documents/android-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 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/java/com/mogujie/coverflowsample/MyActivity.java:
--------------------------------------------------------------------------------
1 | package com.mogujie.coverflowsample;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.util.Log;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 |
9 | import com.dolphinwang.imagecoverflow.CoverFlowView;
10 |
11 |
12 | public class MyActivity extends Activity {
13 |
14 | protected static final String VIEW_LOG_TAG = "CoverFlowDemo";
15 |
16 | @Override
17 | protected void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | View v = LayoutInflater.from(this).inflate(R.layout.activity_main,
20 | null, false);
21 | setContentView(v);
22 |
23 | final CoverFlowView mCoverFlowView = (CoverFlowView) findViewById(R.id.coverflow);
24 |
25 | final MyCoverFlowAdapter adapter = new MyCoverFlowAdapter(this);
26 | mCoverFlowView.setAdapter(adapter);
27 | mCoverFlowView
28 | .setStateListener(new CoverFlowView.StateListener() {
29 |
30 | @Override
31 | public void imageOnTop(CoverFlowView view, int position, float left, float top, float right, float bottom) {
32 | Log.e(VIEW_LOG_TAG, position + " on top!");
33 | }
34 |
35 | @Override
36 | public void invalidationCompleted(CoverFlowView view) {
37 |
38 | }
39 | });
40 |
41 | mCoverFlowView
42 | .setImageLongClickListener(new CoverFlowView.ImageLongClickListener() {
43 |
44 | @Override
45 | public void onLongClick(CoverFlowView view, int position) {
46 | Log.e(VIEW_LOG_TAG, "image long clicked ==>"
47 | + position);
48 | }
49 | });
50 |
51 | mCoverFlowView.setImageClickListener(new CoverFlowView.ImageClickListener() {
52 | @Override
53 | public void onClick(CoverFlowView coverFlowView, int position) {
54 | Log.e(VIEW_LOG_TAG, position + " clicked!");
55 | coverFlowView.setSelection(position);
56 | }
57 | });
58 |
59 | findViewById(R.id.change_bitmap_button).setOnClickListener(new View.OnClickListener() {
60 | @Override
61 | public void onClick(View v) {
62 | // adapter.changeBitmap();
63 | mCoverFlowView.setSelection(2);
64 | }
65 | });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/java/com/mogujie/coverflowsample/MyCoverFlowAdapter.java:
--------------------------------------------------------------------------------
1 | package com.mogujie.coverflowsample;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 |
7 | import com.dolphinwang.imagecoverflow.CoverFlowAdapter;
8 |
9 | public class MyCoverFlowAdapter extends CoverFlowAdapter {
10 |
11 | private boolean dataChanged;
12 |
13 | public MyCoverFlowAdapter(Context context) {
14 |
15 | image1 = BitmapFactory.decodeResource(context.getResources(),
16 | R.drawable.footprint_header_bg1);
17 |
18 | image2 = BitmapFactory.decodeResource(context.getResources(),
19 | R.drawable.ic_launcher);
20 | }
21 |
22 | public void changeBitmap() {
23 | dataChanged = true;
24 |
25 | notifyDataSetChanged();
26 | }
27 |
28 | private Bitmap image1 = null;
29 |
30 | private Bitmap image2 = null;
31 |
32 | @Override
33 | public int getCount() {
34 | return dataChanged ? 3 : 8;
35 | }
36 |
37 | @Override
38 | public Bitmap getImage(final int position) {
39 | return (dataChanged && position == 0) ? image2 : image1;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/coverflowsample/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/coverflowsample/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/coverflowsample/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/drawable-xxhdpi/footprint_header_bg1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/coverflowsample/src/main/res/drawable-xxhdpi/footprint_header_bg1.png
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/coverflowsample/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
21 |
22 |
28 |
29 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CoverFlowSample
3 |
4 |
--------------------------------------------------------------------------------
/coverflowsample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/imagecoverflow_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dolphinwang/ImageCoverFlow/bdc814c894644f84fd85a3eb3ff186af7c1cf9fa/imagecoverflow_screenshot.png
--------------------------------------------------------------------------------
/import-summary.txt:
--------------------------------------------------------------------------------
1 | ECLIPSE ANDROID PROJECT IMPORT SUMMARY
2 | ======================================
3 |
4 | Ignored Files:
5 | --------------
6 | The following files were *not* copied into the new Gradle project; you
7 | should evaluate whether these are still needed in your project and if
8 | so manually move them:
9 |
10 | * .gitignore
11 | * README.md
12 | * ic_launcher-web.png
13 | * imagecoverflow_screenshot.png
14 | * proguard-project.txt
15 |
16 | Replaced Jars with Dependencies:
17 | --------------------------------
18 | The importer recognized the following .jar files as third party
19 | libraries and replaced them with Gradle dependencies instead. This has
20 | the advantage that more explicit version information is known, and the
21 | libraries can be updated automatically. However, it is possible that
22 | the .jar file in your project was of an older version than the
23 | dependency we picked, which could render the project not compileable.
24 | You can disable the jar replacement in the import wizard and try again:
25 |
26 | android-support-v4.jar => com.android.support:support-v4:18.0.0
27 |
28 | Moved Files:
29 | ------------
30 | Android Gradle projects use a different directory structure than ADT
31 | Eclipse projects. Here's how the projects were restructured:
32 |
33 | * AndroidManifest.xml => app/src/main/AndroidManifest.xml
34 | * res/ => app/src/main/res/
35 | * src/ => app/src/main/java/
36 |
37 | Next Steps:
38 | -----------
39 | You can now build the project. The Gradle project needs network
40 | connectivity to download dependencies.
41 |
42 | Bugs:
43 | -----
44 | If for some reason your project does not build, and you determine that
45 | it is due to a bug or limitation of the Eclipse to Gradle importer,
46 | please file a bug at http://b.android.com with category
47 | Component-Tools.
48 |
49 | (This import summary is for your information only, and can be deleted
50 | after import once you are satisfied with the results.)
51 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':coverflowsample'
2 |
--------------------------------------------------------------------------------