├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CircularViewPager
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── sanyuzhang
│ │ └── circular
│ │ └── viewpager
│ │ └── cvp
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── sanyuzhang
│ │ │ └── circular
│ │ │ └── viewpager
│ │ │ └── cvp
│ │ │ ├── adapter
│ │ │ └── CircularTabLayoutAdapter.java
│ │ │ ├── util
│ │ │ └── CircularUtils.java
│ │ │ └── view
│ │ │ ├── CircularTabLayout.java
│ │ │ ├── CircularViewPager.java
│ │ │ ├── ITabSetting.java
│ │ │ ├── SnappyLinearLayoutManager.java
│ │ │ └── TabIndicatorDecorator.java
│ └── res
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── ids.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── sanyuzhang
│ └── circular
│ └── viewpager
│ └── cvp
│ └── ExampleUnitTest.java
├── LICENSE
├── PULL_REQUEST_TEMPLATE.md
├── README.md
└── Sample
├── .gitignore
├── CircularViewPager
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── sanyuzhang
│ │ └── circular
│ │ └── viewpager
│ │ └── cvp
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── sanyuzhang
│ │ │ └── circular
│ │ │ └── viewpager
│ │ │ └── cvp
│ │ │ ├── adapter
│ │ │ └── CircularTabLayoutAdapter.java
│ │ │ ├── util
│ │ │ └── CircularUtils.java
│ │ │ └── view
│ │ │ ├── CircularTabLayout.java
│ │ │ ├── CircularViewPager.java
│ │ │ ├── ITabSetting.java
│ │ │ ├── SnappyLinearLayoutManager.java
│ │ │ └── TabIndicatorDecorator.java
│ └── res
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── ids.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── sanyuzhang
│ └── circular
│ └── viewpager
│ └── cvp
│ └── ExampleUnitTest.java
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── sanyuzhang
│ │ └── circular
│ │ └── viewpager
│ │ └── sample
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── sanyuzhang
│ │ │ └── circular
│ │ │ └── viewpager
│ │ │ └── sample
│ │ │ ├── MainActivity.java
│ │ │ └── SampleFragment.java
│ └── res
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── fragment_sample.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── sanyuzhang
│ └── circular
│ └── viewpager
│ └── sample
│ └── ExampleUnitTest.java
├── build.gradle
├── demo
└── demo.gif
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/*
38 |
39 | # Keystore files
40 | *.jks
41 |
42 | # External native build folder generated in Android Studio 2.2 and later
43 | .externalNativeBuild
44 |
45 | # Google Services (e.g. APIs or Firebase)
46 | google-services.json
47 |
48 | # Freeline
49 | freeline.py
50 | freeline/
51 | freeline_project_description.json
52 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at deviltrigger1120@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributions are very welcome. If you find a bug in the library or want a feature and think you can fix it yourself, fork and pull request and i will greatly appreciate it!
2 |
3 | I appreciate pull requests for new features as well as bugs. However, when it comes to new features please also explain the use case and way you think the library should include. If you don't want to start coding a feature without knowing if the feature will have chance of being included, open an issue and we can discuss the feature!
4 |
--------------------------------------------------------------------------------
/CircularViewPager/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/CircularViewPager/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 28
5 | buildToolsVersion "28.0.2"
6 |
7 | defaultConfig {
8 | minSdkVersion 15
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "0.6.0"
12 |
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 |
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation fileTree(dir: 'libs', include: ['*.jar'])
26 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
27 | exclude group: 'com.android.support', module: 'support-annotations'
28 | })
29 | implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
30 | implementation 'com.android.support:recyclerview-v7:28.0.0-rc02'
31 | implementation('com.android.support:design:28.0.0-rc02') {
32 | exclude module: 'support-v4'
33 | exclude module: 'appcompat-v7'
34 | }
35 | testImplementation 'junit:junit:4.12'
36 | }
37 |
--------------------------------------------------------------------------------
/CircularViewPager/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/j_cho/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/CircularViewPager/src/androidTest/java/com/sanyuzhang/circular/viewpager/cvp/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.sanyuzhang.circular.viewpager.cvp.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/adapter/CircularTabLayoutAdapter.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.adapter;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.ActionBar;
5 | import android.content.Context;
6 | import android.content.res.ColorStateList;
7 | import android.graphics.drawable.Drawable;
8 | import android.os.Build;
9 | import android.support.annotation.NonNull;
10 | import android.support.v4.view.ViewCompat;
11 | import android.support.v7.widget.AppCompatTextView;
12 | import android.support.v7.widget.RecyclerView;
13 | import android.text.TextUtils;
14 | import android.util.TypedValue;
15 | import android.view.Gravity;
16 | import android.view.View;
17 | import android.view.ViewGroup;
18 | import android.view.ViewParent;
19 | import android.view.accessibility.AccessibilityEvent;
20 | import android.view.accessibility.AccessibilityNodeInfo;
21 | import android.widget.ImageView;
22 | import android.widget.LinearLayout;
23 | import android.widget.TextView;
24 | import android.widget.Toast;
25 |
26 | import com.sanyuzhang.circular.viewpager.cvp.R;
27 | import com.sanyuzhang.circular.viewpager.cvp.util.CircularUtils;
28 | import com.sanyuzhang.circular.viewpager.cvp.view.CircularTabLayout;
29 |
30 | import java.util.List;
31 |
32 | import static android.support.v7.content.res.AppCompatResources.getDrawable;
33 |
34 | /**
35 | * Created by j_cho on 2017/10/03.
36 | */
37 |
38 | public class CircularTabLayoutAdapter extends RecyclerView.Adapter {
39 |
40 | private Context mContext;
41 |
42 | private CircularTabLayout.ITabSelectedListener mTabSelectedListener;
43 |
44 | private int mCurrentPosition = CircularUtils.START_POSITION;
45 |
46 | /**
47 | * real tab
48 | */
49 | private List mObjectList;
50 |
51 | /**
52 | * ViewHolder.itemView listener
53 | */
54 | private View.OnClickListener mOnClickListener = new View.OnClickListener() {
55 | @Override
56 | public void onClick(View v) {
57 | mTabSelectedListener.onTabItemClicked((Integer) v.getTag());
58 | }
59 | };
60 |
61 | /**
62 | * TabView Style
63 | */
64 | private int mTabViewMinimumHeight;
65 |
66 | private int mTabViewBackground;
67 |
68 | private int[] mTabViewPadding;
69 |
70 | private int[] mTabViewMarings;
71 |
72 | private int mTabMinWidth;
73 |
74 | private int mTabMaxWidth;
75 |
76 | private int mTabTextAppearance;
77 |
78 | private ColorStateList mTabTextColors;
79 |
80 | private int mTabTextSize;
81 |
82 | /**
83 | * Constructor.
84 | *
85 | * @param context
86 | * @param tabSelectedListener
87 | */
88 | public CircularTabLayoutAdapter(@NonNull Context context, @NonNull CircularTabLayout.ITabSelectedListener tabSelectedListener) {
89 | mContext = context;
90 | mTabSelectedListener = tabSelectedListener;
91 | }
92 |
93 | @Override
94 | public TabViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
95 | TabView tabView = createTabView();
96 | tabView.setId(R.id.recycler_tab_layout_tab_view);
97 | return new TabViewHolder(tabView);
98 | }
99 |
100 | @Override
101 | public void onBindViewHolder(TabViewHolder holder, int position) {
102 | int realFragmentPosition = calculateRealPositionFromDummyPosition(position);
103 | holder.itemView.setTag(position);
104 | holder.itemView.setOnClickListener(mOnClickListener);
105 | holder.tabView.update(mObjectList.get(realFragmentPosition));
106 | holder.tabView.setSelected(isSelectedItem(position));
107 | }
108 |
109 | @Override
110 | public int getItemCount() {
111 | return CircularUtils.ITEM_COUNT;
112 | }
113 |
114 | public int getCurrentPosition() {
115 | return mCurrentPosition;
116 | }
117 |
118 | /**
119 | * @param position
120 | */
121 | public void setCurrentPosition(int position) {
122 | int previousPosition = mCurrentPosition;
123 | mCurrentPosition = position;
124 | notifyItemChanged(previousPosition);
125 | notifyItemChanged(mCurrentPosition);
126 | }
127 |
128 | /**
129 | * @param position
130 | * @return
131 | */
132 | public boolean isSelectedItem(int position) {
133 | return mCurrentPosition == position;
134 | }
135 |
136 | /**
137 | * calculate the real position from DummyPosition
138 | *
139 | * @param dummyPosition
140 | * @return
141 | */
142 | public int calculateRealPositionFromDummyPosition(int dummyPosition) {
143 | return CircularUtils.calculateRealPositionFromDummyPosition(mObjectList.size(), dummyPosition);
144 | }
145 |
146 | /**
147 | * calculate DummyPosition from real position.
148 | *
149 | * @param realPosition
150 | * @return
151 | */
152 | public int calculateDummyPositionFromRealPosition(int realPosition) {
153 | return CircularUtils.calculateDummyPositionFromRealPosition(mObjectList.size(), mCurrentPosition,
154 | realPosition);
155 | }
156 |
157 | /**
158 | * @param realPosition
159 | * @return
160 | */
161 | public int moveToPosition(int realPosition) {
162 | int dummyPosition = calculateDummyPositionFromRealPosition(realPosition);
163 | setCurrentPosition(dummyPosition);
164 | return dummyPosition;
165 | }
166 |
167 | public void setTabViewStyle(int tabViewMinimumHeight, int tabViewBackground, int[] tabViewMargins,
168 | int[] tabViewPadding, int tabMinWidth, int tabMaxWidth,
169 | int tabTextSize, int tabTextAppearance, ColorStateList tabTextColors) {
170 | mTabViewMinimumHeight = tabViewMinimumHeight;
171 | mTabViewBackground = tabViewBackground;
172 | mTabViewPadding = tabViewPadding;
173 | mTabViewMarings = tabViewMargins;
174 | mTabMinWidth = tabMinWidth;
175 | mTabMaxWidth = tabMaxWidth;
176 | mTabTextSize = tabTextSize;
177 | mTabTextAppearance = tabTextAppearance;
178 | mTabTextColors = tabTextColors;
179 | }
180 |
181 | /**
182 | * @return
183 | */
184 | protected TabView createTabView() {
185 | TabView tabView = new TabView(mContext, mTabViewBackground, mTabViewMarings, mTabViewPadding, mTabMinWidth,
186 | mTabMaxWidth, mTabTextSize, mTabTextAppearance, mTabTextColors);
187 | tabView.setFocusable(true);
188 | tabView.setMinimumHeight(mTabViewMinimumHeight);
189 | return tabView;
190 | }
191 |
192 | /**
193 | * RecyclerView.Tab list
194 | *
195 | * @param objectList
196 | */
197 | public void setTabData(List objectList) {
198 | mObjectList = objectList;
199 | }
200 |
201 | static class TabViewHolder extends RecyclerView.ViewHolder {
202 |
203 | TabView tabView;
204 |
205 | public TabViewHolder(View itemView) {
206 | super(itemView);
207 | tabView = (TabView) itemView.findViewById(R.id.recycler_tab_layout_tab_view);
208 | }
209 | }
210 |
211 | static class TabView extends LinearLayout implements View.OnLongClickListener {
212 |
213 | private static final int MAX_TAB_TEXT_LINES = 2;
214 |
215 | private CircularTabLayout.Tab mTab;
216 |
217 | private TextView mTextView;
218 |
219 | private ImageView mIconView;
220 |
221 | private View mCustomView;
222 |
223 | private int mTabTextSize;
224 |
225 | private int mTabMinWidth;
226 |
227 | private int mTabMaxWidth;
228 |
229 | private int mTabTextAppearance;
230 |
231 | private ColorStateList mTabTextColors;
232 |
233 | public TabView(Context context, int background, int[] margins,
234 | int[] paddings, int tabMinWidth, int tabMaxWidth,
235 | int tabTextSize, int tabTextAppearance, ColorStateList tabTextColors) {
236 | super(context);
237 | if (background != 0) {
238 | setBackgroundDrawable(getDrawable(context, background));
239 | }
240 |
241 | ViewCompat.setPaddingRelative(this, paddings[0], paddings[1], paddings[2], paddings[3]);
242 |
243 | mTabMinWidth = tabMinWidth;
244 | mTabMaxWidth = tabMaxWidth;
245 | mTabTextSize = tabTextSize;
246 | mTabTextAppearance = tabTextAppearance;
247 | mTabTextColors = tabTextColors;
248 |
249 | MarginLayoutParams marginLayoutParams = new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT);
250 |
251 | marginLayoutParams.leftMargin = margins[0];
252 | marginLayoutParams.topMargin = margins[1];
253 | marginLayoutParams.rightMargin = margins[2];
254 | marginLayoutParams.bottomMargin = margins[3];
255 |
256 | setLayoutParams(marginLayoutParams);
257 |
258 | setGravity(Gravity.CENTER);
259 | }
260 |
261 | public void setSelected(boolean selected) {
262 | boolean changed = isSelected() != selected;
263 | super.setSelected(selected);
264 | if (changed && selected) {
265 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
266 | if (mTextView != null) {
267 | mTextView.setSelected(selected);
268 | }
269 | if (mIconView != null) {
270 | mIconView.setSelected(selected);
271 | }
272 | }
273 | }
274 |
275 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
276 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
277 | super.onInitializeAccessibilityEvent(event);
278 | event.setClassName(ActionBar.Tab.class.getName());
279 | }
280 |
281 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
282 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
283 | super.onInitializeAccessibilityNodeInfo(info);
284 | info.setClassName(ActionBar.Tab.class.getName());
285 | }
286 |
287 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
288 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
289 | if (mTabMaxWidth != 0 && getMeasuredWidth() > mTabMaxWidth) {
290 | super.onMeasure(MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
291 | } else if (mTabMinWidth > 0 && getMeasuredHeight() < mTabMinWidth) {
292 | super.onMeasure(MeasureSpec.makeMeasureSpec(mTabMinWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
293 | }
294 | }
295 |
296 | final void update(CircularTabLayout.Tab tab) {
297 | ColorStateList tabTextColors = tab.getTextColorStateList() == null ? mTabTextColors : tab.getTextColorStateList();
298 |
299 | mTab = tab;
300 | View custom = tab.getCustomView();
301 | if (custom != null) {
302 | ViewParent icon = custom.getParent();
303 | if (icon != this) {
304 | if (icon != null) {
305 | ((ViewGroup) icon).removeView(custom);
306 | }
307 | addView(custom);
308 | }
309 |
310 | mCustomView = custom;
311 | if (mTextView != null) {
312 | mTextView.setVisibility(View.GONE);
313 | }
314 |
315 | if (mIconView != null) {
316 | mIconView.setVisibility(View.GONE);
317 | mIconView.setImageDrawable(null);
318 | }
319 | } else {
320 | if (mCustomView != null) {
321 | removeView(mCustomView);
322 | mCustomView = null;
323 | }
324 |
325 | Drawable icon1 = tab.getIcon();
326 | CharSequence text = tab.getText();
327 | if (icon1 != null) {
328 | if (mIconView == null) {
329 | ImageView hasText = new ImageView(getContext());
330 | LayoutParams textView = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
331 | textView.gravity = Gravity.CENTER_VERTICAL;
332 | hasText.setLayoutParams(textView);
333 | addView(hasText, 0);
334 | mIconView = hasText;
335 | }
336 |
337 | mIconView.setImageDrawable(icon1);
338 | mIconView.setVisibility(View.VISIBLE);
339 | } else if (mIconView != null) {
340 | mIconView.setVisibility(View.GONE);
341 | mIconView.setImageDrawable(null);
342 | }
343 |
344 | boolean hasText1 = !TextUtils.isEmpty(text);
345 | if (hasText1) {
346 | if (mTextView == null) {
347 | AppCompatTextView textView1 = new AppCompatTextView(getContext());
348 | textView1.setTextAppearance(getContext(), mTabTextAppearance);
349 | if (mTabTextSize != -1) {
350 | textView1.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabTextSize);
351 | }
352 | textView1.setMaxLines(MAX_TAB_TEXT_LINES);
353 | textView1.setEllipsize(TextUtils.TruncateAt.END);
354 | textView1.setGravity(Gravity.CENTER);
355 | if (tabTextColors != null) {
356 | textView1.setTextColor(tabTextColors);
357 | }
358 | addView(textView1, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
359 | mTextView = textView1;
360 | }
361 | mTextView.setText(text);
362 | mTextView.setContentDescription(tab.getContentDescription());
363 | mTextView.setVisibility(View.VISIBLE);
364 | } else if (mTextView != null) {
365 | mTextView.setVisibility(View.GONE);
366 | mTextView.setText(null);
367 | }
368 |
369 | if (mIconView != null) {
370 | mIconView.setContentDescription(tab.getContentDescription());
371 | }
372 |
373 | if (!hasText1 && !TextUtils.isEmpty(tab.getContentDescription())) {
374 | setOnLongClickListener(this);
375 | setLongClickable(true);
376 | } else {
377 | setOnLongClickListener(null);
378 | setLongClickable(false);
379 | }
380 | }
381 | }
382 |
383 | @Override
384 | public boolean onLongClick(View v) {
385 | final int[] screenPos = new int[2];
386 | getLocationOnScreen(screenPos);
387 | final Context context = getContext();
388 | final int width = getWidth();
389 | final int height = getHeight();
390 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
391 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), Toast.LENGTH_SHORT);
392 | // display below the tab
393 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, (screenPos[0] + width / 2) - screenWidth / 2, height);
394 | cheatSheet.show();
395 | return true;
396 | }
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/util/CircularUtils.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.util;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.View;
6 |
7 | /**
8 | * Created by j_cho on 2017/10/03.
9 | */
10 |
11 | public class CircularUtils {
12 |
13 | public static final int ITEM_COUNT = Integer.MAX_VALUE;
14 |
15 | public static final int START_POSITION = Integer.MAX_VALUE / 2;
16 |
17 | /**
18 | * @param size real item size
19 | * @param dummyPosition dummy positon
20 | * @return
21 | */
22 | public static int calculateRealPositionFromDummyPosition(int size, int dummyPosition) {
23 | int offset = START_POSITION % size;
24 | int position = (dummyPosition - offset) % size;
25 | return position < 0 ? position + size : position;
26 | }
27 |
28 | /**
29 | * @param size real item size
30 | * @param currentDummyPosition current dummy position
31 | * @param realPosition current real position
32 | * @return
33 | */
34 | public static int calculateDummyPositionFromRealPosition(int size, int currentDummyPosition, int realPosition) {
35 | return currentDummyPosition + calculateLoopDistance(size, calculateRealPositionFromDummyPosition(size, currentDummyPosition), realPosition);
36 | }
37 |
38 | /**
39 | * @param size real item size
40 | * @param from
41 | * @param to
42 | * @return
43 | */
44 | public static int calculateLoopDistance(int size, int from, int to) {
45 | int diff = to - from;
46 | if (diff < -size / 2) {
47 | diff += size;
48 | }
49 | if (diff > size / 2) {
50 | diff -= size;
51 | }
52 |
53 | return diff;
54 | }
55 |
56 | /**
57 | * get the center RecyclerView
58 | *
59 | * @param recyclerView
60 | * @return
61 | */
62 | public static View getCenterXChild(@NonNull RecyclerView recyclerView) {
63 | int childCount = recyclerView.getChildCount();
64 | if (childCount > 0) {
65 | for (int i = 0; i < childCount; i++) {
66 | View child = recyclerView.getChildAt(i);
67 | if (isChildInCenterX(recyclerView, child)) {
68 | return child;
69 | }
70 | }
71 | }
72 | return null;
73 | }
74 |
75 | /**
76 | * get the position of the center RecyclerView
77 | *
78 | * @param recyclerView
79 | * @return
80 | */
81 | public static int getCenterXChildPosition(@NonNull RecyclerView recyclerView) {
82 | int childCount = recyclerView.getChildCount();
83 | if (childCount > 0) {
84 | for (int i = 0; i < childCount; i++) {
85 | View child = recyclerView.getChildAt(i);
86 | if (isChildInCenterX(recyclerView, child)) {
87 | return recyclerView.getChildAdapterPosition(child);
88 | }
89 | }
90 | }
91 | return childCount;
92 | }
93 |
94 | /**
95 | * get the center RecyclerView
96 | *
97 | * @param recyclerView
98 | * @return
99 | */
100 | public static View getCenterYChild(@NonNull RecyclerView recyclerView) {
101 | int childCount = recyclerView.getChildCount();
102 | if (childCount > 0) {
103 | for (int i = 0; i < childCount; i++) {
104 | View child = recyclerView.getChildAt(i);
105 | if (isChildInCenterY(recyclerView, child)) {
106 | return child;
107 | }
108 | }
109 | }
110 | return null;
111 | }
112 |
113 | /**
114 | * get the position of the center RecyclerView
115 | *
116 | * @param recyclerView
117 | * @return
118 | */
119 | public static int getCenterYChildPosition(@NonNull RecyclerView recyclerView) {
120 | int childCount = recyclerView.getChildCount();
121 | if (childCount > 0) {
122 | for (int i = 0; i < childCount; i++) {
123 | View child = recyclerView.getChildAt(i);
124 | if (isChildInCenterY(recyclerView, child)) {
125 | return recyclerView.getChildAdapterPosition(child);
126 | }
127 | }
128 | }
129 | return childCount;
130 | }
131 |
132 | /**
133 | * @param recyclerView
134 | * @param view
135 | * @return
136 | */
137 | public static boolean isChildInCenterX(@NonNull RecyclerView recyclerView, @NonNull View view) {
138 | int childCount = recyclerView.getChildCount();
139 | int[] lvLocationOnScreen = new int[2];
140 | recyclerView.getLocationOnScreen(lvLocationOnScreen);
141 | int middleX = lvLocationOnScreen[0] + recyclerView.getWidth() / 2;
142 | if (childCount > 0) {
143 | int[] vLocationOnScreen = new int[2];
144 | view.getLocationOnScreen(vLocationOnScreen);
145 | int width = view.getWidth();
146 | if (vLocationOnScreen[0] <= middleX && vLocationOnScreen[0] + width >= middleX) {
147 | return true;
148 | }
149 | }
150 | return false;
151 | }
152 |
153 | /**
154 | * @param recyclerView
155 | * @param view
156 | * @return
157 | */
158 | public static boolean isChildInCenterY(@NonNull RecyclerView recyclerView, @NonNull View view) {
159 | int childCount = recyclerView.getChildCount();
160 | int[] lvLocationOnScreen = new int[2];
161 | recyclerView.getLocationOnScreen(lvLocationOnScreen);
162 | int middleY = lvLocationOnScreen[1] + recyclerView.getHeight() / 2;
163 | if (childCount > 0) {
164 | int[] vLocationOnScreen = new int[2];
165 | view.getLocationOnScreen(vLocationOnScreen);
166 | if (vLocationOnScreen[1] <= middleY && vLocationOnScreen[1] + view.getHeight() >= middleY) {
167 | return true;
168 | }
169 | }
170 | return false;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/CircularTabLayout.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.content.res.ColorStateList;
6 | import android.content.res.TypedArray;
7 | import android.graphics.Paint;
8 | import android.graphics.drawable.Drawable;
9 | import android.os.Build;
10 | import android.support.annotation.NonNull;
11 | import android.support.v4.view.PagerAdapter;
12 | import android.support.v4.view.ViewPager;
13 | import android.support.v7.widget.LinearLayoutManager;
14 | import android.support.v7.widget.RecyclerView;
15 | import android.util.AttributeSet;
16 | import android.view.LayoutInflater;
17 | import android.view.MotionEvent;
18 | import android.view.View;
19 | import android.view.ViewTreeObserver;
20 |
21 | import com.sanyuzhang.circular.viewpager.cvp.R;
22 | import com.sanyuzhang.circular.viewpager.cvp.adapter.CircularTabLayoutAdapter;
23 | import com.sanyuzhang.circular.viewpager.cvp.util.CircularUtils;
24 |
25 | import java.util.ArrayList;
26 | import java.util.List;
27 |
28 | import static android.support.v7.content.res.AppCompatResources.getDrawable;
29 |
30 | /**
31 | * Created by j_cho on 2017/10/03.
32 | */
33 |
34 | public class CircularTabLayout extends RecyclerView {
35 |
36 | /**
37 | * Tab default style
38 | */
39 | private static final int DEFAULT_HEIGHT = 48;
40 |
41 | private static final int TAB_MIN_WIDTH_MARGIN = 56;
42 |
43 | /**
44 | * time to scroll 1 inch
45 | */
46 | private static final float MILLISECONDS_PER_INCH = 25f;
47 |
48 | private static final float FLING_FACTOR = 0.15f;
49 |
50 | private static final float TRIGGER_OFFSET = 0.25f;
51 |
52 | /**
53 | * ItemDecoration of the RecyclerView.
54 | */
55 | private final Paint mSelectedIndicatorPaint;
56 |
57 | /**
58 | * circular ViewPager.
59 | */
60 | private ViewPager mViewPager;
61 |
62 | /**
63 | * the adapter of the CircularTabLayout
64 | */
65 | private CircularTabLayoutAdapter mCircularTabLayoutAdapter;
66 |
67 | private boolean mScrollingByManual;
68 |
69 | /**
70 | * Tab Padding.
71 | */
72 | private int mTabPaddingStart;
73 |
74 | private int mTabPaddingTop;
75 |
76 | private int mTabPaddingEnd;
77 |
78 | private int mTabPaddingBottom;
79 |
80 | /**
81 | * Tab Margins.
82 | */
83 | private int mTabMarginStart;
84 |
85 | private int mTabMarginTop;
86 |
87 | private int mTabMarginEnd;
88 |
89 | private int mTabMarginBottom;
90 |
91 | /**
92 | * Tab Texts.
93 | */
94 | private int mTabTextAppearance;
95 |
96 | private int mTabTextSize;
97 |
98 | private ColorStateList mTabTextColors;
99 |
100 | /**
101 | * Tab Background.
102 | */
103 | private int mTabBackgroundResId;
104 |
105 | /**
106 | * Tab Size.
107 | */
108 | private int mViewHeight;
109 |
110 | private int mTabMinWidth;
111 |
112 | private int mTabMaxWidth;
113 |
114 | private int mRequestedTabMaxWidth;
115 |
116 | private int mMode;
117 |
118 | private int mSelectedIndicatorHeight;
119 |
120 | /**
121 | * last selected position
122 | */
123 | private int mLastCurrentItem = CircularUtils.START_POSITION;
124 |
125 | private float mScrollMillisecondsPerInch;
126 |
127 | private ItemDecoration mItemDecoration = null;
128 |
129 | private boolean mScrollEnabled;
130 |
131 | private boolean mNeedAdjust;
132 |
133 | private int mFirstLeftWhenDragging;
134 |
135 | private View mCurrentCenterView;
136 |
137 | private int mMaxLeftWhenDragging = Integer.MIN_VALUE;
138 |
139 | private int mMinLeftWhenDragging = Integer.MAX_VALUE;
140 |
141 | private int mMaxTopWhenDragging = Integer.MIN_VALUE;
142 |
143 | private int mMinTopWhenDragging = Integer.MAX_VALUE;
144 |
145 | private float mTriggerOffset;
146 |
147 | private float mFlingFactor;
148 |
149 | private float mTouchSpan;
150 |
151 | /**
152 | * Constructor.
153 | *
154 | * @param context
155 | */
156 | public CircularTabLayout(Context context) {
157 | this(context, null);
158 | }
159 |
160 | /**
161 | * Constructor.
162 | *
163 | * @param context
164 | * @param attrs
165 | */
166 | public CircularTabLayout(Context context, AttributeSet attrs) {
167 | this(context, attrs, 0);
168 | }
169 |
170 | /**
171 | * Constructor.
172 | *
173 | * @param context
174 | * @param attrs
175 | * @param defStyle
176 | */
177 | public CircularTabLayout(Context context, AttributeSet attrs, int defStyle) {
178 | super(context, attrs, defStyle);
179 |
180 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, defStyle, R.style.Widget_Design_TabLayout);
181 | // Padding
182 | int defaultPadding = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0);
183 | mTabPaddingStart = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, defaultPadding);
184 | mTabPaddingTop = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, defaultPadding);
185 | mTabPaddingEnd = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, defaultPadding);
186 | mTabPaddingBottom = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, defaultPadding);
187 |
188 | // Text
189 | mTabTextAppearance = typedArray.getResourceId(R.styleable.TabLayout_tabTextAppearance, R.style.TextAppearance_Design_Tab);
190 | mTabTextColors = loadTextColorFromTextAppearance(mTabTextAppearance);
191 | if (typedArray.hasValue(R.styleable.TabLayout_tabTextColor)) {
192 | mTabTextColors = typedArray.getColorStateList(R.styleable.TabLayout_tabTextColor);
193 | }
194 | if (typedArray.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
195 | int selected = typedArray.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
196 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
197 | }
198 |
199 | mTabMinWidth = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 0);
200 | mRequestedTabMaxWidth = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 0);
201 | mTabBackgroundResId = typedArray.getResourceId(R.styleable.TabLayout_tabBackground, 0);
202 |
203 | mMode = typedArray.getInt(R.styleable.TabLayout_tabMode, 1);
204 |
205 | // Indicator.
206 | mSelectedIndicatorHeight = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0);
207 | mSelectedIndicatorPaint = new Paint();
208 | mSelectedIndicatorPaint.setColor(typedArray.getColor(R.styleable.TabLayout_tabIndicatorColor, 0));
209 | typedArray.recycle();
210 | typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircularTabLayout);
211 | mScrollMillisecondsPerInch = typedArray.getFloat(R.styleable.CircularTabLayout_scrollMillisecondsPerInch, MILLISECONDS_PER_INCH);
212 |
213 | int defaultMargin = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMargins, 0);
214 | mTabMarginStart = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginStart, defaultMargin);
215 | mTabMarginTop = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginTop, defaultMargin);
216 | mTabMarginEnd = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginEnd, defaultMargin);
217 | mTabMarginBottom = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginBottom, defaultMargin);
218 |
219 | mScrollEnabled = typedArray.getBoolean(R.styleable.CircularTabLayout_scrollEnabled, true);
220 | mFlingFactor = typedArray.getFloat(R.styleable.CircularTabLayout_flingFactor, FLING_FACTOR);
221 | mTriggerOffset = typedArray.getFloat(R.styleable.CircularTabLayout_triggerOffset, TRIGGER_OFFSET);
222 |
223 | mTabTextSize = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabTextSize, -1);
224 |
225 | typedArray.recycle();
226 |
227 | initialize();
228 | }
229 |
230 | /**
231 | * @param defaultColor
232 | * @param selectedColor
233 | * @return
234 | */
235 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
236 | int[][] states = new int[2][];
237 | int[] colors = new int[2];
238 | byte i = 0;
239 | states[i] = SELECTED_STATE_SET;
240 | colors[i] = selectedColor;
241 | int var5 = i + 1;
242 | states[var5] = EMPTY_STATE_SET;
243 | colors[var5] = defaultColor;
244 | ++var5;
245 | return new ColorStateList(states, colors);
246 | }
247 |
248 | /**
249 | * @param textAppearanceResId
250 | * @return
251 | */
252 | private ColorStateList loadTextColorFromTextAppearance(int textAppearanceResId) {
253 | TypedArray typedArray = getContext().obtainStyledAttributes(textAppearanceResId, R.styleable.TextAppearance);
254 |
255 | ColorStateList var3;
256 | try {
257 | var3 = typedArray.getColorStateList(R.styleable.TextAppearance_android_textColor);
258 | } finally {
259 | typedArray.recycle();
260 | }
261 | return var3;
262 | }
263 |
264 | /**
265 | * @param dps
266 | * @return
267 | */
268 | private int dpToPx(int dps) {
269 | return Math.round(this.getResources().getDisplayMetrics().density * (float) dps);
270 | }
271 |
272 | /**
273 | * @param widthMeasureSpec
274 | * @param heightMeasureSpec
275 | */
276 | @Override
277 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
278 | switch (View.MeasureSpec.getMode(heightMeasureSpec)) {
279 | case View.MeasureSpec.AT_MOST:
280 | heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
281 | Math.min(dpToPx(DEFAULT_HEIGHT), View.MeasureSpec.getSize(heightMeasureSpec)),
282 | View.MeasureSpec.EXACTLY);
283 | break;
284 | case View.MeasureSpec.UNSPECIFIED:
285 | heightMeasureSpec =
286 | View.MeasureSpec.makeMeasureSpec(dpToPx(DEFAULT_HEIGHT), View.MeasureSpec.EXACTLY);
287 | }
288 |
289 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
290 |
291 | int defaultTabMaxWidth;
292 | if (mMode == 1 && getChildCount() == 1) {
293 | View maxTabWidth = getChildAt(0);
294 | defaultTabMaxWidth = getMeasuredWidth();
295 | if (maxTabWidth.getMeasuredWidth() > defaultTabMaxWidth) {
296 | int childHeightMeasureSpec =
297 | getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(),
298 | maxTabWidth.getLayoutParams().height);
299 | int childWidthMeasureSpec =
300 | View.MeasureSpec.makeMeasureSpec(defaultTabMaxWidth, View.MeasureSpec.EXACTLY);
301 | maxTabWidth.measure(childWidthMeasureSpec, childHeightMeasureSpec);
302 | }
303 | }
304 |
305 | int maxTabWidth1 = mRequestedTabMaxWidth;
306 | defaultTabMaxWidth = getMeasuredWidth() - dpToPx(TAB_MIN_WIDTH_MARGIN);
307 | if (maxTabWidth1 == 0 || maxTabWidth1 > defaultTabMaxWidth) {
308 | maxTabWidth1 = defaultTabMaxWidth;
309 | }
310 |
311 | mTabMaxWidth = maxTabWidth1;
312 | mViewHeight = getMeasuredHeight();
313 |
314 | setTabViewStyle();
315 | }
316 |
317 | private void initialize() {
318 | // SnappyLinearLayoutManagerの初期化
319 | SnappyLinearLayoutManager snappyLinearLayoutManager =
320 | new SnappyLinearLayoutManager(getContext(), HORIZONTAL, false);
321 | snappyLinearLayoutManager.setMillisecondsPerInch(mScrollMillisecondsPerInch);
322 | setLayoutManager(snappyLinearLayoutManager);
323 | setHasFixedSize(true);
324 | setIndicatorDecoration(
325 | new TabIndicatorDecorator(mSelectedIndicatorPaint, mSelectedIndicatorHeight));
326 | }
327 |
328 | public void setScrollEnable(boolean scrollEnable) {
329 | mScrollEnabled = scrollEnable;
330 | }
331 |
332 | public void setIndicatorDecoration(@NonNull ItemDecoration decoration) {
333 | if (mItemDecoration != null) {
334 | removeItemDecoration(mItemDecoration);
335 | }
336 | mItemDecoration = decoration;
337 | addItemDecoration(decoration);
338 | }
339 |
340 | public void setupWithViewPager(@NonNull CircularViewPager CircularViewPager) {
341 | setupWithViewPager((ViewPager) CircularViewPager);
342 | }
343 |
344 | // public void setupWithViewPager(@NonNull CircularViewPager CircularViewPager) {
345 | // // version of v4.viewpager
346 | // setupWithViewPager((ViewPager) CircularViewPager);
347 | // }
348 |
349 | private void setupWithViewPager(@NonNull ViewPager viewPager) {
350 | mViewPager = viewPager;
351 |
352 | PagerAdapter adapter = viewPager.getAdapter();
353 | if (adapter == null) {
354 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
355 | }
356 |
357 | setTabsFromPagerAdapter(adapter);
358 |
359 | viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
360 | public void onPageSelected(int position) {
361 | if (!mScrollingByManual) {
362 | smoothScrollToPosition(mCircularTabLayoutAdapter.moveToPosition(position));
363 | }
364 | mScrollingByManual = false;
365 | }
366 | });
367 | }
368 |
369 | public void setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) {
370 | List list = new ArrayList<>();
371 | if (adapter instanceof ITabSetting) {
372 | for (int i = 0, count = adapter.getCount(); i < count; ++i) {
373 | list.add(((ITabSetting) adapter).getTabItem(i));
374 | }
375 | } else {
376 | for (int i = 0, count = adapter.getCount(); i < count; ++i) {
377 | list.add(new Tab(getContext()).setText(adapter.getPageTitle(i)));
378 | }
379 | }
380 |
381 | mCircularTabLayoutAdapter =
382 | new CircularTabLayoutAdapter(getContext(), new ITabSelectedListener() {
383 | @Override
384 | public void onTabItemClicked(int position) {
385 | smoothScrollToPositionFromTabSelect(position);
386 | }
387 | });
388 | mCircularTabLayoutAdapter.setTabData(list);
389 |
390 | setTabViewStyle();
391 |
392 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
393 | @SuppressWarnings("deprecation")
394 | @SuppressLint("NewApi")
395 | @Override
396 | public void onGlobalLayout() {
397 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
398 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
399 | } else {
400 | getViewTreeObserver().removeOnGlobalLayoutListener(this);
401 | }
402 |
403 | LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
404 | View targetView = linearLayoutManager.findViewByPosition(0);
405 | if (targetView != null) {
406 | RecyclerView.LayoutParams params =
407 | (RecyclerView.LayoutParams) targetView.getLayoutParams();
408 | int start = linearLayoutManager.getPaddingLeft();
409 | int end = linearLayoutManager.getWidth() - linearLayoutManager.getPaddingRight();
410 | int left = linearLayoutManager.getDecoratedLeft(targetView) - params.leftMargin;
411 | int right = linearLayoutManager.getDecoratedRight(targetView) + params.rightMargin;
412 | int offset = (start + ((end - start) / 2 - (right - left) / 2)) - left;
413 | linearLayoutManager.scrollToPositionWithOffset(
414 | mCircularTabLayoutAdapter.getCurrentPosition(), offset);
415 | }
416 | }
417 | });
418 | }
419 |
420 | /**
421 | * set the style of tab view
422 | */
423 | private void setTabViewStyle() {
424 | if (mTabMaxWidth != 0
425 | && mViewHeight != 0
426 | && getAdapter() == null
427 | && mCircularTabLayoutAdapter != null) {
428 | mCircularTabLayoutAdapter.setTabViewStyle(mViewHeight, mTabBackgroundResId,
429 | new int[]{mTabMarginStart, mTabMarginTop, mTabMarginEnd, mTabMarginBottom},
430 | new int[]{mTabPaddingStart, mTabPaddingTop, mTabPaddingEnd, mTabPaddingBottom},
431 | mTabMinWidth, mTabMaxWidth, mTabTextSize, mTabTextAppearance, mTabTextColors);
432 | setAdapter(mCircularTabLayoutAdapter);
433 | }
434 | }
435 |
436 | /**
437 | * scroll to certain tab by click event
438 | *
439 | * @param position
440 | */
441 | public void smoothScrollToPositionFromTabSelect(int position) {
442 | if (mLastCurrentItem == position) {
443 | return;
444 | }
445 |
446 | mScrollingByManual = true;
447 |
448 | smoothScrollToPosition(position);
449 | }
450 |
451 | @Override
452 | public int computeVerticalScrollRange() {
453 | if (mScrollEnabled) {
454 | return super.computeVerticalScrollRange();
455 | }
456 | return 0;
457 | }
458 |
459 | @Override
460 | public boolean onInterceptTouchEvent(MotionEvent e) {
461 | if (mScrollEnabled) {
462 | return super.onInterceptTouchEvent(e);
463 | }
464 | return false;
465 | }
466 |
467 | @Override
468 | public void smoothScrollToPosition(int position) {
469 | super.smoothScrollToPosition(position);
470 |
471 | if (mLastCurrentItem == position) {
472 | return;
473 | }
474 |
475 | if (mViewPager != null) {
476 | mViewPager.setCurrentItem(
477 | mCircularTabLayoutAdapter.calculateRealPositionFromDummyPosition(position));
478 | }
479 |
480 | mCircularTabLayoutAdapter.setCurrentPosition(position);
481 |
482 | mLastCurrentItem = position;
483 | }
484 |
485 | @Override
486 | public boolean fling(int velocityX, int velocityY) {
487 | boolean flinging =
488 | super.fling((int) (velocityX * mFlingFactor), (int) (velocityY * mFlingFactor));
489 |
490 | if (flinging) {
491 | adjustPositionX(velocityX);
492 | }
493 |
494 | return flinging;
495 | }
496 |
497 | @Override
498 | public boolean onTouchEvent(MotionEvent e) {
499 | // recording the max/min value in touch track
500 | switch (e.getAction()) {
501 | case MotionEvent.ACTION_MOVE:
502 | if (mCurrentCenterView != null) {
503 | mMaxLeftWhenDragging = Math.max(mCurrentCenterView.getLeft(), mMaxLeftWhenDragging);
504 | mMaxTopWhenDragging = Math.max(mCurrentCenterView.getTop(), mMaxTopWhenDragging);
505 | mMinLeftWhenDragging = Math.min(mCurrentCenterView.getLeft(), mMinLeftWhenDragging);
506 | mMinTopWhenDragging = Math.min(mCurrentCenterView.getTop(), mMinTopWhenDragging);
507 | }
508 | break;
509 | }
510 |
511 | return super.onTouchEvent(e);
512 | }
513 |
514 | @Override
515 | public void onScrollStateChanged(int state) {
516 | super.onScrollStateChanged(state);
517 |
518 | switch (state) {
519 | case SCROLL_STATE_DRAGGING:
520 | mNeedAdjust = true;
521 | mCurrentCenterView = CircularUtils.getCenterXChild(this);
522 | if (mCurrentCenterView != null) {
523 | mFirstLeftWhenDragging = mCurrentCenterView.getLeft();
524 | }
525 | mTouchSpan = 0;
526 | break;
527 | case SCROLL_STATE_SETTLING:
528 | mNeedAdjust = false;
529 | if (mCurrentCenterView != null) {
530 | mTouchSpan = mCurrentCenterView.getLeft() - mFirstLeftWhenDragging;
531 | } else {
532 | mTouchSpan = 0;
533 | }
534 | mCurrentCenterView = null;
535 | break;
536 | case SCROLL_STATE_IDLE:
537 | if (mNeedAdjust) {
538 | int targetPosition = CircularUtils.getCenterXChildPosition(this);
539 | if (mCurrentCenterView != null) {
540 | targetPosition = getChildAdapterPosition(mCurrentCenterView);
541 | if (targetPosition == NO_POSITION) {
542 | resetDraggingStatus(true);
543 | smoothScrollToPosition(mLastCurrentItem);
544 | return;
545 | }
546 | int spanX = mCurrentCenterView.getLeft() - mFirstLeftWhenDragging;
547 | // if user is tending to cancel paging action, don't perform position changing
548 | if (spanX > mCurrentCenterView.getWidth() * mTriggerOffset
549 | && mCurrentCenterView.getLeft() >= mMaxLeftWhenDragging) {
550 | targetPosition--;
551 | } else if (spanX < mCurrentCenterView.getWidth() * -mTriggerOffset
552 | && mCurrentCenterView.getLeft() <= mMinLeftWhenDragging) {
553 | targetPosition++;
554 | }
555 | }
556 | smoothScrollToPosition(getTargetPosition(targetPosition, getAdapter().getItemCount()));
557 | mCurrentCenterView = null;
558 | }
559 | resetDraggingStatus(false);
560 | break;
561 | }
562 | }
563 |
564 | /**
565 | * reset dragging status
566 | *
567 | * @param resetAll
568 | */
569 | private void resetDraggingStatus(boolean resetAll) {
570 | if (resetAll) {
571 | mNeedAdjust = false;
572 | mTouchSpan = 0f;
573 | mCurrentCenterView = null;
574 | }
575 |
576 | // reset
577 | mMaxLeftWhenDragging = Integer.MIN_VALUE;
578 | mMinLeftWhenDragging = Integer.MAX_VALUE;
579 | mMaxTopWhenDragging = Integer.MIN_VALUE;
580 | mMinTopWhenDragging = Integer.MAX_VALUE;
581 | }
582 |
583 | private void adjustPositionX(int velocityX) {
584 | int childCount = getChildCount();
585 | if (childCount > 0) {
586 | int curPosition = CircularUtils.getCenterXChildPosition(this);
587 | int childWidth = getWidth() - getPaddingLeft() - getPaddingRight();
588 | int flingCount = (int) (velocityX * mFlingFactor / childWidth);
589 | int targetPosition = curPosition + flingCount;
590 | targetPosition = Math.max(targetPosition, 0);
591 | targetPosition = Math.min(targetPosition, getAdapter().getItemCount() - 1);
592 | if (targetPosition == curPosition) {
593 | View centerXChild = CircularUtils.getCenterXChild(this);
594 | if (centerXChild != null) {
595 | if (mTouchSpan > centerXChild.getWidth() * mTriggerOffset * mTriggerOffset
596 | && targetPosition != 0) {
597 | targetPosition--;
598 | } else if (mTouchSpan < centerXChild.getWidth() * -mTriggerOffset
599 | && targetPosition != getAdapter().getItemCount() - 1) {
600 | targetPosition++;
601 | }
602 | }
603 | }
604 | smoothScrollToPosition(getTargetPosition(targetPosition, getAdapter().getItemCount()));
605 | }
606 | }
607 |
608 | /**
609 | * limit the position between max and min
610 | *
611 | * @param position
612 | * @param count
613 | * @return
614 | */
615 | private int getTargetPosition(int position, int count) {
616 | if (position < 0) {
617 | return 0;
618 | }
619 | if (position >= count) {
620 | return count - 1;
621 | }
622 | return position;
623 | }
624 |
625 | public interface ITabSelectedListener {
626 | void onTabItemClicked(int position);
627 | }
628 |
629 | public static final class Tab {
630 |
631 | private Context mContext;
632 |
633 | private Object mTag;
634 |
635 | private Drawable mIcon;
636 |
637 | private CharSequence mText;
638 |
639 | private CharSequence mContentDesc;
640 |
641 | private ColorStateList mColorStateList;
642 |
643 | private View mCustomView;
644 |
645 | public Tab(Context context) {
646 | mContext = context;
647 | }
648 |
649 | public Object getTag() {
650 | return mTag;
651 | }
652 |
653 | public CircularTabLayout.Tab setTag(Object tag) {
654 | mTag = tag;
655 | return this;
656 | }
657 |
658 | public View getCustomView() {
659 | return mCustomView;
660 | }
661 |
662 | public CircularTabLayout.Tab setCustomView(View view) {
663 | mCustomView = view;
664 | return this;
665 | }
666 |
667 | public CircularTabLayout.Tab setCustomView(int layoutResId) {
668 | return setCustomView(LayoutInflater.from(mContext).inflate(layoutResId, null));
669 | }
670 |
671 | public Drawable getIcon() {
672 | return mIcon;
673 | }
674 |
675 | public CircularTabLayout.Tab setIcon(Drawable icon) {
676 | mIcon = icon;
677 | return this;
678 | }
679 |
680 | public CircularTabLayout.Tab setIcon(int resId) {
681 | return setIcon(getDrawable(mContext, resId));
682 | }
683 |
684 | public CharSequence getText() {
685 | return mText;
686 | }
687 |
688 | public CircularTabLayout.Tab setText(CharSequence text) {
689 | mText = text;
690 | return this;
691 | }
692 |
693 | public CircularTabLayout.Tab setText(int resId) {
694 | return setText(mContext.getResources().getText(resId));
695 | }
696 |
697 | public CircularTabLayout.Tab setTextColorState(ColorStateList colorStateList) {
698 | mColorStateList = colorStateList;
699 | return this;
700 | }
701 |
702 | public ColorStateList getTextColorStateList() {
703 | return mColorStateList;
704 | }
705 |
706 | public CircularTabLayout.Tab setContentDescription(int resId) {
707 | return setContentDescription(mContext.getResources().getText(resId));
708 | }
709 |
710 | public CharSequence getContentDescription() {
711 | return mContentDesc;
712 | }
713 |
714 | public CircularTabLayout.Tab setContentDescription(CharSequence contentDesc) {
715 | mContentDesc = contentDesc;
716 | return this;
717 | }
718 | }
719 | }
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/CircularViewPager.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.support.v4.app.Fragment;
4 | import android.support.v4.app.FragmentManager;
5 | import android.content.Context;
6 | import android.support.annotation.NonNull;
7 | import android.support.v4.app.FragmentPagerAdapter;
8 | import android.support.v4.view.PagerAdapter;
9 | import android.support.v4.view.ViewPager;
10 | import android.util.AttributeSet;
11 | import android.view.ViewGroup;
12 |
13 | import com.sanyuzhang.circular.viewpager.cvp.util.CircularUtils;
14 |
15 | import java.util.HashMap;
16 | import java.util.Map;
17 |
18 | /**
19 | * Created by j_cho on 2017/10/03.
20 | */
21 |
22 | public class CircularViewPager extends ViewPager {
23 |
24 | private int mCurrentPosition = CircularUtils.START_POSITION;
25 |
26 | private Map mOnPageChangeListeners;
27 |
28 | private FragmentPagerAdapter mOriginalAdapter;
29 |
30 | private InternalFragmentPagerAdapter mInternalFragmentPagerAdapter;
31 |
32 | /**
33 | * Constructor.
34 | *
35 | * @param context
36 | */
37 | public CircularViewPager(Context context) {
38 | this(context, null);
39 | }
40 |
41 | /**
42 | * Constructor.
43 | *
44 | * @param context
45 | * @param attrs
46 | */
47 | public CircularViewPager(Context context, AttributeSet attrs) {
48 | super(context, attrs);
49 |
50 | mOnPageChangeListeners = new HashMap<>();
51 | }
52 |
53 | @Override
54 | public void setCurrentItem(int item) {
55 | super.setCurrentItem(movetoPos(item));
56 | }
57 |
58 | @Override
59 | public PagerAdapter getAdapter() {
60 | return mOriginalAdapter;
61 | }
62 |
63 | @Override
64 | @Deprecated
65 | public void setAdapter(PagerAdapter adapter) {
66 | super.setAdapter(null);
67 | }
68 |
69 | @Override
70 | public void addOnPageChangeListener(OnPageChangeListener listener) {
71 | mOnPageChangeListeners.put(listener, new InternalOnPageChangeListener(listener));
72 | super.addOnPageChangeListener(mOnPageChangeListeners.get(listener));
73 | }
74 |
75 | @Override
76 | public void removeOnPageChangeListener(OnPageChangeListener listener) {
77 | super.removeOnPageChangeListener(mOnPageChangeListeners.remove(listener));
78 | }
79 |
80 | private int toRealPos(int dummyPos) {
81 | int size = mOriginalAdapter.getCount();
82 | int offset = size * 3 / 2 % size;
83 | int pos = (dummyPos - offset) % size;
84 | pos = pos < 0 ? pos + size : pos;
85 | return pos;
86 | }
87 |
88 | private int toDummyPos(int realPos) {
89 | int currentRealPos = toRealPos(mCurrentPosition);
90 | return mCurrentPosition + calcLoopDistance(currentRealPos, realPos);
91 | }
92 |
93 | private int calcLoopDistance(int from, int to) {
94 | int size = mOriginalAdapter.getCount();
95 | int diff = to - from;
96 | if (diff < -size / 2) {
97 | diff += size;
98 | }
99 | if (diff > size / 2) {
100 | diff -= size;
101 | }
102 | return diff;
103 | }
104 |
105 | public int movetoPos(int realPos) {
106 | int dummyPos = toDummyPos(realPos);
107 | setCurrentPosition(dummyPos);
108 | return dummyPos;
109 | }
110 |
111 | public int getCurrentPosition() {
112 | return mCurrentPosition;
113 | }
114 |
115 | private void setCurrentPosition(int pos) {
116 | mCurrentPosition = pos;
117 | }
118 |
119 | public boolean isSelectedItem(int pos) {
120 | return mCurrentPosition == pos;
121 | }
122 |
123 | public void setFragmentAdapter(@NonNull FragmentPagerAdapter adapter,
124 | @NonNull FragmentManager fm) {
125 | mOriginalAdapter = adapter;
126 | mInternalFragmentPagerAdapter = new InternalFragmentPagerAdapter(fm);
127 |
128 | super.setAdapter(mInternalFragmentPagerAdapter);
129 | mCurrentPosition = mOriginalAdapter.getCount() * 3 / 2;
130 | super.setCurrentItem(mOriginalAdapter.getCount() * 3 / 2, false);
131 |
132 | addOnPageChangeListener(new SimpleOnPageChangeListener() {
133 | @Override
134 | public void onPageScrollStateChanged(int state) {
135 | if (state == SCROLL_STATE_IDLE) {
136 | int size = mOriginalAdapter.getCount();
137 | if (mCurrentPosition < size / 2 + size % 2) {
138 | mInternalFragmentPagerAdapter.shiftIndex(false);
139 | } else if (mCurrentPosition > size * 2 + size / 2) {
140 | mInternalFragmentPagerAdapter.shiftIndex(true);
141 | }
142 | }
143 | }
144 | });
145 | }
146 |
147 | private class InternalFragmentPagerAdapter extends FragmentPagerAdapter {
148 |
149 | private Map mTagPositionMap;
150 |
151 | public InternalFragmentPagerAdapter(FragmentManager fm) {
152 | super(fm);
153 |
154 | mTagPositionMap = new HashMap<>();
155 | }
156 |
157 | @Override
158 | public Fragment getItem(int position) {
159 | return mOriginalAdapter.getItem(toRealPos(position));
160 | }
161 |
162 | @Override
163 | public Object instantiateItem(ViewGroup container, int position) {
164 | Fragment fragment = (Fragment) super.instantiateItem(container, position);
165 | mTagPositionMap.put(fragment.getTag(), position);
166 | return fragment;
167 | }
168 |
169 | @Override
170 | public long getItemId(int position) {
171 | return toRealPos(position);
172 | }
173 |
174 | @Override
175 | public int getCount() {
176 | return mOriginalAdapter.getCount() * 3;
177 | }
178 |
179 | @Override
180 | public int getItemPosition(Object object) {
181 | return mTagPositionMap.get(((Fragment) object).getTag());
182 | }
183 |
184 | public void shiftIndex(boolean left) {
185 | int size = mOriginalAdapter.getCount();
186 |
187 | if (left) {
188 | for (Map.Entry entry : mTagPositionMap.entrySet()) {
189 | entry.setValue(entry.getValue() - size);
190 | }
191 | mCurrentPosition = mCurrentPosition - size;
192 | } else {
193 | for (Map.Entry entry : mTagPositionMap.entrySet()) {
194 | entry.setValue(entry.getValue() + size);
195 | }
196 | mCurrentPosition = mCurrentPosition + size;
197 | }
198 | notifyDataSetChanged();
199 | }
200 | }
201 |
202 | private class InternalOnPageChangeListener implements OnPageChangeListener {
203 |
204 | private final OnPageChangeListener mOriginalOnPageChangeListener;
205 |
206 | public InternalOnPageChangeListener(OnPageChangeListener org) {
207 | mOriginalOnPageChangeListener = org;
208 | }
209 |
210 | @Override
211 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
212 | mOriginalOnPageChangeListener.onPageScrolled(toRealPos(position), positionOffset,
213 | positionOffsetPixels);
214 | }
215 |
216 | @Override
217 | public void onPageSelected(int position) {
218 | mOriginalOnPageChangeListener.onPageSelected(toRealPos(position));
219 | }
220 |
221 | @Override
222 | public void onPageScrollStateChanged(int state) {
223 | mOriginalOnPageChangeListener.onPageScrollStateChanged(state);
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/ITabSetting.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | /**
4 | * Created by j_cho on 2017/10/03.
5 | */
6 |
7 | public interface ITabSetting {
8 |
9 | CircularTabLayout.Tab getTabItem(int position);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/SnappyLinearLayoutManager.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.PointF;
5 | import android.support.v7.widget.LinearLayoutManager;
6 | import android.support.v7.widget.LinearSmoothScroller;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.util.DisplayMetrics;
9 | import android.view.View;
10 |
11 | import com.sanyuzhang.circular.viewpager.cvp.adapter.CircularTabLayoutAdapter;
12 |
13 | /**
14 | * Created by j_cho on 2017/10/03.
15 | */
16 |
17 | public class SnappyLinearLayoutManager extends LinearLayoutManager {
18 |
19 | public static final String TAG = SnappyLinearLayoutManager.class.getSimpleName();
20 |
21 | private float mMillisecondsPerInch;
22 |
23 | public SnappyLinearLayoutManager(Context context) {
24 | this(context, VERTICAL, false);
25 | }
26 |
27 | public SnappyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
28 | super(context, orientation, reverseLayout);
29 | }
30 |
31 | public void setMillisecondsPerInch(float millisecondsPerInch) {
32 | mMillisecondsPerInch = millisecondsPerInch;
33 | }
34 |
35 | @Override
36 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
37 | final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
38 |
39 | // I want a behavior where the scrolling always snaps to the beginning of
40 | // the list. Snapping to end is also trivial given the default implementation.
41 | // If you need a different behavior, you may need to override more
42 | // of the LinearSmoothScrolling methods.
43 | protected int getHorizontalSnapPreference() {
44 | return SNAP_TO_ANY;
45 | }
46 |
47 | protected int getVerticalSnapPreference() {
48 | return SNAP_TO_START;
49 | }
50 |
51 | @Override
52 | public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
53 | if (snapPreference != SNAP_TO_ANY) {
54 | return super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference);
55 | } else {
56 | int viewWidth = viewEnd - viewStart;
57 | int boxWidth = boxEnd - boxStart;
58 | return (boxStart + (boxWidth / 2 - viewWidth / 2)) - viewStart;
59 | }
60 | }
61 |
62 | @Override
63 | public int calculateDxToMakeVisible(View view, int snapPreference) {
64 | return super.calculateDxToMakeVisible(view, snapPreference);
65 | }
66 |
67 | @Override
68 | public PointF computeScrollVectorForPosition(int targetPosition) {
69 | return SnappyLinearLayoutManager.this.computeScrollVectorForPosition(targetPosition);
70 | }
71 |
72 | @Override
73 | protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
74 | return mMillisecondsPerInch / displayMetrics.densityDpi;
75 | }
76 | };
77 |
78 | linearSmoothScroller.setTargetPosition(position);
79 | startSmoothScroll(linearSmoothScroller);
80 | RecyclerView.Adapter> adapter = recyclerView.getAdapter();
81 |
82 | if (adapter instanceof CircularTabLayoutAdapter) {
83 | CircularTabLayoutAdapter CircularTabLayoutAdapter = (CircularTabLayoutAdapter) adapter;
84 | CircularTabLayoutAdapter.setCurrentPosition(position);
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/TabIndicatorDecorator.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 | import android.graphics.Rect;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.view.View;
8 |
9 | import com.sanyuzhang.circular.viewpager.cvp.adapter.CircularTabLayoutAdapter;
10 |
11 | /**
12 | * Created by j_cho on 2017/10/03.
13 | */
14 |
15 | public class TabIndicatorDecorator extends RecyclerView.ItemDecoration {
16 |
17 | private Paint mPaint = new Paint();
18 |
19 | private int mIndicationHeight;
20 |
21 | public TabIndicatorDecorator(Paint paint, int height) {
22 | mPaint = paint;
23 | mIndicationHeight = height;
24 | }
25 |
26 | @Override
27 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
28 | super.onDrawOver(c, parent, state);
29 |
30 | RecyclerView.Adapter> adapter = parent.getAdapter();
31 |
32 | if (!(adapter instanceof CircularTabLayoutAdapter)) {
33 | return;
34 | }
35 |
36 | CircularTabLayoutAdapter CircularTabLayoutAdapter = (CircularTabLayoutAdapter) adapter;
37 | View target = parent.getLayoutManager().findViewByPosition(CircularTabLayoutAdapter.getCurrentPosition());
38 |
39 | if (target != null) {
40 | RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) target.getLayoutParams();
41 | int top = target.getBottom() + params.bottomMargin;
42 | int bottom = top + mIndicationHeight;
43 | c.drawRect(target.getLeft(), top, target.getRight(), bottom, mPaint);
44 | }
45 | }
46 |
47 | @Override
48 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
49 | super.getItemOffsets(outRect, view, parent, state);
50 | outRect.set(0, 0, 0, mIndicationHeight);
51 | }
52 | }
--------------------------------------------------------------------------------
/CircularViewPager/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
9 |
11 |
13 |
15 |
16 |
18 |
20 |
22 |
24 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/CircularViewPager/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | cvp
3 |
4 |
--------------------------------------------------------------------------------
/CircularViewPager/src/test/java/com/sanyuzhang/circular/viewpager/cvp/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 SanyuZhang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ###### What does this PR do?
2 |
3 | - [x] Fixes #
4 |
5 | ###### Where should the reviewer start?
6 |
7 | - [x] Reviewer @
8 | - [x] Merge
9 |
10 | ###### How did you test?
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CircularViewPager
2 | =================
3 | A Circular ViewPager for Android
4 |
5 | CircularViewPager actively supports android versions 4.0.3 (Ice Cream Sandwich) and above.
6 | That said, it works all the way down to 4.0.3 but is not actively tested or working perfectly.
7 | - [x] Supports compileSdkVersion 28.0.2
8 | - [x] Supports Gradle 4.4
9 |
10 | Here is a short gif showing the functionality you get with this library:
11 |
12 | 
13 |
14 |
15 | Goal
16 | ----
17 | The goal of this project is to deliver a high performance circular viewpager.
18 |
19 |
20 | Installing
21 | ---------------
22 | **Cloning**
23 | First of all you will have to clone the library.
24 | ```shell
25 | git clone git@github.com:sanyuzhang/CircularViewPager.git
26 | ```
27 |
28 | Now that you have the library you will have to import it into Android Studio.
29 | In Android Studio navigate the menus like this.
30 | ```
31 | File -> New -> Import Module -> CloneLocation/CircularViewPager/CircularViewPager
32 | ```
33 | Remember to add this to the build.gradle configuration of your app
34 | ```
35 | dependencies {
36 | ...
37 | compile project(':CircularViewPager')
38 | }
39 | ```
40 |
41 | In the following dialog navigate to CircularViewPager which you cloned to your computer in the previous steps and select the `build.gradle`.
42 |
43 | **Maven**
44 | Upcoming...
45 |
46 | **Gradle**
47 | Upcoming...
48 |
49 | Getting Started
50 | ---------------
51 | **Base usage**
52 |
53 | Ok lets start with your activities or fragments xml file. It might look something like this.
54 | ```xml
55 |
67 |
68 |
73 | ```
74 |
75 | Now in your activities `onCreate()` or your fragments `onCreateView()` you would want to do something like this
76 | ```java
77 | CircularTabLayout tabLayout = (CircularTabLayout) findViewById(R.id.circular_tab);
78 | CircularViewPager viewPager = (CircularViewPager) findViewById(R.id.circular_viewpager);
79 | MyAdapter adapter = new MyAdapter(getFragmentManager());
80 | viewPager.setFragmentAdapter(adapter, getFragmentManager());
81 | tabLayout.setupWithViewPager(viewPager);
82 | ```
83 |
84 | `MyAdapter` in the above example would look something like this, a list of fragments.
85 | ```java
86 | class MyAdapter extends FragmentPagerAdapter {
87 |
88 | List mFragments = new ArrayList<>();
89 |
90 | public MyAdapter(FragmentManager fm) {
91 | super(fm);
92 | for (int position = 0; position < 5; position++) {
93 | mFragments.add(new SampleFragment());
94 | }
95 | }
96 |
97 | @Override
98 | public Fragment getItem(int position) {
99 | return mFragments.get(position);
100 | }
101 |
102 | @Override
103 | public int getCount() {
104 | return mFragments.size();
105 | }
106 |
107 | @Override
108 | public CharSequence getPageTitle(int position) {
109 | return String.format("Page %d", position);
110 | }
111 |
112 | }
113 | ```
114 |
115 | That's it! Look through the API docs below to get know about things to customize and if you have any problems getting started please open an issue as it probably means the getting started guide need some improvement!
116 |
117 | **Styling**
118 |
119 | You can apply your own theme to `CircularTabLayout`. Say you define a style called `CircularTabTextAppearance` in values/styles.xml:
120 | ```xml
121 |
122 |
126 |
127 | ```
128 | Then add `CircularTabTextAppearance` to `CircularTabLayout`:
129 | ```xml
130 |
135 | ```
136 |
137 | Upcoming...
138 |
--------------------------------------------------------------------------------
/Sample/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/*
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 28
5 | buildToolsVersion "28.0.2"
6 |
7 | defaultConfig {
8 | minSdkVersion 15
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "0.6.0"
12 |
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 |
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation fileTree(dir: 'libs', include: ['*.jar'])
26 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
27 | exclude group: 'com.android.support', module: 'support-annotations'
28 | })
29 | implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
30 | implementation 'com.android.support:recyclerview-v7:28.0.0-rc02'
31 | implementation('com.android.support:design:28.0.0-rc02') {
32 | exclude module: 'support-v4'
33 | exclude module: 'appcompat-v7'
34 | }
35 | testImplementation 'junit:junit:4.12'
36 | }
37 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/j_cho/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/androidTest/java/com/sanyuzhang/circular/viewpager/cvp/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.sanyuzhang.circular.viewpager.cvp.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/adapter/CircularTabLayoutAdapter.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.adapter;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.ActionBar;
5 | import android.content.Context;
6 | import android.content.res.ColorStateList;
7 | import android.graphics.drawable.Drawable;
8 | import android.os.Build;
9 | import android.support.annotation.NonNull;
10 | import android.support.v4.view.ViewCompat;
11 | import android.support.v7.widget.AppCompatTextView;
12 | import android.support.v7.widget.RecyclerView;
13 | import android.text.TextUtils;
14 | import android.util.TypedValue;
15 | import android.view.Gravity;
16 | import android.view.View;
17 | import android.view.ViewGroup;
18 | import android.view.ViewParent;
19 | import android.view.accessibility.AccessibilityEvent;
20 | import android.view.accessibility.AccessibilityNodeInfo;
21 | import android.widget.ImageView;
22 | import android.widget.LinearLayout;
23 | import android.widget.TextView;
24 | import android.widget.Toast;
25 |
26 | import com.sanyuzhang.circular.viewpager.cvp.R;
27 | import com.sanyuzhang.circular.viewpager.cvp.util.CircularUtils;
28 | import com.sanyuzhang.circular.viewpager.cvp.view.CircularTabLayout;
29 |
30 | import java.util.List;
31 |
32 | import static android.support.v7.content.res.AppCompatResources.getDrawable;
33 |
34 | /**
35 | * Created by j_cho on 2017/10/03.
36 | */
37 |
38 | public class CircularTabLayoutAdapter extends RecyclerView.Adapter {
39 |
40 | private Context mContext;
41 |
42 | private CircularTabLayout.ITabSelectedListener mTabSelectedListener;
43 |
44 | private int mCurrentPosition = CircularUtils.START_POSITION;
45 |
46 | /**
47 | * real tab
48 | */
49 | private List mObjectList;
50 |
51 | /**
52 | * ViewHolder.itemView listener
53 | */
54 | private View.OnClickListener mOnClickListener = new View.OnClickListener() {
55 | @Override
56 | public void onClick(View v) {
57 | mTabSelectedListener.onTabItemClicked((Integer) v.getTag());
58 | }
59 | };
60 |
61 | /**
62 | * TabView Style
63 | */
64 | private int mTabViewMinimumHeight;
65 |
66 | private int mTabViewBackground;
67 |
68 | private int[] mTabViewPadding;
69 |
70 | private int[] mTabViewMarings;
71 |
72 | private int mTabMinWidth;
73 |
74 | private int mTabMaxWidth;
75 |
76 | private int mTabTextAppearance;
77 |
78 | private ColorStateList mTabTextColors;
79 |
80 | private int mTabTextSize;
81 |
82 | /**
83 | * Constructor.
84 | *
85 | * @param context
86 | * @param tabSelectedListener
87 | */
88 | public CircularTabLayoutAdapter(@NonNull Context context, @NonNull CircularTabLayout.ITabSelectedListener tabSelectedListener) {
89 | mContext = context;
90 | mTabSelectedListener = tabSelectedListener;
91 | }
92 |
93 | @Override
94 | public TabViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
95 | TabView tabView = createTabView();
96 | tabView.setId(R.id.recycler_tab_layout_tab_view);
97 | return new TabViewHolder(tabView);
98 | }
99 |
100 | @Override
101 | public void onBindViewHolder(TabViewHolder holder, int position) {
102 | int realFragmentPosition = calculateRealPositionFromDummyPosition(position);
103 | holder.itemView.setTag(position);
104 | holder.itemView.setOnClickListener(mOnClickListener);
105 | holder.tabView.update(mObjectList.get(realFragmentPosition));
106 | holder.tabView.setSelected(isSelectedItem(position));
107 | }
108 |
109 | @Override
110 | public int getItemCount() {
111 | return CircularUtils.ITEM_COUNT;
112 | }
113 |
114 | public int getCurrentPosition() {
115 | return mCurrentPosition;
116 | }
117 |
118 | /**
119 | * @param position
120 | */
121 | public void setCurrentPosition(int position) {
122 | int previousPosition = mCurrentPosition;
123 | mCurrentPosition = position;
124 | notifyItemChanged(previousPosition);
125 | notifyItemChanged(mCurrentPosition);
126 | }
127 |
128 | /**
129 | * @param position
130 | * @return
131 | */
132 | public boolean isSelectedItem(int position) {
133 | return mCurrentPosition == position;
134 | }
135 |
136 | /**
137 | * calculate the real position from DummyPosition
138 | *
139 | * @param dummyPosition
140 | * @return
141 | */
142 | public int calculateRealPositionFromDummyPosition(int dummyPosition) {
143 | return CircularUtils.calculateRealPositionFromDummyPosition(mObjectList.size(), dummyPosition);
144 | }
145 |
146 | /**
147 | * calculate DummyPosition from real position.
148 | *
149 | * @param realPosition
150 | * @return
151 | */
152 | public int calculateDummyPositionFromRealPosition(int realPosition) {
153 | return CircularUtils.calculateDummyPositionFromRealPosition(mObjectList.size(), mCurrentPosition,
154 | realPosition);
155 | }
156 |
157 | /**
158 | * @param realPosition
159 | * @return
160 | */
161 | public int moveToPosition(int realPosition) {
162 | int dummyPosition = calculateDummyPositionFromRealPosition(realPosition);
163 | setCurrentPosition(dummyPosition);
164 | return dummyPosition;
165 | }
166 |
167 | public void setTabViewStyle(int tabViewMinimumHeight, int tabViewBackground, int[] tabViewMargins,
168 | int[] tabViewPadding, int tabMinWidth, int tabMaxWidth,
169 | int tabTextSize, int tabTextAppearance, ColorStateList tabTextColors) {
170 | mTabViewMinimumHeight = tabViewMinimumHeight;
171 | mTabViewBackground = tabViewBackground;
172 | mTabViewPadding = tabViewPadding;
173 | mTabViewMarings = tabViewMargins;
174 | mTabMinWidth = tabMinWidth;
175 | mTabMaxWidth = tabMaxWidth;
176 | mTabTextSize = tabTextSize;
177 | mTabTextAppearance = tabTextAppearance;
178 | mTabTextColors = tabTextColors;
179 | }
180 |
181 | /**
182 | * @return
183 | */
184 | protected TabView createTabView() {
185 | TabView tabView = new TabView(mContext, mTabViewBackground, mTabViewMarings, mTabViewPadding, mTabMinWidth,
186 | mTabMaxWidth, mTabTextSize, mTabTextAppearance, mTabTextColors);
187 | tabView.setFocusable(true);
188 | tabView.setMinimumHeight(mTabViewMinimumHeight);
189 | return tabView;
190 | }
191 |
192 | /**
193 | * RecyclerView.Tab list
194 | *
195 | * @param objectList
196 | */
197 | public void setTabData(List objectList) {
198 | mObjectList = objectList;
199 | }
200 |
201 | static class TabViewHolder extends RecyclerView.ViewHolder {
202 |
203 | TabView tabView;
204 |
205 | public TabViewHolder(View itemView) {
206 | super(itemView);
207 | tabView = (TabView) itemView.findViewById(R.id.recycler_tab_layout_tab_view);
208 | }
209 | }
210 |
211 | static class TabView extends LinearLayout implements View.OnLongClickListener {
212 |
213 | private static final int MAX_TAB_TEXT_LINES = 2;
214 |
215 | private CircularTabLayout.Tab mTab;
216 |
217 | private TextView mTextView;
218 |
219 | private ImageView mIconView;
220 |
221 | private View mCustomView;
222 |
223 | private int mTabTextSize;
224 |
225 | private int mTabMinWidth;
226 |
227 | private int mTabMaxWidth;
228 |
229 | private int mTabTextAppearance;
230 |
231 | private ColorStateList mTabTextColors;
232 |
233 | public TabView(Context context, int background, int[] margins,
234 | int[] paddings, int tabMinWidth, int tabMaxWidth,
235 | int tabTextSize, int tabTextAppearance, ColorStateList tabTextColors) {
236 | super(context);
237 | if (background != 0) {
238 | setBackgroundDrawable(getDrawable(context, background));
239 | }
240 |
241 | ViewCompat.setPaddingRelative(this, paddings[0], paddings[1], paddings[2], paddings[3]);
242 |
243 | mTabMinWidth = tabMinWidth;
244 | mTabMaxWidth = tabMaxWidth;
245 | mTabTextSize = tabTextSize;
246 | mTabTextAppearance = tabTextAppearance;
247 | mTabTextColors = tabTextColors;
248 |
249 | MarginLayoutParams marginLayoutParams = new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT);
250 |
251 | marginLayoutParams.leftMargin = margins[0];
252 | marginLayoutParams.topMargin = margins[1];
253 | marginLayoutParams.rightMargin = margins[2];
254 | marginLayoutParams.bottomMargin = margins[3];
255 |
256 | setLayoutParams(marginLayoutParams);
257 |
258 | setGravity(Gravity.CENTER);
259 | }
260 |
261 | public void setSelected(boolean selected) {
262 | boolean changed = isSelected() != selected;
263 | super.setSelected(selected);
264 | if (changed && selected) {
265 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
266 | if (mTextView != null) {
267 | mTextView.setSelected(selected);
268 | }
269 | if (mIconView != null) {
270 | mIconView.setSelected(selected);
271 | }
272 | }
273 | }
274 |
275 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
276 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
277 | super.onInitializeAccessibilityEvent(event);
278 | event.setClassName(ActionBar.Tab.class.getName());
279 | }
280 |
281 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
282 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
283 | super.onInitializeAccessibilityNodeInfo(info);
284 | info.setClassName(ActionBar.Tab.class.getName());
285 | }
286 |
287 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
288 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
289 | if (mTabMaxWidth != 0 && getMeasuredWidth() > mTabMaxWidth) {
290 | super.onMeasure(MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
291 | } else if (mTabMinWidth > 0 && getMeasuredHeight() < mTabMinWidth) {
292 | super.onMeasure(MeasureSpec.makeMeasureSpec(mTabMinWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
293 | }
294 | }
295 |
296 | final void update(CircularTabLayout.Tab tab) {
297 | ColorStateList tabTextColors = tab.getTextColorStateList() == null ? mTabTextColors : tab.getTextColorStateList();
298 |
299 | mTab = tab;
300 | View custom = tab.getCustomView();
301 | if (custom != null) {
302 | ViewParent icon = custom.getParent();
303 | if (icon != this) {
304 | if (icon != null) {
305 | ((ViewGroup) icon).removeView(custom);
306 | }
307 | addView(custom);
308 | }
309 |
310 | mCustomView = custom;
311 | if (mTextView != null) {
312 | mTextView.setVisibility(View.GONE);
313 | }
314 |
315 | if (mIconView != null) {
316 | mIconView.setVisibility(View.GONE);
317 | mIconView.setImageDrawable(null);
318 | }
319 | } else {
320 | if (mCustomView != null) {
321 | removeView(mCustomView);
322 | mCustomView = null;
323 | }
324 |
325 | Drawable icon1 = tab.getIcon();
326 | CharSequence text = tab.getText();
327 | if (icon1 != null) {
328 | if (mIconView == null) {
329 | ImageView hasText = new ImageView(getContext());
330 | LayoutParams textView = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
331 | textView.gravity = Gravity.CENTER_VERTICAL;
332 | hasText.setLayoutParams(textView);
333 | addView(hasText, 0);
334 | mIconView = hasText;
335 | }
336 |
337 | mIconView.setImageDrawable(icon1);
338 | mIconView.setVisibility(View.VISIBLE);
339 | } else if (mIconView != null) {
340 | mIconView.setVisibility(View.GONE);
341 | mIconView.setImageDrawable(null);
342 | }
343 |
344 | boolean hasText1 = !TextUtils.isEmpty(text);
345 | if (hasText1) {
346 | if (mTextView == null) {
347 | AppCompatTextView textView1 = new AppCompatTextView(getContext());
348 | textView1.setTextAppearance(getContext(), mTabTextAppearance);
349 | if (mTabTextSize != -1) {
350 | textView1.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabTextSize);
351 | }
352 | textView1.setMaxLines(MAX_TAB_TEXT_LINES);
353 | textView1.setEllipsize(TextUtils.TruncateAt.END);
354 | textView1.setGravity(Gravity.CENTER);
355 | if (tabTextColors != null) {
356 | textView1.setTextColor(tabTextColors);
357 | }
358 | addView(textView1, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
359 | mTextView = textView1;
360 | }
361 | mTextView.setText(text);
362 | mTextView.setContentDescription(tab.getContentDescription());
363 | mTextView.setVisibility(View.VISIBLE);
364 | } else if (mTextView != null) {
365 | mTextView.setVisibility(View.GONE);
366 | mTextView.setText(null);
367 | }
368 |
369 | if (mIconView != null) {
370 | mIconView.setContentDescription(tab.getContentDescription());
371 | }
372 |
373 | if (!hasText1 && !TextUtils.isEmpty(tab.getContentDescription())) {
374 | setOnLongClickListener(this);
375 | setLongClickable(true);
376 | } else {
377 | setOnLongClickListener(null);
378 | setLongClickable(false);
379 | }
380 | }
381 | }
382 |
383 | @Override
384 | public boolean onLongClick(View v) {
385 | final int[] screenPos = new int[2];
386 | getLocationOnScreen(screenPos);
387 | final Context context = getContext();
388 | final int width = getWidth();
389 | final int height = getHeight();
390 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
391 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), Toast.LENGTH_SHORT);
392 | // display below the tab
393 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, (screenPos[0] + width / 2) - screenWidth / 2, height);
394 | cheatSheet.show();
395 | return true;
396 | }
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/util/CircularUtils.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.util;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.View;
6 |
7 | /**
8 | * Created by j_cho on 2017/10/03.
9 | */
10 |
11 | public class CircularUtils {
12 |
13 | public static final int ITEM_COUNT = Integer.MAX_VALUE;
14 |
15 | public static final int START_POSITION = Integer.MAX_VALUE / 2;
16 |
17 | /**
18 | * @param size real item size
19 | * @param dummyPosition dummy positon
20 | * @return
21 | */
22 | public static int calculateRealPositionFromDummyPosition(int size, int dummyPosition) {
23 | int offset = START_POSITION % size;
24 | int position = (dummyPosition - offset) % size;
25 | return position < 0 ? position + size : position;
26 | }
27 |
28 | /**
29 | * @param size real item size
30 | * @param currentDummyPosition current dummy position
31 | * @param realPosition current real position
32 | * @return
33 | */
34 | public static int calculateDummyPositionFromRealPosition(int size, int currentDummyPosition, int realPosition) {
35 | return currentDummyPosition + calculateLoopDistance(size, calculateRealPositionFromDummyPosition(size, currentDummyPosition), realPosition);
36 | }
37 |
38 | /**
39 | * @param size real item size
40 | * @param from
41 | * @param to
42 | * @return
43 | */
44 | public static int calculateLoopDistance(int size, int from, int to) {
45 | int diff = to - from;
46 | if (diff < -size / 2) {
47 | diff += size;
48 | }
49 | if (diff > size / 2) {
50 | diff -= size;
51 | }
52 |
53 | return diff;
54 | }
55 |
56 | /**
57 | * get the center RecyclerView
58 | *
59 | * @param recyclerView
60 | * @return
61 | */
62 | public static View getCenterXChild(@NonNull RecyclerView recyclerView) {
63 | int childCount = recyclerView.getChildCount();
64 | if (childCount > 0) {
65 | for (int i = 0; i < childCount; i++) {
66 | View child = recyclerView.getChildAt(i);
67 | if (isChildInCenterX(recyclerView, child)) {
68 | return child;
69 | }
70 | }
71 | }
72 | return null;
73 | }
74 |
75 | /**
76 | * get the position of the center RecyclerView
77 | *
78 | * @param recyclerView
79 | * @return
80 | */
81 | public static int getCenterXChildPosition(@NonNull RecyclerView recyclerView) {
82 | int childCount = recyclerView.getChildCount();
83 | if (childCount > 0) {
84 | for (int i = 0; i < childCount; i++) {
85 | View child = recyclerView.getChildAt(i);
86 | if (isChildInCenterX(recyclerView, child)) {
87 | return recyclerView.getChildAdapterPosition(child);
88 | }
89 | }
90 | }
91 | return childCount;
92 | }
93 |
94 | /**
95 | * get the center RecyclerView
96 | *
97 | * @param recyclerView
98 | * @return
99 | */
100 | public static View getCenterYChild(@NonNull RecyclerView recyclerView) {
101 | int childCount = recyclerView.getChildCount();
102 | if (childCount > 0) {
103 | for (int i = 0; i < childCount; i++) {
104 | View child = recyclerView.getChildAt(i);
105 | if (isChildInCenterY(recyclerView, child)) {
106 | return child;
107 | }
108 | }
109 | }
110 | return null;
111 | }
112 |
113 | /**
114 | * get the position of the center RecyclerView
115 | *
116 | * @param recyclerView
117 | * @return
118 | */
119 | public static int getCenterYChildPosition(@NonNull RecyclerView recyclerView) {
120 | int childCount = recyclerView.getChildCount();
121 | if (childCount > 0) {
122 | for (int i = 0; i < childCount; i++) {
123 | View child = recyclerView.getChildAt(i);
124 | if (isChildInCenterY(recyclerView, child)) {
125 | return recyclerView.getChildAdapterPosition(child);
126 | }
127 | }
128 | }
129 | return childCount;
130 | }
131 |
132 | /**
133 | * @param recyclerView
134 | * @param view
135 | * @return
136 | */
137 | public static boolean isChildInCenterX(@NonNull RecyclerView recyclerView, @NonNull View view) {
138 | int childCount = recyclerView.getChildCount();
139 | int[] lvLocationOnScreen = new int[2];
140 | recyclerView.getLocationOnScreen(lvLocationOnScreen);
141 | int middleX = lvLocationOnScreen[0] + recyclerView.getWidth() / 2;
142 | if (childCount > 0) {
143 | int[] vLocationOnScreen = new int[2];
144 | view.getLocationOnScreen(vLocationOnScreen);
145 | int width = view.getWidth();
146 | if (vLocationOnScreen[0] <= middleX && vLocationOnScreen[0] + width >= middleX) {
147 | return true;
148 | }
149 | }
150 | return false;
151 | }
152 |
153 | /**
154 | * @param recyclerView
155 | * @param view
156 | * @return
157 | */
158 | public static boolean isChildInCenterY(@NonNull RecyclerView recyclerView, @NonNull View view) {
159 | int childCount = recyclerView.getChildCount();
160 | int[] lvLocationOnScreen = new int[2];
161 | recyclerView.getLocationOnScreen(lvLocationOnScreen);
162 | int middleY = lvLocationOnScreen[1] + recyclerView.getHeight() / 2;
163 | if (childCount > 0) {
164 | int[] vLocationOnScreen = new int[2];
165 | view.getLocationOnScreen(vLocationOnScreen);
166 | if (vLocationOnScreen[1] <= middleY && vLocationOnScreen[1] + view.getHeight() >= middleY) {
167 | return true;
168 | }
169 | }
170 | return false;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/CircularTabLayout.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.content.res.ColorStateList;
6 | import android.content.res.TypedArray;
7 | import android.graphics.Paint;
8 | import android.graphics.drawable.Drawable;
9 | import android.os.Build;
10 | import android.support.annotation.NonNull;
11 | import android.support.v4.view.PagerAdapter;
12 | import android.support.v4.view.ViewPager;
13 | import android.support.v7.widget.LinearLayoutManager;
14 | import android.support.v7.widget.RecyclerView;
15 | import android.util.AttributeSet;
16 | import android.view.LayoutInflater;
17 | import android.view.MotionEvent;
18 | import android.view.View;
19 | import android.view.ViewTreeObserver;
20 |
21 | import com.sanyuzhang.circular.viewpager.cvp.R;
22 | import com.sanyuzhang.circular.viewpager.cvp.adapter.CircularTabLayoutAdapter;
23 | import com.sanyuzhang.circular.viewpager.cvp.util.CircularUtils;
24 |
25 | import java.util.ArrayList;
26 | import java.util.List;
27 |
28 | import static android.support.v7.content.res.AppCompatResources.getDrawable;
29 |
30 | /**
31 | * Created by j_cho on 2017/10/03.
32 | */
33 |
34 | public class CircularTabLayout extends RecyclerView {
35 |
36 | /**
37 | * Tab default style
38 | */
39 | private static final int DEFAULT_HEIGHT = 48;
40 |
41 | private static final int TAB_MIN_WIDTH_MARGIN = 56;
42 |
43 | /**
44 | * time to scroll 1 inch
45 | */
46 | private static final float MILLISECONDS_PER_INCH = 25f;
47 |
48 | private static final float FLING_FACTOR = 0.15f;
49 |
50 | private static final float TRIGGER_OFFSET = 0.25f;
51 |
52 | /**
53 | * ItemDecoration of the RecyclerView.
54 | */
55 | private final Paint mSelectedIndicatorPaint;
56 |
57 | /**
58 | * circular ViewPager.
59 | */
60 | private ViewPager mViewPager;
61 |
62 | /**
63 | * the adapter of the CircularTabLayout
64 | */
65 | private CircularTabLayoutAdapter mCircularTabLayoutAdapter;
66 |
67 | private boolean mScrollingByManual;
68 |
69 | /**
70 | * Tab Padding.
71 | */
72 | private int mTabPaddingStart;
73 |
74 | private int mTabPaddingTop;
75 |
76 | private int mTabPaddingEnd;
77 |
78 | private int mTabPaddingBottom;
79 |
80 | /**
81 | * Tab Margins.
82 | */
83 | private int mTabMarginStart;
84 |
85 | private int mTabMarginTop;
86 |
87 | private int mTabMarginEnd;
88 |
89 | private int mTabMarginBottom;
90 |
91 | /**
92 | * Tab Texts.
93 | */
94 | private int mTabTextAppearance;
95 |
96 | private int mTabTextSize;
97 |
98 | private ColorStateList mTabTextColors;
99 |
100 | /**
101 | * Tab Background.
102 | */
103 | private int mTabBackgroundResId;
104 |
105 | /**
106 | * Tab Size.
107 | */
108 | private int mViewHeight;
109 |
110 | private int mTabMinWidth;
111 |
112 | private int mTabMaxWidth;
113 |
114 | private int mRequestedTabMaxWidth;
115 |
116 | private int mMode;
117 |
118 | private int mSelectedIndicatorHeight;
119 |
120 | /**
121 | * last selected position
122 | */
123 | private int mLastCurrentItem = CircularUtils.START_POSITION;
124 |
125 | private float mScrollMillisecondsPerInch;
126 |
127 | private ItemDecoration mItemDecoration = null;
128 |
129 | private boolean mScrollEnabled;
130 |
131 | private boolean mNeedAdjust;
132 |
133 | private int mFirstLeftWhenDragging;
134 |
135 | private View mCurrentCenterView;
136 |
137 | private int mMaxLeftWhenDragging = Integer.MIN_VALUE;
138 |
139 | private int mMinLeftWhenDragging = Integer.MAX_VALUE;
140 |
141 | private int mMaxTopWhenDragging = Integer.MIN_VALUE;
142 |
143 | private int mMinTopWhenDragging = Integer.MAX_VALUE;
144 |
145 | private float mTriggerOffset;
146 |
147 | private float mFlingFactor;
148 |
149 | private float mTouchSpan;
150 |
151 | /**
152 | * Constructor.
153 | *
154 | * @param context
155 | */
156 | public CircularTabLayout(Context context) {
157 | this(context, null);
158 | }
159 |
160 | /**
161 | * Constructor.
162 | *
163 | * @param context
164 | * @param attrs
165 | */
166 | public CircularTabLayout(Context context, AttributeSet attrs) {
167 | this(context, attrs, 0);
168 | }
169 |
170 | /**
171 | * Constructor.
172 | *
173 | * @param context
174 | * @param attrs
175 | * @param defStyle
176 | */
177 | public CircularTabLayout(Context context, AttributeSet attrs, int defStyle) {
178 | super(context, attrs, defStyle);
179 |
180 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, defStyle, R.style.Widget_Design_TabLayout);
181 | // Padding
182 | int defaultPadding = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0);
183 | mTabPaddingStart = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, defaultPadding);
184 | mTabPaddingTop = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, defaultPadding);
185 | mTabPaddingEnd = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, defaultPadding);
186 | mTabPaddingBottom = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, defaultPadding);
187 |
188 | // Text
189 | mTabTextAppearance = typedArray.getResourceId(R.styleable.TabLayout_tabTextAppearance, R.style.TextAppearance_Design_Tab);
190 | mTabTextColors = loadTextColorFromTextAppearance(mTabTextAppearance);
191 | if (typedArray.hasValue(R.styleable.TabLayout_tabTextColor)) {
192 | mTabTextColors = typedArray.getColorStateList(R.styleable.TabLayout_tabTextColor);
193 | }
194 | if (typedArray.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
195 | int selected = typedArray.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
196 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
197 | }
198 |
199 | mTabMinWidth = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 0);
200 | mRequestedTabMaxWidth = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 0);
201 | mTabBackgroundResId = typedArray.getResourceId(R.styleable.TabLayout_tabBackground, 0);
202 |
203 | mMode = typedArray.getInt(R.styleable.TabLayout_tabMode, 1);
204 |
205 | // Indicator.
206 | mSelectedIndicatorHeight = typedArray.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0);
207 | mSelectedIndicatorPaint = new Paint();
208 | mSelectedIndicatorPaint.setColor(typedArray.getColor(R.styleable.TabLayout_tabIndicatorColor, 0));
209 | typedArray.recycle();
210 | typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircularTabLayout);
211 | mScrollMillisecondsPerInch = typedArray.getFloat(R.styleable.CircularTabLayout_scrollMillisecondsPerInch, MILLISECONDS_PER_INCH);
212 |
213 | int defaultMargin = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMargins, 0);
214 | mTabMarginStart = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginStart, defaultMargin);
215 | mTabMarginTop = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginTop, defaultMargin);
216 | mTabMarginEnd = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginEnd, defaultMargin);
217 | mTabMarginBottom = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabMarginBottom, defaultMargin);
218 |
219 | mScrollEnabled = typedArray.getBoolean(R.styleable.CircularTabLayout_scrollEnabled, true);
220 | mFlingFactor = typedArray.getFloat(R.styleable.CircularTabLayout_flingFactor, FLING_FACTOR);
221 | mTriggerOffset = typedArray.getFloat(R.styleable.CircularTabLayout_triggerOffset, TRIGGER_OFFSET);
222 |
223 | mTabTextSize = typedArray.getDimensionPixelSize(R.styleable.CircularTabLayout_tabTextSize, -1);
224 |
225 | typedArray.recycle();
226 |
227 | initialize();
228 | }
229 |
230 | /**
231 | * @param defaultColor
232 | * @param selectedColor
233 | * @return
234 | */
235 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
236 | int[][] states = new int[2][];
237 | int[] colors = new int[2];
238 | byte i = 0;
239 | states[i] = SELECTED_STATE_SET;
240 | colors[i] = selectedColor;
241 | int var5 = i + 1;
242 | states[var5] = EMPTY_STATE_SET;
243 | colors[var5] = defaultColor;
244 | ++var5;
245 | return new ColorStateList(states, colors);
246 | }
247 |
248 | /**
249 | * @param textAppearanceResId
250 | * @return
251 | */
252 | private ColorStateList loadTextColorFromTextAppearance(int textAppearanceResId) {
253 | TypedArray typedArray = getContext().obtainStyledAttributes(textAppearanceResId, R.styleable.TextAppearance);
254 |
255 | ColorStateList var3;
256 | try {
257 | var3 = typedArray.getColorStateList(R.styleable.TextAppearance_android_textColor);
258 | } finally {
259 | typedArray.recycle();
260 | }
261 | return var3;
262 | }
263 |
264 | /**
265 | * @param dps
266 | * @return
267 | */
268 | private int dpToPx(int dps) {
269 | return Math.round(this.getResources().getDisplayMetrics().density * (float) dps);
270 | }
271 |
272 | /**
273 | * @param widthMeasureSpec
274 | * @param heightMeasureSpec
275 | */
276 | @Override
277 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
278 | switch (View.MeasureSpec.getMode(heightMeasureSpec)) {
279 | case View.MeasureSpec.AT_MOST:
280 | heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
281 | Math.min(dpToPx(DEFAULT_HEIGHT), View.MeasureSpec.getSize(heightMeasureSpec)),
282 | View.MeasureSpec.EXACTLY);
283 | break;
284 | case View.MeasureSpec.UNSPECIFIED:
285 | heightMeasureSpec =
286 | View.MeasureSpec.makeMeasureSpec(dpToPx(DEFAULT_HEIGHT), View.MeasureSpec.EXACTLY);
287 | }
288 |
289 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
290 |
291 | int defaultTabMaxWidth;
292 | if (mMode == 1 && getChildCount() == 1) {
293 | View maxTabWidth = getChildAt(0);
294 | defaultTabMaxWidth = getMeasuredWidth();
295 | if (maxTabWidth.getMeasuredWidth() > defaultTabMaxWidth) {
296 | int childHeightMeasureSpec =
297 | getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(),
298 | maxTabWidth.getLayoutParams().height);
299 | int childWidthMeasureSpec =
300 | View.MeasureSpec.makeMeasureSpec(defaultTabMaxWidth, View.MeasureSpec.EXACTLY);
301 | maxTabWidth.measure(childWidthMeasureSpec, childHeightMeasureSpec);
302 | }
303 | }
304 |
305 | int maxTabWidth1 = mRequestedTabMaxWidth;
306 | defaultTabMaxWidth = getMeasuredWidth() - dpToPx(TAB_MIN_WIDTH_MARGIN);
307 | if (maxTabWidth1 == 0 || maxTabWidth1 > defaultTabMaxWidth) {
308 | maxTabWidth1 = defaultTabMaxWidth;
309 | }
310 |
311 | mTabMaxWidth = maxTabWidth1;
312 | mViewHeight = getMeasuredHeight();
313 |
314 | setTabViewStyle();
315 | }
316 |
317 | private void initialize() {
318 | // SnappyLinearLayoutManagerの初期化
319 | SnappyLinearLayoutManager snappyLinearLayoutManager =
320 | new SnappyLinearLayoutManager(getContext(), HORIZONTAL, false);
321 | snappyLinearLayoutManager.setMillisecondsPerInch(mScrollMillisecondsPerInch);
322 | setLayoutManager(snappyLinearLayoutManager);
323 | setHasFixedSize(true);
324 | setIndicatorDecoration(
325 | new TabIndicatorDecorator(mSelectedIndicatorPaint, mSelectedIndicatorHeight));
326 | }
327 |
328 | public void setScrollEnable(boolean scrollEnable) {
329 | mScrollEnabled = scrollEnable;
330 | }
331 |
332 | public void setIndicatorDecoration(@NonNull ItemDecoration decoration) {
333 | if (mItemDecoration != null) {
334 | removeItemDecoration(mItemDecoration);
335 | }
336 | mItemDecoration = decoration;
337 | addItemDecoration(decoration);
338 | }
339 |
340 | public void setupWithViewPager(@NonNull CircularViewPager CircularViewPager) {
341 | setupWithViewPager((ViewPager) CircularViewPager);
342 | }
343 |
344 | // public void setupWithViewPager(@NonNull CircularViewPager CircularViewPager) {
345 | // // version of v4.viewpager
346 | // setupWithViewPager((ViewPager) CircularViewPager);
347 | // }
348 |
349 | private void setupWithViewPager(@NonNull ViewPager viewPager) {
350 | mViewPager = viewPager;
351 |
352 | PagerAdapter adapter = viewPager.getAdapter();
353 | if (adapter == null) {
354 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
355 | }
356 |
357 | setTabsFromPagerAdapter(adapter);
358 |
359 | viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
360 | public void onPageSelected(int position) {
361 | if (!mScrollingByManual) {
362 | smoothScrollToPosition(mCircularTabLayoutAdapter.moveToPosition(position));
363 | }
364 | mScrollingByManual = false;
365 | }
366 | });
367 | }
368 |
369 | public void setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) {
370 | List list = new ArrayList<>();
371 | if (adapter instanceof ITabSetting) {
372 | for (int i = 0, count = adapter.getCount(); i < count; ++i) {
373 | list.add(((ITabSetting) adapter).getTabItem(i));
374 | }
375 | } else {
376 | for (int i = 0, count = adapter.getCount(); i < count; ++i) {
377 | list.add(new Tab(getContext()).setText(adapter.getPageTitle(i)));
378 | }
379 | }
380 |
381 | mCircularTabLayoutAdapter =
382 | new CircularTabLayoutAdapter(getContext(), new ITabSelectedListener() {
383 | @Override
384 | public void onTabItemClicked(int position) {
385 | smoothScrollToPositionFromTabSelect(position);
386 | }
387 | });
388 | mCircularTabLayoutAdapter.setTabData(list);
389 |
390 | setTabViewStyle();
391 |
392 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
393 | @SuppressWarnings("deprecation")
394 | @SuppressLint("NewApi")
395 | @Override
396 | public void onGlobalLayout() {
397 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
398 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
399 | } else {
400 | getViewTreeObserver().removeOnGlobalLayoutListener(this);
401 | }
402 |
403 | LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
404 | View targetView = linearLayoutManager.findViewByPosition(0);
405 | if (targetView != null) {
406 | RecyclerView.LayoutParams params =
407 | (RecyclerView.LayoutParams) targetView.getLayoutParams();
408 | int start = linearLayoutManager.getPaddingLeft();
409 | int end = linearLayoutManager.getWidth() - linearLayoutManager.getPaddingRight();
410 | int left = linearLayoutManager.getDecoratedLeft(targetView) - params.leftMargin;
411 | int right = linearLayoutManager.getDecoratedRight(targetView) + params.rightMargin;
412 | int offset = (start + ((end - start) / 2 - (right - left) / 2)) - left;
413 | linearLayoutManager.scrollToPositionWithOffset(
414 | mCircularTabLayoutAdapter.getCurrentPosition(), offset);
415 | }
416 | }
417 | });
418 | }
419 |
420 | /**
421 | * set the style of tab view
422 | */
423 | private void setTabViewStyle() {
424 | if (mTabMaxWidth != 0
425 | && mViewHeight != 0
426 | && getAdapter() == null
427 | && mCircularTabLayoutAdapter != null) {
428 | mCircularTabLayoutAdapter.setTabViewStyle(mViewHeight, mTabBackgroundResId,
429 | new int[]{mTabMarginStart, mTabMarginTop, mTabMarginEnd, mTabMarginBottom},
430 | new int[]{mTabPaddingStart, mTabPaddingTop, mTabPaddingEnd, mTabPaddingBottom},
431 | mTabMinWidth, mTabMaxWidth, mTabTextSize, mTabTextAppearance, mTabTextColors);
432 | setAdapter(mCircularTabLayoutAdapter);
433 | }
434 | }
435 |
436 | /**
437 | * scroll to certain tab by click event
438 | *
439 | * @param position
440 | */
441 | public void smoothScrollToPositionFromTabSelect(int position) {
442 | if (mLastCurrentItem == position) {
443 | return;
444 | }
445 |
446 | mScrollingByManual = true;
447 |
448 | smoothScrollToPosition(position);
449 | }
450 |
451 | @Override
452 | public int computeVerticalScrollRange() {
453 | if (mScrollEnabled) {
454 | return super.computeVerticalScrollRange();
455 | }
456 | return 0;
457 | }
458 |
459 | @Override
460 | public boolean onInterceptTouchEvent(MotionEvent e) {
461 | if (mScrollEnabled) {
462 | return super.onInterceptTouchEvent(e);
463 | }
464 | return false;
465 | }
466 |
467 | @Override
468 | public void smoothScrollToPosition(int position) {
469 | super.smoothScrollToPosition(position);
470 |
471 | if (mLastCurrentItem == position) {
472 | return;
473 | }
474 |
475 | if (mViewPager != null) {
476 | mViewPager.setCurrentItem(
477 | mCircularTabLayoutAdapter.calculateRealPositionFromDummyPosition(position));
478 | }
479 |
480 | mCircularTabLayoutAdapter.setCurrentPosition(position);
481 |
482 | mLastCurrentItem = position;
483 | }
484 |
485 | @Override
486 | public boolean fling(int velocityX, int velocityY) {
487 | boolean flinging =
488 | super.fling((int) (velocityX * mFlingFactor), (int) (velocityY * mFlingFactor));
489 |
490 | if (flinging) {
491 | adjustPositionX(velocityX);
492 | }
493 |
494 | return flinging;
495 | }
496 |
497 | @Override
498 | public boolean onTouchEvent(MotionEvent e) {
499 | // recording the max/min value in touch track
500 | switch (e.getAction()) {
501 | case MotionEvent.ACTION_MOVE:
502 | if (mCurrentCenterView != null) {
503 | mMaxLeftWhenDragging = Math.max(mCurrentCenterView.getLeft(), mMaxLeftWhenDragging);
504 | mMaxTopWhenDragging = Math.max(mCurrentCenterView.getTop(), mMaxTopWhenDragging);
505 | mMinLeftWhenDragging = Math.min(mCurrentCenterView.getLeft(), mMinLeftWhenDragging);
506 | mMinTopWhenDragging = Math.min(mCurrentCenterView.getTop(), mMinTopWhenDragging);
507 | }
508 | break;
509 | }
510 |
511 | return super.onTouchEvent(e);
512 | }
513 |
514 | @Override
515 | public void onScrollStateChanged(int state) {
516 | super.onScrollStateChanged(state);
517 |
518 | switch (state) {
519 | case SCROLL_STATE_DRAGGING:
520 | mNeedAdjust = true;
521 | mCurrentCenterView = CircularUtils.getCenterXChild(this);
522 | if (mCurrentCenterView != null) {
523 | mFirstLeftWhenDragging = mCurrentCenterView.getLeft();
524 | }
525 | mTouchSpan = 0;
526 | break;
527 | case SCROLL_STATE_SETTLING:
528 | mNeedAdjust = false;
529 | if (mCurrentCenterView != null) {
530 | mTouchSpan = mCurrentCenterView.getLeft() - mFirstLeftWhenDragging;
531 | } else {
532 | mTouchSpan = 0;
533 | }
534 | mCurrentCenterView = null;
535 | break;
536 | case SCROLL_STATE_IDLE:
537 | if (mNeedAdjust) {
538 | int targetPosition = CircularUtils.getCenterXChildPosition(this);
539 | if (mCurrentCenterView != null) {
540 | targetPosition = getChildAdapterPosition(mCurrentCenterView);
541 | if (targetPosition == NO_POSITION) {
542 | resetDraggingStatus(true);
543 | smoothScrollToPosition(mLastCurrentItem);
544 | return;
545 | }
546 | int spanX = mCurrentCenterView.getLeft() - mFirstLeftWhenDragging;
547 | // if user is tending to cancel paging action, don't perform position changing
548 | if (spanX > mCurrentCenterView.getWidth() * mTriggerOffset
549 | && mCurrentCenterView.getLeft() >= mMaxLeftWhenDragging) {
550 | targetPosition--;
551 | } else if (spanX < mCurrentCenterView.getWidth() * -mTriggerOffset
552 | && mCurrentCenterView.getLeft() <= mMinLeftWhenDragging) {
553 | targetPosition++;
554 | }
555 | }
556 | smoothScrollToPosition(getTargetPosition(targetPosition, getAdapter().getItemCount()));
557 | mCurrentCenterView = null;
558 | }
559 | resetDraggingStatus(false);
560 | break;
561 | }
562 | }
563 |
564 | /**
565 | * reset dragging status
566 | *
567 | * @param resetAll
568 | */
569 | private void resetDraggingStatus(boolean resetAll) {
570 | if (resetAll) {
571 | mNeedAdjust = false;
572 | mTouchSpan = 0f;
573 | mCurrentCenterView = null;
574 | }
575 |
576 | // reset
577 | mMaxLeftWhenDragging = Integer.MIN_VALUE;
578 | mMinLeftWhenDragging = Integer.MAX_VALUE;
579 | mMaxTopWhenDragging = Integer.MIN_VALUE;
580 | mMinTopWhenDragging = Integer.MAX_VALUE;
581 | }
582 |
583 | private void adjustPositionX(int velocityX) {
584 | int childCount = getChildCount();
585 | if (childCount > 0) {
586 | int curPosition = CircularUtils.getCenterXChildPosition(this);
587 | int childWidth = getWidth() - getPaddingLeft() - getPaddingRight();
588 | int flingCount = (int) (velocityX * mFlingFactor / childWidth);
589 | int targetPosition = curPosition + flingCount;
590 | targetPosition = Math.max(targetPosition, 0);
591 | targetPosition = Math.min(targetPosition, getAdapter().getItemCount() - 1);
592 | if (targetPosition == curPosition) {
593 | View centerXChild = CircularUtils.getCenterXChild(this);
594 | if (centerXChild != null) {
595 | if (mTouchSpan > centerXChild.getWidth() * mTriggerOffset * mTriggerOffset
596 | && targetPosition != 0) {
597 | targetPosition--;
598 | } else if (mTouchSpan < centerXChild.getWidth() * -mTriggerOffset
599 | && targetPosition != getAdapter().getItemCount() - 1) {
600 | targetPosition++;
601 | }
602 | }
603 | }
604 | smoothScrollToPosition(getTargetPosition(targetPosition, getAdapter().getItemCount()));
605 | }
606 | }
607 |
608 | /**
609 | * limit the position between max and min
610 | *
611 | * @param position
612 | * @param count
613 | * @return
614 | */
615 | private int getTargetPosition(int position, int count) {
616 | if (position < 0) {
617 | return 0;
618 | }
619 | if (position >= count) {
620 | return count - 1;
621 | }
622 | return position;
623 | }
624 |
625 | public interface ITabSelectedListener {
626 | void onTabItemClicked(int position);
627 | }
628 |
629 | public static final class Tab {
630 |
631 | private Context mContext;
632 |
633 | private Object mTag;
634 |
635 | private Drawable mIcon;
636 |
637 | private CharSequence mText;
638 |
639 | private CharSequence mContentDesc;
640 |
641 | private ColorStateList mColorStateList;
642 |
643 | private View mCustomView;
644 |
645 | public Tab(Context context) {
646 | mContext = context;
647 | }
648 |
649 | public Object getTag() {
650 | return mTag;
651 | }
652 |
653 | public CircularTabLayout.Tab setTag(Object tag) {
654 | mTag = tag;
655 | return this;
656 | }
657 |
658 | public View getCustomView() {
659 | return mCustomView;
660 | }
661 |
662 | public CircularTabLayout.Tab setCustomView(View view) {
663 | mCustomView = view;
664 | return this;
665 | }
666 |
667 | public CircularTabLayout.Tab setCustomView(int layoutResId) {
668 | return setCustomView(LayoutInflater.from(mContext).inflate(layoutResId, null));
669 | }
670 |
671 | public Drawable getIcon() {
672 | return mIcon;
673 | }
674 |
675 | public CircularTabLayout.Tab setIcon(Drawable icon) {
676 | mIcon = icon;
677 | return this;
678 | }
679 |
680 | public CircularTabLayout.Tab setIcon(int resId) {
681 | return setIcon(getDrawable(mContext, resId));
682 | }
683 |
684 | public CharSequence getText() {
685 | return mText;
686 | }
687 |
688 | public CircularTabLayout.Tab setText(CharSequence text) {
689 | mText = text;
690 | return this;
691 | }
692 |
693 | public CircularTabLayout.Tab setText(int resId) {
694 | return setText(mContext.getResources().getText(resId));
695 | }
696 |
697 | public CircularTabLayout.Tab setTextColorState(ColorStateList colorStateList) {
698 | mColorStateList = colorStateList;
699 | return this;
700 | }
701 |
702 | public ColorStateList getTextColorStateList() {
703 | return mColorStateList;
704 | }
705 |
706 | public CircularTabLayout.Tab setContentDescription(int resId) {
707 | return setContentDescription(mContext.getResources().getText(resId));
708 | }
709 |
710 | public CharSequence getContentDescription() {
711 | return mContentDesc;
712 | }
713 |
714 | public CircularTabLayout.Tab setContentDescription(CharSequence contentDesc) {
715 | mContentDesc = contentDesc;
716 | return this;
717 | }
718 | }
719 | }
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/CircularViewPager.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.support.v4.app.Fragment;
4 | import android.support.v4.app.FragmentManager;
5 | import android.content.Context;
6 | import android.support.annotation.NonNull;
7 | import android.support.v4.app.FragmentPagerAdapter;
8 | import android.support.v4.view.PagerAdapter;
9 | import android.support.v4.view.ViewPager;
10 | import android.util.AttributeSet;
11 | import android.view.ViewGroup;
12 |
13 | import com.sanyuzhang.circular.viewpager.cvp.util.CircularUtils;
14 |
15 | import java.util.HashMap;
16 | import java.util.Map;
17 |
18 | /**
19 | * Created by j_cho on 2017/10/03.
20 | */
21 |
22 | public class CircularViewPager extends ViewPager {
23 |
24 | private int mCurrentPosition = CircularUtils.START_POSITION;
25 |
26 | private Map mOnPageChangeListeners;
27 |
28 | private FragmentPagerAdapter mOriginalAdapter;
29 |
30 | private InternalFragmentPagerAdapter mInternalFragmentPagerAdapter;
31 |
32 | /**
33 | * Constructor.
34 | *
35 | * @param context
36 | */
37 | public CircularViewPager(Context context) {
38 | this(context, null);
39 | }
40 |
41 | /**
42 | * Constructor.
43 | *
44 | * @param context
45 | * @param attrs
46 | */
47 | public CircularViewPager(Context context, AttributeSet attrs) {
48 | super(context, attrs);
49 |
50 | mOnPageChangeListeners = new HashMap<>();
51 | }
52 |
53 | @Override
54 | public void setCurrentItem(int item) {
55 | super.setCurrentItem(movetoPos(item));
56 | }
57 |
58 | @Override
59 | public PagerAdapter getAdapter() {
60 | return mOriginalAdapter;
61 | }
62 |
63 | @Override
64 | @Deprecated
65 | public void setAdapter(PagerAdapter adapter) {
66 | super.setAdapter(null);
67 | }
68 |
69 | @Override
70 | public void addOnPageChangeListener(OnPageChangeListener listener) {
71 | mOnPageChangeListeners.put(listener, new InternalOnPageChangeListener(listener));
72 | super.addOnPageChangeListener(mOnPageChangeListeners.get(listener));
73 | }
74 |
75 | @Override
76 | public void removeOnPageChangeListener(OnPageChangeListener listener) {
77 | super.removeOnPageChangeListener(mOnPageChangeListeners.remove(listener));
78 | }
79 |
80 | private int toRealPos(int dummyPos) {
81 | int size = mOriginalAdapter.getCount();
82 | int offset = size * 3 / 2 % size;
83 | int pos = (dummyPos - offset) % size;
84 | pos = pos < 0 ? pos + size : pos;
85 | return pos;
86 | }
87 |
88 | private int toDummyPos(int realPos) {
89 | int currentRealPos = toRealPos(mCurrentPosition);
90 | return mCurrentPosition + calcLoopDistance(currentRealPos, realPos);
91 | }
92 |
93 | private int calcLoopDistance(int from, int to) {
94 | int size = mOriginalAdapter.getCount();
95 | int diff = to - from;
96 | if (diff < -size / 2) {
97 | diff += size;
98 | }
99 | if (diff > size / 2) {
100 | diff -= size;
101 | }
102 | return diff;
103 | }
104 |
105 | public int movetoPos(int realPos) {
106 | int dummyPos = toDummyPos(realPos);
107 | setCurrentPosition(dummyPos);
108 | return dummyPos;
109 | }
110 |
111 | public int getCurrentPosition() {
112 | return mCurrentPosition;
113 | }
114 |
115 | private void setCurrentPosition(int pos) {
116 | mCurrentPosition = pos;
117 | }
118 |
119 | public boolean isSelectedItem(int pos) {
120 | return mCurrentPosition == pos;
121 | }
122 |
123 | public void setFragmentAdapter(@NonNull FragmentPagerAdapter adapter,
124 | @NonNull FragmentManager fm) {
125 | mOriginalAdapter = adapter;
126 | mInternalFragmentPagerAdapter = new InternalFragmentPagerAdapter(fm);
127 |
128 | super.setAdapter(mInternalFragmentPagerAdapter);
129 | mCurrentPosition = mOriginalAdapter.getCount() * 3 / 2;
130 | super.setCurrentItem(mOriginalAdapter.getCount() * 3 / 2, false);
131 |
132 | addOnPageChangeListener(new SimpleOnPageChangeListener() {
133 | @Override
134 | public void onPageScrollStateChanged(int state) {
135 | if (state == SCROLL_STATE_IDLE) {
136 | int size = mOriginalAdapter.getCount();
137 | if (mCurrentPosition < size / 2 + size % 2) {
138 | mInternalFragmentPagerAdapter.shiftIndex(false);
139 | } else if (mCurrentPosition > size * 2 + size / 2) {
140 | mInternalFragmentPagerAdapter.shiftIndex(true);
141 | }
142 | }
143 | }
144 | });
145 | }
146 |
147 | private class InternalFragmentPagerAdapter extends FragmentPagerAdapter {
148 |
149 | private Map mTagPositionMap;
150 |
151 | public InternalFragmentPagerAdapter(FragmentManager fm) {
152 | super(fm);
153 |
154 | mTagPositionMap = new HashMap<>();
155 | }
156 |
157 | @Override
158 | public Fragment getItem(int position) {
159 | return mOriginalAdapter.getItem(toRealPos(position));
160 | }
161 |
162 | @Override
163 | public Object instantiateItem(ViewGroup container, int position) {
164 | Fragment fragment = (Fragment) super.instantiateItem(container, position);
165 | mTagPositionMap.put(fragment.getTag(), position);
166 | return fragment;
167 | }
168 |
169 | @Override
170 | public long getItemId(int position) {
171 | return toRealPos(position);
172 | }
173 |
174 | @Override
175 | public int getCount() {
176 | return mOriginalAdapter.getCount() * 3;
177 | }
178 |
179 | @Override
180 | public int getItemPosition(Object object) {
181 | return mTagPositionMap.get(((Fragment) object).getTag());
182 | }
183 |
184 | public void shiftIndex(boolean left) {
185 | int size = mOriginalAdapter.getCount();
186 |
187 | if (left) {
188 | for (Map.Entry entry : mTagPositionMap.entrySet()) {
189 | entry.setValue(entry.getValue() - size);
190 | }
191 | mCurrentPosition = mCurrentPosition - size;
192 | } else {
193 | for (Map.Entry entry : mTagPositionMap.entrySet()) {
194 | entry.setValue(entry.getValue() + size);
195 | }
196 | mCurrentPosition = mCurrentPosition + size;
197 | }
198 | notifyDataSetChanged();
199 | }
200 | }
201 |
202 | private class InternalOnPageChangeListener implements OnPageChangeListener {
203 |
204 | private final OnPageChangeListener mOriginalOnPageChangeListener;
205 |
206 | public InternalOnPageChangeListener(OnPageChangeListener org) {
207 | mOriginalOnPageChangeListener = org;
208 | }
209 |
210 | @Override
211 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
212 | mOriginalOnPageChangeListener.onPageScrolled(toRealPos(position), positionOffset,
213 | positionOffsetPixels);
214 | }
215 |
216 | @Override
217 | public void onPageSelected(int position) {
218 | mOriginalOnPageChangeListener.onPageSelected(toRealPos(position));
219 | }
220 |
221 | @Override
222 | public void onPageScrollStateChanged(int state) {
223 | mOriginalOnPageChangeListener.onPageScrollStateChanged(state);
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/ITabSetting.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | /**
4 | * Created by j_cho on 2017/10/03.
5 | */
6 |
7 | public interface ITabSetting {
8 |
9 | CircularTabLayout.Tab getTabItem(int position);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/SnappyLinearLayoutManager.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.PointF;
5 | import android.support.v7.widget.LinearLayoutManager;
6 | import android.support.v7.widget.LinearSmoothScroller;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.util.DisplayMetrics;
9 | import android.view.View;
10 |
11 | import com.sanyuzhang.circular.viewpager.cvp.adapter.CircularTabLayoutAdapter;
12 |
13 | /**
14 | * Created by j_cho on 2017/10/03.
15 | */
16 |
17 | public class SnappyLinearLayoutManager extends LinearLayoutManager {
18 |
19 | public static final String TAG = SnappyLinearLayoutManager.class.getSimpleName();
20 |
21 | private float mMillisecondsPerInch;
22 |
23 | public SnappyLinearLayoutManager(Context context) {
24 | this(context, VERTICAL, false);
25 | }
26 |
27 | public SnappyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
28 | super(context, orientation, reverseLayout);
29 | }
30 |
31 | public void setMillisecondsPerInch(float millisecondsPerInch) {
32 | mMillisecondsPerInch = millisecondsPerInch;
33 | }
34 |
35 | @Override
36 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
37 | final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
38 |
39 | // I want a behavior where the scrolling always snaps to the beginning of
40 | // the list. Snapping to end is also trivial given the default implementation.
41 | // If you need a different behavior, you may need to override more
42 | // of the LinearSmoothScrolling methods.
43 | protected int getHorizontalSnapPreference() {
44 | return SNAP_TO_ANY;
45 | }
46 |
47 | protected int getVerticalSnapPreference() {
48 | return SNAP_TO_START;
49 | }
50 |
51 | @Override
52 | public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
53 | if (snapPreference != SNAP_TO_ANY) {
54 | return super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference);
55 | } else {
56 | int viewWidth = viewEnd - viewStart;
57 | int boxWidth = boxEnd - boxStart;
58 | return (boxStart + (boxWidth / 2 - viewWidth / 2)) - viewStart;
59 | }
60 | }
61 |
62 | @Override
63 | public int calculateDxToMakeVisible(View view, int snapPreference) {
64 | return super.calculateDxToMakeVisible(view, snapPreference);
65 | }
66 |
67 | @Override
68 | public PointF computeScrollVectorForPosition(int targetPosition) {
69 | return SnappyLinearLayoutManager.this.computeScrollVectorForPosition(targetPosition);
70 | }
71 |
72 | @Override
73 | protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
74 | return mMillisecondsPerInch / displayMetrics.densityDpi;
75 | }
76 | };
77 |
78 | linearSmoothScroller.setTargetPosition(position);
79 | startSmoothScroll(linearSmoothScroller);
80 | RecyclerView.Adapter> adapter = recyclerView.getAdapter();
81 |
82 | if (adapter instanceof CircularTabLayoutAdapter) {
83 | CircularTabLayoutAdapter CircularTabLayoutAdapter = (CircularTabLayoutAdapter) adapter;
84 | CircularTabLayoutAdapter.setCurrentPosition(position);
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/java/com/sanyuzhang/circular/viewpager/cvp/view/TabIndicatorDecorator.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp.view;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 | import android.graphics.Rect;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.view.View;
8 |
9 | import com.sanyuzhang.circular.viewpager.cvp.adapter.CircularTabLayoutAdapter;
10 |
11 | /**
12 | * Created by j_cho on 2017/10/03.
13 | */
14 |
15 | public class TabIndicatorDecorator extends RecyclerView.ItemDecoration {
16 |
17 | private Paint mPaint = new Paint();
18 |
19 | private int mIndicationHeight;
20 |
21 | public TabIndicatorDecorator(Paint paint, int height) {
22 | mPaint = paint;
23 | mIndicationHeight = height;
24 | }
25 |
26 | @Override
27 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
28 | super.onDrawOver(c, parent, state);
29 |
30 | RecyclerView.Adapter> adapter = parent.getAdapter();
31 |
32 | if (!(adapter instanceof CircularTabLayoutAdapter)) {
33 | return;
34 | }
35 |
36 | CircularTabLayoutAdapter CircularTabLayoutAdapter = (CircularTabLayoutAdapter) adapter;
37 | View target = parent.getLayoutManager().findViewByPosition(CircularTabLayoutAdapter.getCurrentPosition());
38 |
39 | if (target != null) {
40 | RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) target.getLayoutParams();
41 | int top = target.getBottom() + params.bottomMargin;
42 | int bottom = top + mIndicationHeight;
43 | c.drawRect(target.getLeft(), top, target.getRight(), bottom, mPaint);
44 | }
45 | }
46 |
47 | @Override
48 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
49 | super.getItemOffsets(outRect, view, parent, state);
50 | outRect.set(0, 0, 0, mIndicationHeight);
51 | }
52 | }
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
9 |
11 |
13 |
15 |
16 |
18 |
20 |
22 |
24 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | cvp
3 |
4 |
--------------------------------------------------------------------------------
/Sample/CircularViewPager/src/test/java/com/sanyuzhang/circular/viewpager/cvp/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.cvp;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/Sample/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Sample/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | buildToolsVersion "28.0.2"
6 | defaultConfig {
7 | applicationId "com.sanyuzhang.circular.viewpager.sample"
8 | minSdkVersion 15
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation fileTree(dir: 'libs', include: ['*.jar'])
24 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
25 | exclude group: 'com.android.support', module: 'support-annotations'
26 | })
27 | implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
28 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
29 | implementation 'com.android.support:design:28.0.0-rc02'
30 | testImplementation 'junit:junit:4.12'
31 |
32 | implementation project(':CircularViewPager')
33 | }
34 |
--------------------------------------------------------------------------------
/Sample/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/j_cho/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/Sample/app/src/androidTest/java/com/sanyuzhang/circular/viewpager/sample/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.sample;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.sanyuzhang.circular.viewpager.sample", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sample/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Sample/app/src/main/java/com/sanyuzhang/circular/viewpager/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.sample;
2 |
3 | import android.support.v4.app.Fragment;
4 | import android.support.v4.app.FragmentManager;
5 | import android.os.Bundle;
6 | import android.support.v4.app.FragmentPagerAdapter;
7 | import android.support.v7.app.AppCompatActivity;
8 | import android.support.v7.widget.Toolbar;
9 | import android.view.Menu;
10 | import android.view.MenuItem;
11 |
12 | import com.sanyuzhang.circular.viewpager.cvp.view.CircularTabLayout;
13 | import com.sanyuzhang.circular.viewpager.cvp.view.CircularViewPager;
14 |
15 | import java.util.ArrayList;
16 | import java.util.List;
17 |
18 |
19 | public class MainActivity extends AppCompatActivity {
20 |
21 | @Override
22 | protected void onCreate(Bundle savedInstanceState) {
23 | super.onCreate(savedInstanceState);
24 | setContentView(R.layout.activity_main);
25 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
26 | setSupportActionBar(toolbar);
27 |
28 | CircularTabLayout tabLayout = (CircularTabLayout) findViewById(R.id.circular_tab);
29 | CircularViewPager viewPager = (CircularViewPager) findViewById(R.id.circular_viewpager);
30 |
31 | SampleFragmentPagerAdapter adapter = new SampleFragmentPagerAdapter(getSupportFragmentManager());
32 |
33 | viewPager.setFragmentAdapter(adapter, getSupportFragmentManager());
34 | tabLayout.setupWithViewPager(viewPager);
35 | }
36 |
37 | @Override
38 | public boolean onCreateOptionsMenu(Menu menu) {
39 | getMenuInflater().inflate(R.menu.menu_main, menu);
40 | return true;
41 | }
42 |
43 | @Override
44 | public boolean onOptionsItemSelected(MenuItem item) {
45 | int id = item.getItemId();
46 | if (id == R.id.action_settings) {
47 | return true;
48 | }
49 | return super.onOptionsItemSelected(item);
50 | }
51 |
52 | class SampleFragmentPagerAdapter extends FragmentPagerAdapter {
53 |
54 | List mFragments = new ArrayList<>();
55 |
56 | public SampleFragmentPagerAdapter(FragmentManager fm) {
57 | super(fm);
58 | for (int position = 0; position < 5; position++) {
59 | mFragments.add(SampleFragment.newInstance(String.format("%d", position)));
60 | }
61 | }
62 |
63 | @Override
64 | public Fragment getItem(int position) {
65 | return mFragments.get(position);
66 | }
67 |
68 | @Override
69 | public int getCount() {
70 | return mFragments.size();
71 | }
72 |
73 | @Override
74 | public CharSequence getPageTitle(int position) {
75 | return String.format("Page %d", position);
76 | }
77 |
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sample/app/src/main/java/com/sanyuzhang/circular/viewpager/sample/SampleFragment.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.sample;
2 |
3 | import android.support.v4.app.Fragment;
4 | import android.support.annotation.Nullable;
5 | import android.os.Bundle;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.widget.TextView;
10 |
11 | /**
12 | * A placeholder fragment containing a simple view.
13 | */
14 | public class SampleFragment extends Fragment {
15 |
16 | public static SampleFragment newInstance(String message) {
17 | SampleFragment fragment = new SampleFragment();
18 | Bundle arg = new Bundle();
19 | arg.putString("message", message);
20 | fragment.setArguments(arg);
21 | return fragment;
22 | }
23 |
24 | @Override
25 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
26 | return inflater.inflate(R.layout.fragment_sample, container, false);
27 | }
28 |
29 | @Override
30 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
31 | super.onViewCreated(view, savedInstanceState);
32 | Bundle arg = getArguments();
33 | ((TextView) view.findViewById(R.id.message)).setText(arg.getString("message", "1"));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
17 |
18 |
23 |
24 |
30 |
31 |
32 |
33 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/layout/fragment_sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/Sample/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | #CCCCCC
5 | #000000
6 |
7 | #AAAAAA
8 | #AAAAAA
9 | #AAAAAA
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16sp
3 | 60sp
4 | 12sp
5 | 24dp
6 | 2dp
7 |
8 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Circular Viewpager
3 | Settings
4 |
5 |
--------------------------------------------------------------------------------
/Sample/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Sample/app/src/test/java/com/sanyuzhang/circular/viewpager/sample/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.sanyuzhang.circular.viewpager.sample;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/Sample/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 | maven {
7 | url 'https://maven.google.com/'
8 | name 'Google'
9 | }
10 | google()
11 | }
12 | dependencies {
13 | classpath 'com.android.tools.build:gradle:3.1.4'
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
20 | allprojects {
21 | repositories {
22 | jcenter()
23 | maven {
24 | url 'https://maven.google.com/'
25 | name 'Google'
26 | }
27 | }
28 | }
29 |
30 | task clean(type: Delete) {
31 | delete rootProject.buildDir
32 | }
33 |
--------------------------------------------------------------------------------
/Sample/demo/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/demo/demo.gif
--------------------------------------------------------------------------------
/Sample/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/Sample/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanyuzhang/CircularViewPager/18a7c1f81995546a8ea52c95d61d806bc2669d0a/Sample/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Sample/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Sep 20 16:52:12 EDT 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
7 |
--------------------------------------------------------------------------------
/Sample/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/Sample/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/Sample/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':CircularViewPager'
2 |
--------------------------------------------------------------------------------