├── .gitignore ├── README.md ├── build.gradle ├── coverflowview ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── missmess │ │ └── coverflowview │ │ ├── ACoverFlowAdapter.java │ │ └── CoverFlowView.java │ └── res │ └── values │ ├── attrs.xml │ └── strings.xml ├── gradle.properties ├── raw ├── sample.gif ├── screenshot_1.jpg ├── screenshot_2.jpg └── screenshot_3.jpg ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── missmess │ │ └── demo │ │ ├── MainActivity.java │ │ └── adapter │ │ ├── MyCoverFlowAdapter.java │ │ ├── MyVpAdapter.java │ │ └── NewCoverFlowAdapter.java │ └── res │ ├── drawable │ ├── oval_bg.xml │ └── rectangle_bg.xml │ ├── layout │ ├── activity_main.xml │ ├── item_coverflow1.xml │ ├── item_coverflow2.xml │ ├── item_newcoverflow.xml │ ├── vp_layout1.xml │ ├── vp_layout2.xml │ └── vp_layout3.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ ├── circle.png │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .idea/ 8 | gradle/ 9 | gradlew 10 | gradlew.bat 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoverFlowView 2 | 3 | 基于[ImageCoverFlow](https://github.com/dolphinwang/ImageCoverFlow),但是有较大改动,在ImageCoverFlow的基础上增加了 4 | 对自定义布局的支持,可以支持任意布局。同时增加了一些其他支持,并修复了一些bug,提升绘制速度。 5 | 6 | Based on [ImageCoverFlow](https://github.com/dolphinwang/ImageCoverFlow), add support to custom 7 | layout in CoverFlow, fix some bugs, add other function. 8 | 9 | --- 10 | GIF预览: 11 | 12 | ![gif](https://raw.githubusercontent.com/missmess/CoverFlowView/master/raw/sample.gif) 13 | 14 | --- 15 | 16 | * [主要功能介绍](#主要功能介绍) 17 | * [如何添加到项目中](#如何添加到项目中) 18 | * [如何使用](#如何使用) 19 | * [其它API](#其它API) 20 | * [截图](#截图) 21 | * [关于作者](#关于作者) 22 | 23 | --- 24 | 25 | ### 主要功能介绍 26 | 27 | * 支持自定义布局。 28 | * 支持多种view切换方式。 29 | * 支持view on top, top click, long click监听。 30 | * 支持notify刷新数据源和重设适配器。 31 | 32 | --- 33 | 34 | ### 如何添加到项目中 35 | 36 | 本library已经支持Gradle直接添加远程依赖。Android Studio用户,只需要在项目的build.gradle中添加该dependencies: 37 | 38 | ` 39 | compile "com.missmess.coverflowview:coverflowview:1.2.8" 40 | ` 41 | 42 | --- 43 | 44 | ###如何使用 45 | 46 | 使用非常简单。仅需几句代码。用法如下: 47 | ###### 1、在xml中定义布局。 48 | ```xml 49 | 55 | ``` 56 | ###### 2、创建adapter。继承于ACoverFlowAdapter。adapter的实现与RecyclerView.Adapter完全相同。 57 | ```java 58 | public class MyCoverFlowAdapter extends ACoverFlowAdapter { 59 | @Override 60 | public int getCount() { 61 | return 0; 62 | } 63 | 64 | @Override 65 | public ViewHolder onCreateViewHolder(ViewGroup parent, int type) { 66 | return null; 67 | } 68 | 69 | @Override 70 | public void onBindViewHolder(ViewHolder vh, int position) { 71 | } 72 | 73 | class ViewHolder extends ACoverFlowAdapter.ViewHolder { 74 | public ViewHolder(View itemView) { 75 | super(itemView); 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | ###### 3、为CoverFlowView设置adapter。 82 | ```java 83 | CoverFlowView coverflow_view = (CoverFlowView) findViewById(R.id.coverflow_view); 84 | MyCoverFlowAdapter adapter = new MyCoverFlowAdapter(); 85 | coverflow_view.setAdapter(adapter); 86 | ``` 87 | --- 88 | 89 | ### 其它API 90 | ###### 1、切换CoverFlow 91 | * 你可以通过滑动切换,也可以通过点击CoverFlow的左右两侧实现快速切换。但点击切换需要确保clickSwitchEnable为 92 | true。 93 | 94 | 通过coverFlowView.setClick2SwitchEnabled(boolean enable)方法设置是否启用点击左右侧切换。 95 | 96 | * 如果你想在在代码中切换view。可以有如下的几种方法: 97 | 98 | 1. gotoPrevious():当前位置往前切换一个。 99 | 100 | 2. gotoForward():当前位置往后切换一个。 101 | 102 | 3. setSelection(int selection, boolean smooth):切换到指定的位置,smooth为true则显示过渡动画,为false 103 | 则快速切换。 104 | 105 | ###### 2、更新数据 106 | * 你可以直接使用adapter.notifyDataSetChanged()来刷新你的适配器数据。 107 | * 也可以使用新的adapter: 108 | ```java 109 | coverflow_view.setAdapter(new NewCoverFlowAdapter()); 110 | ``` 111 | 112 | ###### 3、设置监听 113 | CoverFlowView提供三种lister: 114 | 115 | * setOnViewOnTopListener:当任意一个新的view停止滑动后,位于顶部。就会调用。这个是最常用的。 116 | * setOnTopViewClickListener:当位于顶部的view被点击后调用。 117 | * setOnTopViewLongClickListener:当位于顶部的view接收了long click事件时调用。 118 | 119 | 120 | ###### 4、循环模式 121 | 使用setLoopMode(boolean)方法或者在xml中定义loopMode来启用和关闭循环模式。 122 | 在循环模式下,将可以无限向左和向右滑动,item将会循环显示。关闭后,滑动到第一个或者最后一个item,将 123 | 不能继续向左或向右滑动。 124 | 125 | 126 | --- 127 | 128 | ### 截图 129 | 130 | ![image1](https://raw.githubusercontent.com/missmess/CoverFlowView/master/raw/screenshot_1.jpg) 131 | ![image2](https://raw.githubusercontent.com/missmess/CoverFlowView/master/raw/screenshot_2.jpg) 132 | ![image3](https://raw.githubusercontent.com/missmess/CoverFlowView/master/raw/screenshot_3.jpg) 133 | 134 | --- 135 | 136 | ### 未完成工作 137 | 138 | 目前所有工作在onLayout中完成。应该把addView和transform children的工作给剥离出onLayout中。这样可以 139 | 提高性能。 140 | 141 | ### 关于作者 142 | 在使用中有任何问题,欢迎反馈给我,可以用以下联系方式跟我交流: 143 | 144 | * 邮箱: 145 | * GitHub: [@missmess](https://github.com/missmess) 146 | 147 | --- 148 | ###### CopyRight:`missmess` 149 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.0' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | jcenter() 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } 25 | -------------------------------------------------------------------------------- /coverflowview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /coverflowview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'com.jfrog.bintray' 4 | 5 | group = "com.missmess.coverflowview" 6 | version = "1.2.8" 7 | 8 | 9 | android { 10 | compileSdkVersion 23 11 | buildToolsVersion "23.0.3" 12 | 13 | defaultConfig { 14 | minSdkVersion 10 15 | targetSdkVersion 22 16 | versionCode 6 17 | versionName version 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile 'com.nineoldandroids:library:2.4.0' 29 | } 30 | 31 | def siteUrl = 'https://github.com/missmess/CoverFlowView' // project homepage 32 | def gitUrl = 'https://github.com/missmess/CoverFlowView.git' // project git 33 | 34 | install { 35 | repositories.mavenInstaller { 36 | // This generates POM.xml with proper parameters 37 | pom { 38 | project { 39 | packaging 'aar' 40 | name 'CoverFlowView For Android' // name 41 | url siteUrl 42 | licenses { 43 | license { 44 | name 'The Apache Software License, Version 2.0' 45 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 46 | } 47 | } 48 | developers { 49 | developer { 50 | id 'wang' 51 | name 'wang lei' 52 | email '478271233@qq.com' 53 | } 54 | } 55 | scm { 56 | connection gitUrl 57 | developerConnection gitUrl 58 | url siteUrl 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | task sourcesJar(type: Jar) { 66 | from android.sourceSets.main.java.srcDirs 67 | classifier = 'sources' 68 | } 69 | 70 | task javadoc(type: Javadoc) { 71 | source = android.sourceSets.main.java.srcDirs 72 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 73 | options.encoding = "utf-8" 74 | options.charSet = "utf-8" 75 | failOnError false 76 | } 77 | 78 | task javadocJar(type: Jar, dependsOn: javadoc) { 79 | classifier = 'javadoc' 80 | from javadoc.destinationDir 81 | } 82 | 83 | artifacts { 84 | archives javadocJar 85 | archives sourcesJar 86 | } 87 | 88 | Properties properties = new Properties() 89 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 90 | bintray { 91 | user = properties.getProperty("bintray.user") 92 | key = properties.getProperty("bintray.apikey") 93 | configurations = ['archives'] 94 | pkg { 95 | repo = "maven" 96 | name = "coverflowview" // project name in jcenter 97 | websiteUrl = siteUrl 98 | vcsUrl = gitUrl 99 | licenses = ["Apache-2.0"] 100 | publish = true 101 | } 102 | } -------------------------------------------------------------------------------- /coverflowview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /coverflowview/src/main/java/com/missmess/coverflowview/ACoverFlowAdapter.java: -------------------------------------------------------------------------------- 1 | package com.missmess.coverflowview; 2 | 3 | import android.database.DataSetObservable; 4 | import android.database.DataSetObserver; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | 11 | public abstract class ACoverFlowAdapter { 12 | private final DataSetObservable mDataSetObservable = new DataSetObservable(); 13 | 14 | public void registerDataSetObserver(DataSetObserver observer) { 15 | mDataSetObservable.registerObserver(observer); 16 | } 17 | 18 | public void unregisterDataSetObserver(DataSetObserver observer) { 19 | mDataSetObservable.unregisterObserver(observer); 20 | } 21 | 22 | /** 23 | * CoverFlowView数据集发生改变,刷新,尽量保留状态且不影响滑动事件 24 | */ 25 | public void notifyDataSetChanged() { 26 | mDataSetObservable.notifyChanged(); 27 | } 28 | 29 | /** 30 | * CoverFlowView重绘刷新。重置CoverFlowView的所有状态。 31 | */ 32 | public void notifyDataSetInvalidated() { 33 | mDataSetObservable.notifyInvalidated(); 34 | } 35 | 36 | public Object getItem(int position){ 37 | return position; 38 | } 39 | 40 | public long getItemId(int position){ 41 | return position; 42 | } 43 | 44 | protected View getView(int position, View convertView, ViewGroup parent) { 45 | T holder; 46 | int itemViewType = getItemViewType(position); 47 | if(convertView != null) { 48 | Object tag = convertView.getTag(); 49 | // 当前adapter的泛型类型T 50 | Type destType = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; 51 | // 验证tag是否是T的实例,如果是,直接使用 52 | if (((Class) destType).isInstance(tag)) { 53 | holder = (T) tag; 54 | int cPos = holder.position; 55 | if(getItemViewType(cPos) == itemViewType) { 56 | onBindViewHolder(holder, position); 57 | return holder.getItemView(); 58 | } 59 | } 60 | } 61 | 62 | holder = onCreateViewHolder(parent, itemViewType); 63 | holder.position = position; 64 | holder.getItemView().setTag(holder); 65 | 66 | onBindViewHolder(holder, position); 67 | return holder.getItemView(); 68 | } 69 | 70 | protected void refreshView(View view, int position) { 71 | T holder = (T) view.getTag(); 72 | onBindViewHolder(holder, position); 73 | } 74 | 75 | public int getItemViewType(int position) { 76 | return 0; 77 | } 78 | 79 | /** 80 | * 总item数 81 | */ 82 | public abstract int getCount(); 83 | 84 | public abstract T onCreateViewHolder(ViewGroup parent, int type); 85 | 86 | public abstract void onBindViewHolder(T vh, int position); 87 | 88 | public static class ViewHolder { 89 | private View itemView; 90 | int position; 91 | public ViewHolder(View itemView) { 92 | this.itemView = itemView; 93 | } 94 | 95 | public View getItemView() { 96 | return itemView; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /coverflowview/src/main/java/com/missmess/coverflowview/CoverFlowView.java: -------------------------------------------------------------------------------- 1 | package com.missmess.coverflowview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.database.DataSetObserver; 6 | import android.graphics.Rect; 7 | import android.util.AttributeSet; 8 | import android.util.SparseArray; 9 | import android.view.MotionEvent; 10 | import android.view.VelocityTracker; 11 | import android.view.View; 12 | import android.view.ViewConfiguration; 13 | import android.view.ViewGroup; 14 | import android.view.animation.AccelerateDecelerateInterpolator; 15 | import android.view.animation.AnimationUtils; 16 | 17 | import com.nineoldandroids.animation.Animator; 18 | import com.nineoldandroids.animation.AnimatorListenerAdapter; 19 | import com.nineoldandroids.animation.ValueAnimator; 20 | import com.nineoldandroids.view.ViewHelper; 21 | 22 | import java.util.Locale; 23 | 24 | /** 25 | * 将一组view以CoverFlow效果展示出来。 26 | * *

目前的实现是使用{@link #mOffset}来对应滑动量,所有的操作都会触发onLayout方法的调用。在onLayout方法中做view的添加, 27 | * 移除,变换等所有操作。

28 | *

29 | *

原理:假设一屏可显示的view数目为totalVis = 2 * sib + 1(始终为奇数)。左右两边最多显示sib个 30 | * view。adapter的个数为count: 31 | *

    32 | *
  1. adapter中的item将按顺序排列在CoverFlowView中。初始显示的一组view为adapter中position为 33 | * 0~totalVis的item,并且最中间的view设index = 0,即index=0对应position=sib。 34 | * 参考 {@link #convertIndex2Position(int)}
  2. 35 | *
  3. index从左到右增大。在loop模式下,index没有取值范围。在非loop模式,index取值范围 36 | * 为[-sib, count-sib-1]
  4. 37 | *
  5. 内部使用{@link #showViewArray}存储正显示在CoverFlowView上的item。这个map以 38 | * adapter position作为key存储view
  6. 39 | *
  7. CoverFlowView通过{@link #addView(View, int)}的index order来控制CoverFlow的层叠效果。 40 | * 最中间的view的index order最大,越往两边index order越小。
  8. 41 | *
  9. CoverFlowView发生任何移动时{@link #mOffset}都会改变,代表偏移量(浮点数)。滑动停止时, 42 | * {@link #mOffset}=index
  10. 43 | *
  11. 每当滑动到两个view之间的一半(offset值为0.5)时,将会移除一个边界view(往左滑移除右边界)和获取一个新的 44 | * 显示view,并且调整view的index order以保持正确的层叠效果,参见{@link #onLayout(boolean, int, int, int, int)}
  12. 45 | *
46 | *

47 | * 48 | */ 49 | public class CoverFlowView extends ViewGroup { 50 | 51 | public enum CoverFlowGravity { 52 | TOP, BOTTOM, CENTER_VERTICAL 53 | } 54 | 55 | public enum CoverFlowLayoutMode { 56 | MATCH_PARENT, WRAP_CONTENT 57 | } 58 | 59 | /** 60 | * The CoverFlowView is not currently scrolling. 61 | * 62 | * @see #getScrollState() 63 | */ 64 | public static final int SCROLL_STATE_IDLE = 0; 65 | 66 | /** 67 | * The CoverFlowView is currently being dragged by outside input such as user touch input. 68 | * 69 | * @see #getScrollState() 70 | */ 71 | public static final int SCROLL_STATE_DRAGGING = 1; 72 | 73 | /** 74 | * The CoverFlowView is currently animating to a final position while not under 75 | * outside control. 76 | * 77 | * @see #getScrollState() 78 | */ 79 | public static final int SCROLL_STATE_SETTLING = 2; 80 | 81 | private int mScrollState = SCROLL_STATE_IDLE; 82 | 83 | protected CoverFlowGravity mGravity; 84 | protected CoverFlowLayoutMode mLayoutMode; 85 | 86 | /** 87 | * 显示在CoverFlowView上的所有view的map,其中key为对应的adapter position 88 | */ 89 | private SparseArray showViewArray; 90 | // 一屏显示的数量 91 | private int mVisibleChildCount; 92 | // 左右两边显示的个数 93 | protected int mSiblingCount = 1; 94 | private float mOffset = 0f; 95 | private int mExpectedAdapterCount = 0; 96 | 97 | private int paddingLeft; 98 | private int paddingRight; 99 | private int paddingTop; 100 | private int paddingBottom; 101 | private float reflectHeightFraction = 0; 102 | private int reflectGap = 0; 103 | private int mWidth; // 控件的宽度 104 | private int mChildHeight; // child的高度 105 | private int mChildTranslateY; 106 | 107 | // 基础alphaֵ 108 | private static final int ALPHA_DATUM = 76; 109 | // 临界view alpha 110 | private int STANDARD_ALPHA; 111 | 112 | // 临界view的scale比例,因为是反向取值,所以这个值越大,临界view会越小 113 | private float mEdgeScale; 114 | private static final float MIN_SCALE = 0f; 115 | private static final float MAX_SCALE = 0.8f; 116 | private static final float DEFAULT_SCALE = 0.25f; 117 | 118 | private static float MOVE_POS_MULTIPLE = 3.0f; 119 | private static final float MOVE_SPEED_MULTIPLE = 1; 120 | private static final float MAX_SPEED = 6.0f; 121 | private static final float FRICTION = 10.0f; 122 | private static int mTouchSlop; 123 | 124 | private ACoverFlowAdapter mAdapter; 125 | private AdapterDataSetObserver mDataSetObserver; 126 | private OnTopViewClickListener mTopViewClickLister; 127 | private OnViewOnTopListener mViewOnTopListener; 128 | private OnTopViewLongClickListener mTopViewLongClickLister; 129 | 130 | private boolean mDataChanged = false; 131 | 132 | // loop 模式 133 | private boolean mLoopMode; 134 | // 即使在loop模式下,不一定我们就支持loop(如当adapter数量小于可见数目时) 135 | private boolean mShouldLoop; 136 | int lastMidIndex = -1; //最近的中间view的offset值,滑动时改变 137 | int mViewOnTopPosition = -1; // view处于顶部选中状态的的position,-1代表无 138 | 139 | private View touchViewItem = null; 140 | private boolean isOnTopView = false; 141 | 142 | private Runnable longClickRunnable = null; 143 | 144 | private boolean mTouchCanceled = false; 145 | private int mScrollPointerId; 146 | private float mTouchStartPos; 147 | private float mTouchStartX; 148 | private float mTouchStartY; 149 | 150 | private long mStartTime; 151 | private float mStartOffset; 152 | private float mStartSpeed; 153 | private float mDuration; 154 | 155 | private Runnable mSettleAnimationRunnable; 156 | private boolean clickSwitchEnable = true; 157 | private ValueAnimator mScrollAnimator; 158 | private VelocityTracker mVelocity; 159 | 160 | // index的一个有效的取值范围,loop时代表初始的取值范围 161 | private int mLeftEdgeIndex, mRightEdgeIndex; 162 | // index与position之间的通常的一个差值 163 | private int mGapBetweenPositionAndIndex; 164 | private boolean isLaidOut; 165 | 166 | public CoverFlowView(Context context) { 167 | super(context); 168 | init(); 169 | } 170 | 171 | public CoverFlowView(Context context, AttributeSet attrs) { 172 | super(context, attrs); 173 | initAttributes(context, attrs); 174 | init(); 175 | } 176 | 177 | public CoverFlowView(Context context, AttributeSet attrs, int defStyle) { 178 | super(context, attrs, defStyle); 179 | initAttributes(context, attrs); 180 | init(); 181 | } 182 | 183 | private void initAttributes(Context context, AttributeSet attrs) { 184 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoverFlowView); 185 | 186 | int totalVisibleChildren = a.getInt( 187 | R.styleable.CoverFlowView_visibleViews, 3); 188 | if (totalVisibleChildren % 2 == 0) { // 一屏幕必须是奇数显示 189 | throw new IllegalArgumentException( 190 | "visible views must be an odd number"); 191 | } 192 | if (totalVisibleChildren < 3) { 193 | throw new IllegalArgumentException( 194 | "visible views must be a number greater than 3"); 195 | } 196 | 197 | mSiblingCount = totalVisibleChildren >> 1; // 计算出左右两两边的显示个数 198 | mVisibleChildCount = totalVisibleChildren; 199 | 200 | mShouldLoop = mLoopMode = a.getBoolean(R.styleable.CoverFlowView_loopMode, true); 201 | 202 | float scaleRatio = a.getFraction(R.styleable.CoverFlowView_scaleRatio, 1, 1, -1f); 203 | if (scaleRatio == -1) { 204 | mEdgeScale = DEFAULT_SCALE; 205 | } else { 206 | mEdgeScale = (MAX_SCALE - MIN_SCALE) * scaleRatio + MIN_SCALE; 207 | } 208 | 209 | mGravity = CoverFlowGravity.values()[a.getInt( 210 | R.styleable.CoverFlowView_coverflowGravity, 211 | CoverFlowGravity.CENTER_VERTICAL.ordinal())]; 212 | 213 | mLayoutMode = CoverFlowLayoutMode.values()[a.getInt( 214 | R.styleable.CoverFlowView_coverflowLayoutMode, 215 | CoverFlowLayoutMode.WRAP_CONTENT.ordinal())]; 216 | 217 | a.recycle(); 218 | } 219 | 220 | private void init() { 221 | setWillNotDraw(true); 222 | setClickable(true); 223 | 224 | final ViewConfiguration vc = ViewConfiguration.get(getContext()); 225 | mTouchSlop = vc.getScaledTouchSlop(); 226 | 227 | if (showViewArray == null) { 228 | showViewArray = new SparseArray<>(); 229 | } 230 | 231 | // 计算透明度 232 | STANDARD_ALPHA = (255 - ALPHA_DATUM) / mSiblingCount; 233 | 234 | if (mGravity == null) { 235 | mGravity = CoverFlowGravity.CENTER_VERTICAL; 236 | } 237 | if (mLayoutMode == null) { 238 | mLayoutMode = CoverFlowLayoutMode.WRAP_CONTENT; 239 | } 240 | 241 | resetOffset(); 242 | } 243 | 244 | private void resetOffset() { 245 | mOffset = 0f; 246 | lastMidIndex = 0; 247 | mStartOffset = 0f; 248 | } 249 | 250 | /** 251 | * 调整offset偏移量为delta,用来保证触摸事件保持连贯。 252 | * 253 | * @param delta 偏移量 254 | */ 255 | private void justifyOffset(int delta) { 256 | mOffset += (float) delta; 257 | lastMidIndex = (int) Math.floor(mOffset + 0.5); 258 | mStartOffset += (float) delta; 259 | } 260 | 261 | /** 262 | * 创建children view,添加到控件中,并缓存在集合中。 263 | * 264 | * @param inLayout 是否在onLayout中调用 265 | * @param midAdapterPosition 中间的view对应的adapter position 266 | */ 267 | private void applyLayoutChildren(boolean inLayout, int midAdapterPosition) { 268 | // Log.v("CoverFlowView", "applyLayoutChildren:mid position=" + midAdapterPosition); 269 | if (inLayout) { 270 | removeAllViewsInLayout(); 271 | } else { 272 | removeAllViews(); 273 | } 274 | ACoverFlowAdapter adapter = mAdapter; 275 | 276 | if (adapter != null) { 277 | assertAdapterDataSetValidate(); 278 | 279 | int count = adapter.getCount(); 280 | if (count == 0) 281 | return; 282 | if (midAdapterPosition == -1) 283 | return; 284 | 285 | SparseArray temp = new SparseArray<>(); 286 | for (int i = 0, j = (midAdapterPosition - mSiblingCount); i < mVisibleChildCount; ++i, ++j) { 287 | View convertView; 288 | int position = -1; 289 | View view; 290 | if (j < 0) { 291 | if (mShouldLoop) { 292 | position = count + j; 293 | 294 | } 295 | } else if (j >= count) { 296 | if (mShouldLoop) { 297 | position = j - count; 298 | } 299 | } else { 300 | position = j; 301 | } 302 | 303 | if (position != -1) { //需要获取view 304 | convertView = showViewArray.get(position); 305 | view = mAdapter.getView(position, convertView, this); 306 | temp.put(position, view); 307 | 308 | //按Z轴顺序添加view,保持层叠效果 309 | int zpos; 310 | if (i <= mSiblingCount) { 311 | zpos = -1; 312 | } else { 313 | zpos = 0; 314 | } 315 | if (inLayout) { 316 | LayoutParams params = view.getLayoutParams(); 317 | if (params == null) { 318 | params = generateDefaultLayoutParams(); 319 | } 320 | addViewInLayout(view, zpos, params); 321 | } else { 322 | addView(view, zpos); 323 | } 324 | } 325 | } 326 | 327 | showViewArray.clear(); 328 | showViewArray = temp; 329 | 330 | if (inLayout) { 331 | // 如果在layout进程中,不调用此句不会刷新界面? 332 | // 不太清楚原因 333 | post(new Runnable() { 334 | @Override 335 | public void run() { 336 | requestLayout(); 337 | } 338 | }); 339 | } 340 | } 341 | } 342 | 343 | private void assertAdapterDataSetValidate() { 344 | if (mAdapter == null) 345 | return; 346 | 347 | int count = mAdapter.getCount(); 348 | if (count != mExpectedAdapterCount) { 349 | throw new IllegalStateException("The ACoverFlowAdapter changed the adapter's" + 350 | " contents without calling ACoverFlowAdapter#notifyDataSetChanged!" + 351 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + count + 352 | " Pager class: " + getClass() + 353 | " Problematic adapter: " + mAdapter.getClass()); 354 | } 355 | } 356 | 357 | private void checkShouldLoopAndEdge() { 358 | if (mAdapter == null) 359 | return; 360 | 361 | int count = mAdapter.getCount(); 362 | if (count < mVisibleChildCount) { 363 | // 条数不足,无法正常处理loop 364 | mShouldLoop = false; 365 | 366 | int left = count >> 1; 367 | mGapBetweenPositionAndIndex = left; 368 | mLeftEdgeIndex = -left; 369 | mRightEdgeIndex = mLeftEdgeIndex + count - 1; 370 | } else { 371 | mShouldLoop = mLoopMode; 372 | 373 | mGapBetweenPositionAndIndex = mSiblingCount; 374 | mLeftEdgeIndex = -mSiblingCount; 375 | mRightEdgeIndex = mLeftEdgeIndex + count - 1; 376 | } 377 | if (mRightEdgeIndex < mLeftEdgeIndex) 378 | mRightEdgeIndex = mLeftEdgeIndex; 379 | } 380 | 381 | @Override 382 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 383 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 384 | 385 | if (mAdapter == null || showViewArray.size() <= 0) { 386 | return; 387 | } 388 | 389 | paddingLeft = getPaddingLeft(); 390 | paddingRight = getPaddingRight(); 391 | paddingTop = getPaddingTop(); 392 | paddingBottom = getPaddingBottom(); 393 | 394 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 395 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 396 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 397 | 398 | // 控件高度 399 | int avaiblableHeight = heightSize - paddingTop - paddingBottom; 400 | 401 | int maxChildTotalHeight = 0; 402 | for (int i = 0; i < getChildCount() && i < mVisibleChildCount && i < showViewArray.size(); ++i) { 403 | 404 | // View view = showViewArray.get(i+firstIndex); 405 | View view = getChildAt(i); 406 | measureChild(view, widthMeasureSpec, heightMeasureSpec); 407 | 408 | // final int childHeight = ScreenUtil.dp2px(getContext(), 110); 409 | final int childHeight = view.getMeasuredHeight(); 410 | final int childTotalHeight = (int) (childHeight + childHeight 411 | * reflectHeightFraction + reflectGap); 412 | 413 | // 孩子的最大高度 414 | maxChildTotalHeight = (maxChildTotalHeight < childTotalHeight) ? childTotalHeight 415 | : maxChildTotalHeight; 416 | } 417 | 418 | // 如果控件模式为确切值 或者 最大是 419 | if (heightMode == MeasureSpec.EXACTLY 420 | || heightMode == MeasureSpec.AT_MOST) { 421 | // if height which parent provided is less than child need, scale 422 | // child height to parent provide 423 | // 如果控件高度小于孩子控件高度 则缩放孩子高度为控件高度 424 | if (avaiblableHeight < maxChildTotalHeight) { 425 | mChildHeight = avaiblableHeight; 426 | } else { 427 | // if larger than, depends on layout mode 428 | // if layout mode is match_parent, scale child height to parent 429 | // provide 430 | // 如果是填充父窗体模式 则将孩子的高度 设为控件高度 431 | if (mLayoutMode == CoverFlowLayoutMode.MATCH_PARENT) { 432 | mChildHeight = avaiblableHeight; 433 | // if layout mode is wrap_content, keep child's original 434 | // height 435 | // 如果是包裹内容 则将孩子的高度设为孩子允许的最大高度 436 | } else if (mLayoutMode == CoverFlowLayoutMode.WRAP_CONTENT) { 437 | mChildHeight = maxChildTotalHeight; 438 | 439 | // adjust parent's height 440 | // 计算出控件的高度 441 | if (heightMode == MeasureSpec.AT_MOST) { 442 | heightSize = mChildHeight + paddingTop + paddingBottom; 443 | } 444 | } 445 | } 446 | } else { 447 | // height mode is unspecified 448 | // 如果空间高度 没有明确定义 449 | // 如果孩子的模式为填充父窗体 450 | if (mLayoutMode == CoverFlowLayoutMode.MATCH_PARENT) { 451 | mChildHeight = avaiblableHeight; 452 | // 如果孩子的模式为包裹内容 453 | } else if (mLayoutMode == CoverFlowLayoutMode.WRAP_CONTENT) { 454 | mChildHeight = maxChildTotalHeight; 455 | 456 | // 计算出控件的高度 457 | // adjust parent's height 458 | heightSize = mChildHeight + paddingTop + paddingBottom; 459 | } 460 | } 461 | 462 | // Adjust movement in y-axis according to gravity 463 | // 计算出孩子的原点 Y坐标 464 | if (mGravity == CoverFlowGravity.CENTER_VERTICAL) {// 竖直居中 465 | mChildTranslateY = (heightSize >> 1) - (mChildHeight >> 1); 466 | } else if (mGravity == CoverFlowGravity.TOP) {// 顶部对齐 467 | mChildTranslateY = paddingTop; 468 | } else if (mGravity == CoverFlowGravity.BOTTOM) {// 底部对齐 469 | mChildTranslateY = heightSize - paddingBottom - mChildHeight; 470 | } 471 | 472 | // mReflectionTranslateY = (int) (mChildTranslateY + mChildHeight - mChildHeight 473 | // * reflectHeightFraction); 474 | 475 | setMeasuredDimension(widthSize, heightSize); 476 | mWidth = widthSize; 477 | } 478 | 479 | @Override 480 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 481 | // Log.v("CoverFlowView", "onLayout"); 482 | ACoverFlowAdapter adapter = mAdapter; 483 | float offset = mOffset; 484 | int mid = (int) Math.floor(offset + 0.5); 485 | //右边孩子的数量 486 | int rightCount = mSiblingCount; 487 | //左边孩子的数量 488 | int leftCount = mSiblingCount; 489 | 490 | if (mDataChanged) { 491 | // 数据改变时,检查可否loop,计算index边界值 492 | checkShouldLoopAndEdge(); 493 | 494 | int adapterPosition = convertIndex2Position(mid); 495 | applyLayoutChildren(true, adapterPosition); 496 | mDataChanged = false; 497 | } else { 498 | if (lastMidIndex + 1 == mid) { //右滑至item出现了 499 | assertAdapterDataSetValidate(); 500 | int actuallyPositionStart = convertIndex2Position(lastMidIndex - leftCount); 501 | View view = null; 502 | if (actuallyPositionStart != -1) { 503 | view = showViewArray.get(actuallyPositionStart); 504 | showViewArray.remove(actuallyPositionStart); 505 | removeViewInLayout(view); 506 | } 507 | 508 | // 非loop模式下,index<=mRightEdgeIndex。所以只有当mid<=mRightEdgeIndex-sib时,才去获取最右边的view 509 | boolean toGet = mid <= mRightEdgeIndex - mSiblingCount; 510 | if (mShouldLoop || toGet) { 511 | int actuallyPositionEnd = convertIndex2Position(mid + rightCount); 512 | View viewItem = adapter.getView(actuallyPositionEnd, view, this); 513 | showViewArray.put(actuallyPositionEnd, viewItem); 514 | LayoutParams params = viewItem.getLayoutParams(); 515 | if (params == null) { 516 | params = generateDefaultLayoutParams(); 517 | } 518 | addViewInLayout(viewItem, 0, params); 519 | } 520 | 521 | int actuallyPositionMid = convertIndex2Position(mid); 522 | if (actuallyPositionMid != -1) { 523 | View midView = showViewArray.get(actuallyPositionMid); 524 | midView.bringToFront(); 525 | } 526 | } else if (lastMidIndex - 1 == mid) { //左滑至item出现了 527 | assertAdapterDataSetValidate(); 528 | int actuallyPositionEnd = convertIndex2Position(lastMidIndex + rightCount); 529 | View view = null; 530 | if (actuallyPositionEnd != -1) { 531 | view = showViewArray.get(actuallyPositionEnd); 532 | showViewArray.remove(actuallyPositionEnd); 533 | removeViewInLayout(view); 534 | } 535 | 536 | // 非loop模式下,index>=mLeftEdgeIndex。所以mid>=mLeftEdgeIndex+sib时,才去获取最左边的view 537 | boolean toGet = mid >= mLeftEdgeIndex + mSiblingCount; 538 | if (mShouldLoop || toGet) { 539 | int actuallyPositionStart = convertIndex2Position(mid - leftCount); 540 | View viewItem = adapter.getView(actuallyPositionStart, view, this); 541 | showViewArray.put(actuallyPositionStart, viewItem); 542 | LayoutParams params = viewItem.getLayoutParams(); 543 | if (params == null) { 544 | params = generateDefaultLayoutParams(); 545 | } 546 | addViewInLayout(viewItem, 0, params); 547 | } 548 | 549 | int actuallyPositionMid = convertIndex2Position(mid); 550 | if (actuallyPositionMid != -1) { 551 | View midView = showViewArray.get(actuallyPositionMid); 552 | midView.bringToFront(); 553 | } 554 | } 555 | } 556 | 557 | lastMidIndex = mid; 558 | 559 | int i; 560 | // draw the left children 561 | // 计算左边孩子的位置 562 | int startPos = mid - leftCount; 563 | for (i = startPos; i < mid; ++i) { 564 | layoutLeftChild(i, i - offset); 565 | } 566 | 567 | // 计算 右边 和 中间 568 | int endPos = mid + rightCount; 569 | for (i = endPos; i >= mid; i--) { 570 | layoutRightChild(i, i - offset); 571 | } 572 | 573 | postViewOnTop(); 574 | if (!isLaidOut) { 575 | isLaidOut = true; 576 | } 577 | } 578 | 579 | private void postViewOnTop() { 580 | final float offset = mOffset; 581 | if ((offset - (int) offset) == 0.0f) { 582 | int top = convertIndex2Position((int) offset); 583 | if (showViewArray.indexOfKey(top) == -1) // 不存在view 584 | return; 585 | 586 | if (top != mViewOnTopPosition) { 587 | mViewOnTopPosition = top; 588 | post(new Runnable() { 589 | @Override 590 | public void run() { 591 | if (mViewOnTopListener != null) 592 | mViewOnTopListener.viewOnTop(mViewOnTopPosition, getTopView()); 593 | } 594 | }); 595 | } 596 | } 597 | } 598 | 599 | private View layoutLeftChild(int position, float offset) { 600 | //获取实际的position 601 | int actuallyPosition = convertIndex2Position(position); 602 | View child = showViewArray.get(actuallyPosition); 603 | if (child != null) { 604 | makeChildTransformer(child, actuallyPosition, offset); 605 | } 606 | return child; 607 | } 608 | 609 | private View layoutRightChild(int position, float offset) { 610 | //获取实际的position 611 | int actuallyPosition = convertIndex2Position(position); 612 | View child = showViewArray.get(actuallyPosition); 613 | if (child != null) { 614 | makeChildTransformer(child, actuallyPosition, offset); 615 | } 616 | 617 | return child; 618 | } 619 | 620 | private void makeChildTransformer(View child, int position, float offset) { 621 | child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); 622 | 623 | float scale; 624 | scale = 1 - Math.abs(offset) * mEdgeScale; 625 | 626 | 627 | // 延x轴移动的距离应该根据center图片决定 628 | float translateX; 629 | 630 | final int originalChildHeight = (int) (mChildHeight - mChildHeight 631 | * reflectHeightFraction - reflectGap); 632 | 633 | final float originalChildHeightScale = (float) originalChildHeight 634 | / child.getHeight(); 635 | 636 | final float childHeightScale = originalChildHeightScale * scale; 637 | 638 | final int childWidth = (int) (child.getWidth() * childHeightScale); 639 | 640 | final int centerChildWidth = (int) (child.getWidth() * originalChildHeightScale); 641 | 642 | int leftSpace = ((mWidth >> 1) - paddingLeft) - (centerChildWidth >> 1); 643 | int rightSpace = (((mWidth >> 1) - paddingRight) - (centerChildWidth >> 1)); 644 | 645 | //计算出水平方向的x坐标 646 | if (offset <= 0) 647 | translateX = ((float) leftSpace / mSiblingCount) 648 | * (mSiblingCount + offset) + paddingLeft; 649 | else 650 | translateX = mWidth - ((float) rightSpace / mSiblingCount) 651 | * (mSiblingCount - offset) - childWidth 652 | - paddingRight; 653 | 654 | //根据offset 算出透明度 655 | float alpha = 254 - Math.abs(offset) * STANDARD_ALPHA; 656 | ViewHelper.setAlpha(child, 0); 657 | // child.setAlpha(0); 658 | if (alpha < 0) { 659 | alpha = 0; 660 | } else if (alpha > 254) { 661 | alpha = 254; 662 | } 663 | ViewHelper.setAlpha(child, alpha / 254.0f); 664 | // child.setAlpha(alpha/254.0f); 665 | 666 | float adjustedChildTranslateY = 0; 667 | 668 | // //兼容api 10 669 | // if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { 670 | // android.animation.ObjectAnimator anim1 = android.animation.ObjectAnimator.ofFloat(child, "scaleX", 671 | // 1.0f, childHeightScale); 672 | // android.animation.ObjectAnimator anim2 = android.animation.ObjectAnimator.ofFloat(child, "scaleY", 673 | // 1.0f, childHeightScale); 674 | // android.animation.AnimatorSet animSet = new android.animation.AnimatorSet(); 675 | // animSet.setDuration(0); 676 | // //两个动画同时执行 677 | // animSet.playTogether(anim1, anim2); 678 | // 679 | // //显示的调用invalidate 680 | //// child.invalidate(); 681 | // animSet.setTarget(child); 682 | // animSet.start(); 683 | // } else { 684 | // ObjectAnimator anim1 = ObjectAnimator.ofFloat(child, "scaleX", 685 | // 1.0f, childHeightScale); 686 | // ObjectAnimator anim2 = ObjectAnimator.ofFloat(child, "scaleY", 687 | // 1.0f, childHeightScale); 688 | // AnimatorSet animSet = new AnimatorSet(); 689 | // animSet.setDuration(0); 690 | // //两个动画同时执行 691 | // animSet.playTogether(anim1, anim2); 692 | // 693 | // //显示的调用invalidate 694 | //// child.invalidate(); 695 | // animSet.setTarget(child); 696 | // animSet.start(); 697 | // } 698 | float sc = childHeightScale; 699 | ViewHelper.setScaleX(child, sc); 700 | ViewHelper.setScaleY(child, sc); 701 | 702 | ViewHelper.setPivotX(child, 0); 703 | ViewHelper.setPivotY(child, child.getHeight() / 2); 704 | // child.setPivotX(0); 705 | // child.setPivotY(child.getHeight()/2); 706 | 707 | ViewHelper.setTranslationX(child, translateX); 708 | ViewHelper.setTranslationY(child, mChildTranslateY + adjustedChildTranslateY); 709 | // child.setTranslationX(translateX); 710 | // child.setTranslationY(mChildTranslateY+ adjustedChildTranslateY); 711 | } 712 | 713 | /** 714 | * 获取顶部Item position 715 | * 716 | * @return int, -1表示没有 717 | */ 718 | public int getTopViewPosition() { 719 | return convertIndex2Position(lastMidIndex); 720 | } 721 | 722 | /** 723 | * 获取顶部Item View 724 | * 725 | * @return top view 726 | */ 727 | public View getTopView() { 728 | return showViewArray.get(getTopViewPosition()); 729 | } 730 | 731 | /** 732 | * Convert our index to adapter position. 733 | * 734 | * @param index index in CoverFlowView 735 | * @return adapter position, -1 for illegal index 736 | */ 737 | private int convertIndex2Position(int index) { 738 | if (mAdapter == null) { 739 | return index + mGapBetweenPositionAndIndex; 740 | } 741 | 742 | return convertIndex2Position(index, mAdapter.getCount()); 743 | } 744 | 745 | private int convertIndex2Position(int index, int count) { 746 | if (!isLaidOut) 747 | return index; 748 | int position = index + mGapBetweenPositionAndIndex; 749 | 750 | if (mShouldLoop) { 751 | while (position < 0 || position >= count) { 752 | if (position < 0) { 753 | position += count; 754 | } else if (position >= count) { 755 | position -= count; 756 | } 757 | } 758 | } else { 759 | if (position < 0 || position >= count) { 760 | position = -1; 761 | } 762 | } 763 | 764 | return position; 765 | } 766 | 767 | private int convertPosition2Index(int position) { 768 | return position - mGapBetweenPositionAndIndex; 769 | } 770 | 771 | public void setAdapter(ACoverFlowAdapter adapter) { 772 | ACoverFlowAdapter old = mAdapter; 773 | if (old != null) { 774 | old.unregisterDataSetObserver(mDataSetObserver); 775 | } 776 | 777 | stopScroll(); 778 | cancelTouch(); 779 | resetOffset(); 780 | 781 | mExpectedAdapterCount = 0; 782 | 783 | mAdapter = adapter; 784 | if (adapter != null) { 785 | if (mDataSetObserver == null) { 786 | mDataSetObserver = new AdapterDataSetObserver(); 787 | } 788 | adapter.registerDataSetObserver(mDataSetObserver); 789 | mExpectedAdapterCount = adapter.getCount(); 790 | } 791 | 792 | onAdapterChanged(old, adapter); 793 | mDataChanged = true; 794 | requestLayout(); 795 | } 796 | 797 | public ACoverFlowAdapter getAdapter() { 798 | return mAdapter; 799 | } 800 | 801 | private void onAdapterChanged(ACoverFlowAdapter oldAdapter, ACoverFlowAdapter newAdapter) { 802 | // 如果adapter 改变了,view缓存就过期了。清空 803 | showViewArray.clear(); 804 | } 805 | 806 | @Override 807 | public boolean onInterceptTouchEvent(MotionEvent event) { 808 | if (getParent() != null) { 809 | getParent().requestDisallowInterceptTouchEvent(true); 810 | } 811 | 812 | boolean handled = false; 813 | int action = event.getAction(); 814 | switch (action) { 815 | case MotionEvent.ACTION_DOWN: 816 | touchViewItem = getTopView(); 817 | isOnTopView = false; 818 | if (touchViewItem != null) { 819 | isOnTopView = inRangeOfView(touchViewItem, event); 820 | } 821 | // 如果触摸位置不在顶部view内,直接消费这个event。 822 | handled = !isOnTopView; 823 | break; 824 | } 825 | return handled; 826 | } 827 | 828 | @Override 829 | public boolean onTouchEvent(MotionEvent event) { 830 | // Log.d("CoverFlowView", event.toString()); 831 | int action = event.getActionMasked(); 832 | switch (action) { 833 | case MotionEvent.ACTION_DOWN: 834 | // 注意,只在CoverFlowView拦截的情况下执行 835 | // 如果正在settling,停止settle,改为触摸。 836 | if (mScrollState == SCROLL_STATE_SETTLING) { 837 | setScrollState(SCROLL_STATE_DRAGGING); 838 | } 839 | mTouchCanceled = false; 840 | 841 | touchBegan(event); 842 | 843 | if (isOnTopView) { 844 | sendLongClickAction(); 845 | } 846 | break; 847 | case MotionEvent.ACTION_POINTER_DOWN: 848 | touchPointDown(event); 849 | break; 850 | case MotionEvent.ACTION_MOVE: 851 | if (mTouchCanceled) { 852 | break; 853 | } 854 | 855 | touchMoved(event); 856 | 857 | boolean startScroll = false; 858 | if (Math.abs(mTouchStartX - event.getX()) > mTouchSlop 859 | || Math.abs(mTouchStartY - event.getY()) > mTouchSlop) { 860 | removeLongClickAction(); 861 | touchViewItem = null; 862 | isOnTopView = false; 863 | startScroll = true; 864 | } 865 | 866 | if (mScrollState != SCROLL_STATE_DRAGGING) { 867 | if (startScroll) { 868 | setScrollState(SCROLL_STATE_DRAGGING); 869 | } 870 | } 871 | 872 | break; 873 | case MotionEvent.ACTION_POINTER_UP: 874 | touchPointUp(event); 875 | break; 876 | case MotionEvent.ACTION_CANCEL: 877 | case MotionEvent.ACTION_UP: 878 | if (mTouchCanceled) { 879 | break; 880 | } 881 | 882 | removeLongClickAction(); 883 | View topView = getTopView(); 884 | if (isOnTopView && topView != null && touchViewItem == topView 885 | && inRangeOfView(touchViewItem, event)) { 886 | if (mTopViewClickLister != null) { 887 | mTopViewClickLister.onClick(getTopViewPosition(), topView); 888 | } 889 | } 890 | 891 | touchViewItem = null; 892 | isOnTopView = false; 893 | touchEnded(event); 894 | 895 | //不是点击top view。并且启用点击切换。并且点击了左右侧view 896 | if (!isOnTopView && clickSwitchEnable && topView != null 897 | && Math.abs(mTouchStartX - event.getX()) < mTouchSlop 898 | && Math.abs(mTouchStartY - event.getY()) < mTouchSlop 899 | && event.getEventTime() - event.getDownTime() < 500) { 900 | if (atLeftOfView(topView, event)) { 901 | gotoPrevious(); 902 | } else if (atRightOfView(topView, event)) { 903 | gotoForward(); 904 | } 905 | } 906 | 907 | break; 908 | } 909 | 910 | return !mTouchCanceled; 911 | } 912 | 913 | private void cancelTouch() { 914 | mTouchCanceled = true; 915 | setScrollState(SCROLL_STATE_IDLE); 916 | // 移除long click 917 | removeLongClickAction(); 918 | } 919 | 920 | private void sendLongClickAction() { 921 | removeLongClickAction(); 922 | longClickRunnable = new Runnable() { 923 | @Override 924 | public void run() { 925 | touchViewItem = null; 926 | isOnTopView = false; 927 | if (mTopViewLongClickLister != null) { 928 | mTopViewLongClickLister.onLongClick(getTopViewPosition(), getTopView()); 929 | } 930 | } 931 | }; 932 | postDelayed(longClickRunnable, 600L); 933 | } 934 | 935 | private void removeLongClickAction() { 936 | if (longClickRunnable != null) { 937 | removeCallbacks(longClickRunnable); 938 | longClickRunnable = null; 939 | } 940 | } 941 | 942 | private void touchBegan(MotionEvent event) { 943 | endSettleAnimation(); 944 | 945 | float x = event.getX(); 946 | mScrollPointerId = event.getPointerId(0); 947 | mTouchStartX = x; 948 | mTouchStartY = event.getY(); 949 | mStartTime = AnimationUtils.currentAnimationTimeMillis(); 950 | mStartOffset = mOffset; 951 | 952 | mTouchStartPos = (x / mWidth) * MOVE_POS_MULTIPLE - 5; 953 | mTouchStartPos /= 2; 954 | 955 | mVelocity = VelocityTracker.obtain(); 956 | mVelocity.addMovement(event); 957 | } 958 | 959 | private void touchPointDown(MotionEvent event) { 960 | int actionIndex = event.getActionIndex(); 961 | mScrollPointerId = event.getPointerId(actionIndex); 962 | float x = mTouchStartX = event.getX(actionIndex); 963 | mTouchStartY = event.getY(actionIndex); 964 | 965 | mTouchStartPos = (x / mWidth) * MOVE_POS_MULTIPLE - 5; 966 | mTouchStartPos /= 2; 967 | mStartOffset = mOffset; 968 | } 969 | 970 | private void touchMoved(MotionEvent event) { 971 | // Log.i("CoverFlowView", "ScrollPointId = " + mScrollPointerId); 972 | int index = event.findPointerIndex(mScrollPointerId); 973 | float pos = (event.getX(index) / mWidth) * MOVE_POS_MULTIPLE - 5; 974 | pos /= 2; 975 | 976 | attemptSetOffset(mStartOffset + mTouchStartPos - pos); 977 | 978 | invalidate(); 979 | requestLayout(); 980 | mVelocity.addMovement(event); 981 | } 982 | 983 | private void touchPointUp(MotionEvent event) { 984 | int actionIndex = event.getActionIndex(); 985 | // Log.d("CoverFlowView", "point index = " + actionIndex); 986 | if (event.getPointerId(actionIndex) == mScrollPointerId) { 987 | // 选择一个新的触控点处理接下来的事件 988 | int newIndex = actionIndex == 0 ? 1 : 0; 989 | mScrollPointerId = event.getPointerId(newIndex); 990 | float x = mTouchStartX = event.getX(newIndex); 991 | mTouchStartY = event.getY(newIndex); 992 | 993 | mTouchStartPos = (x / mWidth) * MOVE_POS_MULTIPLE - 5; 994 | mTouchStartPos /= 2; 995 | mStartOffset = mOffset; 996 | } 997 | } 998 | 999 | private void touchEnded(MotionEvent event) { 1000 | float pos = (event.getX() / mWidth) * MOVE_POS_MULTIPLE - 5; 1001 | pos /= 2; 1002 | 1003 | if ((mOffset - Math.floor(mOffset)) != 0) { 1004 | mStartOffset += mTouchStartPos - pos; 1005 | attemptSetOffset(mStartOffset); 1006 | 1007 | mVelocity.addMovement(event); 1008 | 1009 | mVelocity.computeCurrentVelocity(1000); 1010 | float speed = mVelocity.getXVelocity(); 1011 | 1012 | speed = (speed / mWidth) * MOVE_SPEED_MULTIPLE; 1013 | if (speed > MAX_SPEED) 1014 | speed = MAX_SPEED; 1015 | else if (speed < -MAX_SPEED) 1016 | speed = -MAX_SPEED; 1017 | 1018 | startSettleAnimation(-speed); 1019 | } else { 1020 | setScrollState(SCROLL_STATE_IDLE); 1021 | } 1022 | 1023 | mVelocity.clear(); 1024 | mVelocity.recycle(); 1025 | } 1026 | 1027 | private void startSettleAnimation(float speed) { 1028 | if (mSettleAnimationRunnable != null) { 1029 | return; 1030 | } 1031 | 1032 | float delta = speed * speed / (FRICTION * 2); 1033 | if (speed < 0) 1034 | delta = -delta; 1035 | 1036 | float nearest = mStartOffset + delta; 1037 | nearest = (float) Math.floor(nearest + 0.5f); 1038 | 1039 | mStartSpeed = (float) Math.sqrt(Math.abs(nearest - mStartOffset) 1040 | * FRICTION * 2); 1041 | if (nearest < mStartOffset) 1042 | mStartSpeed = -mStartSpeed; 1043 | 1044 | mDuration = Math.abs(mStartSpeed / FRICTION); 1045 | mStartTime = AnimationUtils.currentAnimationTimeMillis(); 1046 | 1047 | mSettleAnimationRunnable = new Runnable() { 1048 | @Override 1049 | public void run() { 1050 | driveAnimation(); 1051 | } 1052 | }; 1053 | post(mSettleAnimationRunnable); 1054 | setScrollState(SCROLL_STATE_SETTLING); 1055 | } 1056 | 1057 | private void driveAnimation() { 1058 | float elapsed = (AnimationUtils.currentAnimationTimeMillis() - mStartTime) / 1000.0f; 1059 | if (elapsed >= mDuration) { 1060 | endSettleAnimation(); 1061 | } else { 1062 | updateAnimationAtElapsed(elapsed); 1063 | post(mSettleAnimationRunnable); 1064 | } 1065 | } 1066 | 1067 | private void endSettleAnimation() { 1068 | if (mScrollState == SCROLL_STATE_SETTLING) { 1069 | attemptSetOffset((float) Math.floor(mOffset + 0.5)); 1070 | 1071 | invalidate(); 1072 | requestLayout(); 1073 | 1074 | setScrollState(SCROLL_STATE_IDLE); 1075 | } 1076 | 1077 | if (mSettleAnimationRunnable != null) { 1078 | removeCallbacks(mSettleAnimationRunnable); 1079 | mSettleAnimationRunnable = null; 1080 | } 1081 | } 1082 | 1083 | private void updateAnimationAtElapsed(float elapsed) { 1084 | if (elapsed > mDuration) 1085 | elapsed = mDuration; 1086 | 1087 | float delta = Math.abs(mStartSpeed) * elapsed - FRICTION * elapsed 1088 | * elapsed / 2; 1089 | if (mStartSpeed < 0) 1090 | delta = -delta; 1091 | 1092 | attemptSetOffset(mStartOffset + delta); 1093 | invalidate(); 1094 | requestLayout(); 1095 | } 1096 | 1097 | private void attemptSetOffset(float offset) { 1098 | float old = mOffset; 1099 | if (!mShouldLoop) { 1100 | int start = mLeftEdgeIndex; 1101 | int end = mRightEdgeIndex; 1102 | if (offset < start) { 1103 | offset = start; 1104 | } else if (offset > end) { 1105 | offset = end; 1106 | } 1107 | } 1108 | mOffset = offset; 1109 | } 1110 | 1111 | private boolean inRangeOfView(View view, MotionEvent ev) { 1112 | if (view == null) 1113 | return false; 1114 | Rect frame = new Rect(); 1115 | getViewRect(view, frame); 1116 | return frame.contains((int) ev.getX(), (int) ev.getY()); 1117 | } 1118 | 1119 | private boolean atRightOfView(View view, MotionEvent ev) { 1120 | Rect frame = new Rect(); 1121 | getViewRect(view, frame); 1122 | return ev.getX() > frame.right; 1123 | } 1124 | 1125 | private boolean atLeftOfView(View view, MotionEvent ev) { 1126 | Rect frame = new Rect(); 1127 | getViewRect(view, frame); 1128 | return ev.getX() < frame.left; 1129 | } 1130 | 1131 | private static void getViewRect(View v, Rect rect) { 1132 | rect.left = (int) com.nineoldandroids.view.ViewHelper.getX(v); 1133 | rect.top = (int) com.nineoldandroids.view.ViewHelper.getY(v); 1134 | rect.right = rect.left + v.getWidth(); 1135 | rect.bottom = rect.top + v.getHeight(); 1136 | } 1137 | 1138 | public void setScaleRatio(float scaleRatio) { 1139 | if (scaleRatio > 1) 1140 | scaleRatio = 1; 1141 | if (scaleRatio < 0) 1142 | scaleRatio = 0; 1143 | 1144 | mEdgeScale = (MAX_SCALE - MIN_SCALE) * scaleRatio + MIN_SCALE; 1145 | requestLayout(); 1146 | } 1147 | 1148 | /** 1149 | * 是否可以点击左右侧 来切换上下张 1150 | */ 1151 | public void setClick2SwitchEnabled(boolean enabled) { 1152 | clickSwitchEnable = enabled; 1153 | } 1154 | 1155 | public void setLoopMode(boolean loop) { 1156 | if (loop == mLoopMode) { 1157 | return; 1158 | } 1159 | 1160 | stopScroll(); 1161 | cancelTouch(); 1162 | resetOffset(); 1163 | 1164 | this.mShouldLoop = this.mLoopMode = loop; 1165 | 1166 | mDataChanged = true; 1167 | requestLayout(); 1168 | } 1169 | 1170 | /** 1171 | * 翻到前页 1172 | */ 1173 | public void gotoPrevious() { 1174 | doSmoothScrollAnimator(-1.0f); 1175 | } 1176 | 1177 | /** 1178 | * 前进到后一页 1179 | */ 1180 | public void gotoForward() { 1181 | doSmoothScrollAnimator(1.0f); 1182 | } 1183 | 1184 | private void doSmoothScrollAnimator(float target) { 1185 | if (mScrollState != SCROLL_STATE_IDLE) { 1186 | return; 1187 | } 1188 | if (target == 0) 1189 | return; 1190 | 1191 | float initOffset = mOffset; 1192 | float targetOffset = initOffset + target; 1193 | if (!mShouldLoop) { //target offset越界 1194 | int start = mLeftEdgeIndex; 1195 | int end = mRightEdgeIndex; 1196 | if (targetOffset < start) { 1197 | return; 1198 | } else if (targetOffset > end) { 1199 | return; 1200 | } 1201 | } 1202 | 1203 | ValueAnimator animator = ValueAnimator.ofFloat(initOffset, targetOffset); 1204 | animator.setInterpolator(new AccelerateDecelerateInterpolator()); 1205 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1206 | @Override 1207 | public void onAnimationUpdate(ValueAnimator animation) { 1208 | attemptSetOffset((Float) animation.getAnimatedValue()); 1209 | 1210 | invalidate(); 1211 | requestLayout(); 1212 | } 1213 | }); 1214 | animator.addListener(new AnimatorListenerAdapter() { 1215 | @Override 1216 | public void onAnimationCancel(Animator animation) { 1217 | setScrollState(SCROLL_STATE_IDLE); 1218 | } 1219 | 1220 | @Override 1221 | public void onAnimationEnd(Animator animation) { 1222 | setScrollState(SCROLL_STATE_IDLE); 1223 | } 1224 | }); 1225 | mScrollAnimator = animator; 1226 | setScrollState(SCROLL_STATE_SETTLING); 1227 | animator.setDuration(300).start(); 1228 | } 1229 | 1230 | private void setScrollState(int state) { 1231 | if (state == mScrollState) { 1232 | return; 1233 | } 1234 | mScrollState = state; 1235 | if (state != SCROLL_STATE_SETTLING) { 1236 | stopScrollerInternal(); 1237 | } 1238 | } 1239 | 1240 | public void stopScroll() { 1241 | stopScrollerInternal(); 1242 | setScrollState(SCROLL_STATE_IDLE); 1243 | } 1244 | 1245 | private void stopScrollerInternal() { 1246 | endSettleAnimation(); 1247 | 1248 | if (mScrollAnimator != null) 1249 | mScrollAnimator.cancel(); 1250 | } 1251 | 1252 | public int getScrollState() { 1253 | return mScrollState; 1254 | } 1255 | 1256 | /** 1257 | * 选择某一个位置 1258 | * 1259 | * @param selection seleciton 1260 | * @param smooth 是否平滑滑动过去,还是直接切换 1261 | */ 1262 | public void setSelection(int selection, boolean smooth) { 1263 | if (mAdapter == null) 1264 | return; 1265 | 1266 | int count = mAdapter.getCount(); 1267 | if (selection < 0 || selection >= count) { 1268 | throw new IndexOutOfBoundsException(String.format(Locale.getDefault(), 1269 | "selection out of bound: selection is %d, range is (%d, %d]", selection, 0, 1270 | count)); 1271 | } 1272 | if (smooth) { 1273 | float offset = mOffset; 1274 | int curPos = convertIndex2Position((int) offset); 1275 | if (curPos == -1) 1276 | return; 1277 | 1278 | int minDistance; 1279 | //求出当前位置到达selection位置的最短路径 1280 | int distance1 = selection - curPos; 1281 | 1282 | if (mShouldLoop) { 1283 | int distance2 = selection + count - curPos; 1284 | int distance3 = selection - count - curPos; 1285 | minDistance = Math.abs(distance1) < Math.abs(distance2) ? distance1 : distance2; 1286 | minDistance = Math.abs(minDistance) < Math.abs(distance3) ? minDistance : distance3; 1287 | } else { 1288 | minDistance = distance1; 1289 | } 1290 | 1291 | doSmoothScrollAnimator(minDistance); 1292 | } else { 1293 | cancelTouch(); 1294 | stopScroll(); 1295 | 1296 | final int selectionIndex = convertPosition2Index(selection); 1297 | resetOffset(); 1298 | justifyOffset(selectionIndex); 1299 | 1300 | mDataChanged = true; 1301 | requestLayout(); 1302 | } 1303 | } 1304 | 1305 | private void dataSetChanged(boolean invalidate) { 1306 | if (!invalidate) { 1307 | int oldCount = mExpectedAdapterCount; 1308 | int newCount = mAdapter.getCount(); 1309 | final float offset = mOffset; 1310 | int delta = calculateOffsetDelta(offset, oldCount, newCount); 1311 | justifyOffset(delta); 1312 | } else { 1313 | stopScroll(); 1314 | cancelTouch(); 1315 | resetOffset(); 1316 | } 1317 | 1318 | mExpectedAdapterCount = mAdapter.getCount(); 1319 | mDataChanged = true; 1320 | requestLayout(); 1321 | } 1322 | 1323 | private int calculateOffsetDelta(float offset, int oldCount, int newCount) { 1324 | if (oldCount == newCount) { 1325 | return 0; 1326 | } 1327 | int mid = (int) Math.floor(offset + 0.5); 1328 | int midPosition = convertIndex2Position(mid, oldCount); 1329 | if (midPosition == -1) 1330 | return 0; 1331 | 1332 | if (midPosition > newCount - 1) { 1333 | midPosition = newCount - 1; 1334 | } 1335 | 1336 | int newMid = convertPosition2Index(midPosition); 1337 | return newMid - mid; 1338 | } 1339 | 1340 | public OnViewOnTopListener getOnViewOnTopListener() { 1341 | return mViewOnTopListener; 1342 | } 1343 | 1344 | public void setOnViewOnTopListener(OnViewOnTopListener mViewOnTopListener) { 1345 | this.mViewOnTopListener = mViewOnTopListener; 1346 | } 1347 | 1348 | /** 1349 | * 设置TopView的点击监听 1350 | */ 1351 | public void setOnTopViewClickListener(OnTopViewClickListener topViewClickLister) { 1352 | this.mTopViewClickLister = topViewClickLister; 1353 | } 1354 | 1355 | public OnTopViewClickListener getOnTopViewClickListener() { 1356 | return this.mTopViewClickLister; 1357 | } 1358 | 1359 | /** 1360 | * 设置TopView的长点击监听 1361 | */ 1362 | public void setOnTopViewLongClickListener(OnTopViewLongClickListener topViewLongClickLister) { 1363 | this.mTopViewLongClickLister = topViewLongClickLister; 1364 | } 1365 | 1366 | public OnTopViewLongClickListener getOnTopViewLongClickListener() { 1367 | return this.mTopViewLongClickLister; 1368 | } 1369 | 1370 | public interface OnViewOnTopListener { 1371 | void viewOnTop(int position, View itemView); 1372 | } 1373 | 1374 | public interface OnTopViewClickListener { 1375 | void onClick(int position, View itemView); 1376 | } 1377 | 1378 | public interface OnTopViewLongClickListener { 1379 | void onLongClick(int position, View itemView); 1380 | } 1381 | 1382 | private class AdapterDataSetObserver extends DataSetObserver { 1383 | @Override 1384 | public void onChanged() { 1385 | dataSetChanged(false); 1386 | } 1387 | 1388 | @Override 1389 | public void onInvalidated() { 1390 | dataSetChanged(true); 1391 | } 1392 | } 1393 | } 1394 | -------------------------------------------------------------------------------- /coverflowview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /coverflowview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CoverFlow 3 | 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /raw/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/raw/sample.gif -------------------------------------------------------------------------------- /raw/screenshot_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/raw/screenshot_1.jpg -------------------------------------------------------------------------------- /raw/screenshot_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/raw/screenshot_2.jpg -------------------------------------------------------------------------------- /raw/screenshot_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/raw/screenshot_3.jpg -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.missmess.coverflowview" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:23.3.0' 25 | compile 'com.android.support:design:23.3.0' 26 | compile project(':coverflowview') 27 | } 28 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\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 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/java/com/missmess/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.missmess.demo; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.TabLayout; 5 | import android.support.v4.view.ViewPager; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.widget.Toast; 10 | 11 | import com.missmess.coverflowview.CoverFlowView; 12 | import com.missmess.demo.adapter.MyCoverFlowAdapter; 13 | import com.missmess.demo.adapter.MyVpAdapter; 14 | import com.missmess.demo.adapter.NewCoverFlowAdapter; 15 | 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | private CoverFlowView coverflow_view; 19 | private TabLayout tabl; 20 | private ViewPager vp; 21 | private MyCoverFlowAdapter adapter; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_main); 27 | 28 | init(); 29 | } 30 | 31 | private void init() { 32 | coverflow_view = (CoverFlowView) findViewById(R.id.coverflow_view); 33 | tabl = (TabLayout) findViewById(R.id.tabl); 34 | vp = (ViewPager) findViewById(R.id.vp); 35 | 36 | adapter = new MyCoverFlowAdapter(); 37 | coverflow_view.setAdapter(adapter); 38 | coverflow_view.setOnViewOnTopListener(new CoverFlowView.OnViewOnTopListener() { 39 | @Override 40 | public void viewOnTop(int position, View itemView) { 41 | Log.d("viewOnTop", "position==" + position); 42 | if (adapter == null) 43 | return; 44 | if (position == 0) { 45 | vp.setCurrentItem(1); 46 | } else if (position == 2) { 47 | adapter.animProgress(itemView); 48 | } 49 | } 50 | }); 51 | coverflow_view.setOnTopViewClickListener(new CoverFlowView.OnTopViewClickListener() { 52 | @Override 53 | public void onClick(int position, View itemView) { 54 | Toast.makeText(MainActivity.this, "position " + position + " clicked", Toast.LENGTH_SHORT).show(); 55 | } 56 | }); 57 | coverflow_view.setOnTopViewLongClickListener(new CoverFlowView.OnTopViewLongClickListener() { 58 | @Override 59 | public void onLongClick(int position, View itemView) { 60 | Toast.makeText(MainActivity.this, "position " + position + " long clicked", Toast.LENGTH_SHORT).show(); 61 | } 62 | }); 63 | 64 | vp.setAdapter(new MyVpAdapter(coverflow_view)); 65 | tabl.setupWithViewPager(vp); 66 | } 67 | 68 | public void vp1Clicker(View v) { 69 | int paddingTop = coverflow_view.getPaddingTop(); 70 | int paddingBottom = coverflow_view.getPaddingBottom(); 71 | int paddingLeft = coverflow_view.getPaddingLeft(); 72 | int paddingRight = coverflow_view.getPaddingRight(); 73 | int maxPadding = 250; 74 | int minPadding = -300; 75 | int stepPadding = 50; 76 | switch (v.getId()) { 77 | case R.id.button: 78 | paddingLeft += stepPadding; 79 | paddingLeft = Math.min(maxPadding, Math.max(minPadding, paddingLeft)); 80 | paddingRight += stepPadding; 81 | paddingRight = Math.min(maxPadding, Math.max(minPadding, paddingRight)); 82 | coverflow_view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); 83 | break; 84 | case R.id.button2: 85 | paddingLeft -= stepPadding; 86 | paddingLeft = Math.min(maxPadding, Math.max(minPadding, paddingLeft)); 87 | paddingRight -= stepPadding; 88 | paddingRight = Math.min(maxPadding, Math.max(minPadding, paddingRight)); 89 | coverflow_view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); 90 | break; 91 | } 92 | } 93 | 94 | public void vp2Clicker(View v) { 95 | switch (v.getId()) { 96 | case R.id.button: 97 | coverflow_view.gotoPrevious(); 98 | break; 99 | case R.id.button2: 100 | coverflow_view.gotoForward(); 101 | break; 102 | } 103 | } 104 | 105 | public void vp3Clicker(View v) { 106 | switch (v.getId()) { 107 | case R.id.button: 108 | if(adapter != null) { 109 | if (adapter.getTips() == MyCoverFlowAdapter.InitialTips) { 110 | adapter.setTips(MyCoverFlowAdapter.NewTips); 111 | } else { 112 | adapter.setTips(MyCoverFlowAdapter.InitialTips); 113 | } 114 | adapter.notifyDataSetChanged(); 115 | } 116 | break; 117 | case R.id.button2: 118 | if(adapter != null) { 119 | coverflow_view.setAdapter(new NewCoverFlowAdapter()); 120 | adapter = null; 121 | } else { 122 | adapter = new MyCoverFlowAdapter(); 123 | coverflow_view.setAdapter(adapter); 124 | } 125 | break; 126 | case R.id.button3: 127 | coverflow_view.setSelection(3, true); 128 | break; 129 | case R.id.button4: 130 | coverflow_view.setSelection(2, false); 131 | break; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /sample/src/main/java/com/missmess/demo/adapter/MyCoverFlowAdapter.java: -------------------------------------------------------------------------------- 1 | package com.missmess.demo.adapter; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.content.Context; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageButton; 8 | import android.widget.LinearLayout; 9 | import android.widget.ProgressBar; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | 13 | import com.missmess.coverflowview.ACoverFlowAdapter; 14 | import com.missmess.demo.R; 15 | 16 | /** 17 | * @author wl 18 | * @since 2016/07/28 16:52 19 | */ 20 | public class MyCoverFlowAdapter extends ACoverFlowAdapter { 21 | public static final int[] InitialTips = {R.string.cover_tip0, R.string.cover_tip1, R.string.cover_tip2, R.string.cover_tip3, R.string.cover_tip4}; 22 | public static final int[] NewTips = {R.string.new_cover_tip0, R.string.new_cover_tip1, R.string.new_cover_tip2, R.string.new_cover_tip3, R.string.new_cover_tip4, R.string.app_name}; 23 | private int[] tipRess = InitialTips; 24 | 25 | public void setTips(int[] tips) { 26 | this.tipRess = tips; 27 | } 28 | 29 | public int[] getTips() { 30 | return tipRess; 31 | } 32 | 33 | @Override 34 | public int getCount() { 35 | return tipRess.length; 36 | } 37 | 38 | @Override 39 | public ViewHolder onCreateViewHolder(ViewGroup parent, int type) { 40 | View content; 41 | if(type == 0) { 42 | content = View.inflate(parent.getContext(), R.layout.item_coverflow1, new LinearLayout(parent.getContext())); 43 | return new ViewHolder1(content); 44 | } else if(type == 1) { 45 | content = View.inflate(parent.getContext(), R.layout.item_coverflow2, new LinearLayout(parent.getContext())); 46 | return new ViewHolder2(content); 47 | } 48 | return null; 49 | } 50 | 51 | @Override 52 | public void onBindViewHolder(ACoverFlowAdapter.ViewHolder vh, int position) { 53 | if(vh instanceof ViewHolder1) { 54 | ViewHolder1 holder = (ViewHolder1) vh; 55 | holder.textView.setText(tipRess[position]); 56 | if(position == 2) { 57 | holder.fl.setVisibility(View.VISIBLE); 58 | holder.textView1.setText("0%"); 59 | holder.progressBar.setProgress(0); 60 | } else { 61 | holder.fl.setVisibility(View.GONE); 62 | } 63 | } else if(vh instanceof ViewHolder2) { 64 | ViewHolder2 holder = (ViewHolder2) vh; 65 | final Context context = holder.textView2.getContext(); 66 | holder.textView2.setText(context.getString(R.string.circle, position)); 67 | holder.imageView.setOnClickListener(new View.OnClickListener() { 68 | @Override 69 | public void onClick(View v) { 70 | Toast.makeText(context, "点击了item上的button", Toast.LENGTH_SHORT).show(); 71 | } 72 | }); 73 | } 74 | } 75 | 76 | @Override 77 | public int getItemViewType(int position) { 78 | if(position == 4) 79 | return 1; 80 | return 0; 81 | } 82 | 83 | public void animProgress(View itemView) { 84 | final ViewHolder1 holder = (ViewHolder1) itemView.getTag(); 85 | ValueAnimator animator = ValueAnimator.ofInt(0, 100); 86 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 87 | @Override 88 | public void onAnimationUpdate(ValueAnimator animation) { 89 | int va = (int) animation.getAnimatedValue(); 90 | holder.progressBar.setProgress(va); 91 | holder.textView1.setText(String.format("%d%%", va)); 92 | } 93 | }); 94 | animator.setDuration(500); 95 | animator.start(); 96 | } 97 | 98 | class ViewHolder1 extends ACoverFlowAdapter.ViewHolder { 99 | TextView textView; 100 | View fl; 101 | TextView textView1; 102 | ProgressBar progressBar; 103 | 104 | public ViewHolder1(View itemView) { 105 | super(itemView); 106 | textView = (TextView) itemView.findViewById(R.id.textView); 107 | fl = itemView.findViewById(R.id.fl); 108 | textView1 = (TextView) itemView.findViewById(R.id.textView1); 109 | progressBar = (ProgressBar) itemView.findViewById(R.id.progressBar); 110 | } 111 | } 112 | 113 | class ViewHolder2 extends ACoverFlowAdapter.ViewHolder { 114 | TextView textView2; 115 | ImageButton imageView; 116 | 117 | public ViewHolder2(View itemView) { 118 | super(itemView); 119 | textView2 = (TextView) itemView.findViewById(R.id.textView2); 120 | imageView = (ImageButton) itemView.findViewById(R.id.imageView); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /sample/src/main/java/com/missmess/demo/adapter/MyVpAdapter.java: -------------------------------------------------------------------------------- 1 | package com.missmess.demo.adapter; 2 | 3 | import android.support.v4.view.PagerAdapter; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.CompoundButton; 7 | import android.widget.SeekBar; 8 | import android.widget.Switch; 9 | 10 | import com.missmess.coverflowview.CoverFlowView; 11 | import com.missmess.demo.R; 12 | 13 | /** 14 | * @author wl 15 | * @since 2016/07/28 17:01 16 | */ 17 | public class MyVpAdapter extends PagerAdapter { 18 | private CoverFlowView mCoverFlowView; 19 | 20 | public MyVpAdapter(CoverFlowView coverFlowView) { 21 | this.mCoverFlowView = coverFlowView; 22 | } 23 | 24 | @Override 25 | public int getCount() { 26 | return 3; 27 | } 28 | 29 | @Override 30 | public CharSequence getPageTitle(int position) { 31 | String title = null; 32 | switch (position) { 33 | case 0: 34 | title = "Introduction"; 35 | break; 36 | case 1: 37 | title = "Switch Method"; 38 | break; 39 | case 2: 40 | title = "Change Data"; 41 | break; 42 | } 43 | return title; 44 | } 45 | 46 | @Override 47 | public boolean isViewFromObject(View view, Object object) { 48 | return view == object; 49 | } 50 | 51 | @Override 52 | public Object instantiateItem(ViewGroup container, int position) { 53 | View content = null; 54 | switch (position) { 55 | case 0: 56 | content = View.inflate(container.getContext(), R.layout.vp_layout1, null); 57 | SeekBar seekBar = (SeekBar) content.findViewById(R.id.seek_bar); 58 | seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 59 | @Override 60 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 61 | mCoverFlowView.setScaleRatio((float) progress / 100); 62 | } 63 | 64 | @Override 65 | public void onStartTrackingTouch(SeekBar seekBar) { 66 | 67 | } 68 | 69 | @Override 70 | public void onStopTrackingTouch(SeekBar seekBar) { 71 | 72 | } 73 | }); 74 | break; 75 | case 1: 76 | content = View.inflate(container.getContext(), R.layout.vp_layout2, null); 77 | Switch switch1 = (Switch) content.findViewById(R.id.switch1); 78 | switch1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 79 | @Override 80 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 81 | mCoverFlowView.setClick2SwitchEnabled(isChecked); 82 | } 83 | }); 84 | Switch switch2 = (Switch) content.findViewById(R.id.switch2); 85 | switch2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 86 | @Override 87 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 88 | mCoverFlowView.setLoopMode(isChecked); 89 | } 90 | }); 91 | break; 92 | case 2: 93 | content = View.inflate(container.getContext(), R.layout.vp_layout3, null); 94 | break; 95 | } 96 | container.addView(content); 97 | return content; 98 | } 99 | 100 | @Override 101 | public void destroyItem(ViewGroup container, int position, Object object) { 102 | container.removeView((View) object); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /sample/src/main/java/com/missmess/demo/adapter/NewCoverFlowAdapter.java: -------------------------------------------------------------------------------- 1 | package com.missmess.demo.adapter; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.LinearLayout; 6 | import android.widget.TextView; 7 | 8 | import com.missmess.coverflowview.ACoverFlowAdapter; 9 | import com.missmess.demo.R; 10 | 11 | /** 12 | * @author wl 13 | * @since 2016/07/28 18:23 14 | */ 15 | public class NewCoverFlowAdapter extends ACoverFlowAdapter { 16 | 17 | @Override 18 | public int getCount() { 19 | return 5; 20 | } 21 | 22 | @Override 23 | public ViewHolder onCreateViewHolder(ViewGroup parent, int type) { 24 | View content = View.inflate(parent.getContext(), R.layout.item_newcoverflow, new LinearLayout(parent.getContext())); 25 | return new ViewHolder(content); 26 | } 27 | 28 | @Override 29 | public void onBindViewHolder(ViewHolder holder, int position) { 30 | holder.textView.setText("New Adapter Item " + position); 31 | } 32 | 33 | class ViewHolder extends ACoverFlowAdapter.ViewHolder { 34 | TextView textView; 35 | 36 | public ViewHolder(View itemView) { 37 | super(itemView); 38 | textView = (TextView) itemView.findViewById(R.id.textView); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/oval_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/rectangle_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 24 | 25 | 32 | 33 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 63 | 64 | 68 | 69 | 70 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_coverflow1.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 24 | 25 | 33 | 34 | 42 | 43 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_coverflow2.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_newcoverflow.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/vp_layout1.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 |