├── .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 | ![Oops! The screenshot is missing!](https://github.com/dolphinwang/ImageCoverFlow/raw/master/imagecoverflow_screenshot.png) 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 |