├── .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 | 
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 | 
131 | 
132 | 
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 | * - adapter中的item将按顺序排列在CoverFlowView中。初始显示的一组view为adapter中position为
33 | * 0~totalVis的item,并且最中间的view设index = 0,即index=0对应position=sib。
34 | * 参考 {@link #convertIndex2Position(int)}
35 | * - index从左到右增大。在loop模式下,index没有取值范围。在非loop模式,index取值范围
36 | * 为[-sib, count-sib-1]
37 | * - 内部使用{@link #showViewArray}存储正显示在CoverFlowView上的item。这个map以
38 | * adapter position作为key存储view
39 | * - CoverFlowView通过{@link #addView(View, int)}的index order来控制CoverFlow的层叠效果。
40 | * 最中间的view的index order最大,越往两边index order越小。
41 | * - CoverFlowView发生任何移动时{@link #mOffset}都会改变,代表偏移量(浮点数)。滑动停止时,
42 | * {@link #mOffset}=index
43 | * - 每当滑动到两个view之间的一半(offset值为0.5)时,将会移除一个边界view(往左滑移除右边界)和获取一个新的
44 | * 显示view,并且调整view的index order以保持正确的层叠效果,参见{@link #onLayout(boolean, int, int, int, int)}
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 |
21 |
22 |
29 |
30 |
36 |
37 |
42 |
43 |
49 |
50 |
51 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/vp_layout2.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
23 |
24 |
31 |
32 |
39 |
40 |
46 |
47 |
53 |
54 |
55 |
56 |
62 |
63 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/vp_layout3.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
21 |
22 |
28 |
29 |
35 |
36 |
42 |
43 |
44 |
51 |
52 |
53 |
59 |
60 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/sample/src/main/res/mipmap-xhdpi/circle.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/missmess/CoverFlowView/2b4ae2c71996011289a6e8d8d4bbf899d2d703d3/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 | #FF6600
7 | #666666
8 |
9 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CoverFlowView
3 | You can simply slide the CoverFlowView widget left or right to switch between your coverflow adapter views.
4 | \n\nThese views will overlap when you show too many view at once or your view is large.
5 | \n\nYou can Change total visible views in the layout xml (default value is 3).
6 | \n\nYou can add a listener to handle a top view click, long click and view on top event.
7 | When set \"click to switch\" on, click the left or right edge will also switch the coverflow view
8 | \n\nYou can also control to goto previous or next one in your code.
9 | When in LoopMode, we can scroll left and right infinitely.
10 | When your data set has changed, you can just call adapter\'s notifyDataSetChanged() to get CoverFlowView refresh.
11 | \nYou can also change a new adapter by setAdapter
12 | You can also goto specified views in one method, CoverFlowView will search the fast way to go to destination.
13 | \nAlso, you can close the transition animation.
14 |
15 | Index 0\n\nWhen this view on top, checked TAB 1
16 | Index 1
17 | Index 2
18 | Index 3
19 | Index 4
20 | New 0\n\nWhen this view on top, checked TAB 1
21 | New 1
22 | New 2
23 | New 3
24 | New 4
25 |
26 | Clickable Button\nIndex %d
27 |
28 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':coverflowview'
2 |
--------------------------------------------------------------------------------