├── .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 | ![Demo Gif](https://github.com/sanyuzhang/CircularViewPager/raw/master/Sample/demo/demo.gif) 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 | 5 | 10 | 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 | 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 | --------------------------------------------------------------------------------