├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle ├── lint.xml └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ ├── com │ │ └── hansoolabs │ │ │ └── calendarproto │ │ │ ├── HLog.java │ │ │ ├── MConfig.java │ │ │ ├── MainActivity.java │ │ │ ├── MonthlyFragment.java │ │ │ ├── MonthlyPagerAdapter.java │ │ │ ├── MonthlyView.java │ │ │ └── cal │ │ │ ├── OneDayData.java │ │ │ ├── OneDayView.java │ │ │ ├── OneMonthView.java │ │ │ ├── WeatherInfo.java │ │ │ └── YearMonth.java │ └── fr │ │ └── castorflex │ │ └── android │ │ └── verticalviewpager │ │ └── VerticalViewPager.java │ └── res │ ├── drawable-xhdpi │ ├── cloudy.png │ ├── rainy.png │ ├── snowy.png │ └── sunny.png │ ├── drawable │ ├── day_cell_bg.xml │ ├── dot.xml │ └── lineframe.xml │ ├── layout │ ├── activity_main.xml │ ├── fragment_monthly.xml │ └── oneday.xml │ ├── menu │ ├── calendar_menu.xml │ └── main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proto.gif └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: brownsoo 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | .idea 20 | 21 | # Android Studio 22 | *.iml 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Vertically Scrollable Calendar Workout 2 | 3 | [![codebeat badge](https://codebeat.co/badges/a450e182-9250-4d6f-8c45-7819d6d90b31)](https://codebeat.co/projects/github-com-brownsoo-android-vertically-scrollable-calendar-prototype-master) 4 | 5 | This is the Android sample project making the Vertically scrollable calendar for study. 6 | 7 | ![Proto Image](proto.gif) 8 | 9 | ## 세로형 무한 스크롤 달력 프로토타입 (안드로이드) 10 | 11 | 이 저장소는 안드로이드에서 세로 스크롤이 가능한 달력을 만들기 위해 만든 목업샘플입니다. 12 | 13 | ### Check logic point 1 14 | 특정 날짜와 페이지 위치를 동기화하기 위해 기본 날짜를 만듭니다. 그리고 수천 페이지 중간에 기본 위치도 생성합니다. 15 | 16 | ```java 17 | /** Default year to calculate the page position */ 18 | final static int BASE_YEAR = 2015; 19 | /** Default month to calculate the page position */ 20 | final static int BASE_MONTH = Calendar.JANUARY; 21 | /** Calendar instance based on default year and month */ 22 | final Calendar BASE_CAL; 23 | /** Page numbers to reuse */ 24 | final static int PAGES = 5; 25 | /** Loops, I think 1000 may be infinite scroll. */ 26 | final static int LOOPS = 1000; 27 | /** position basis */ 28 | final static int BASE_POSITION = PAGES * LOOPS / 2; 29 | ... 30 | Calendar base = Calendar.getInstance(); 31 | base.set(BASE_YEAR, BASE_MONTH, 1); 32 | BASE_CAL = base; 33 | ... 34 | ``` 35 | 36 | ### Check logic point 2 37 | 그런 다음 페이지 위치별로 특정 날짜를 얻을 수 있습니다. 38 | 39 | ```java 40 | public YearMonth getYearMonth(int position) { 41 | Calendar cal = (Calendar)BASE_CAL.clone(); 42 | cal.add(Calendar.MONTH, position - BASE_POSITION); 43 | return new YearMonth(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)); 44 | } 45 | ``` 46 | 47 | ### Check logic point 3 48 | 주어진 날짜별로 특정 페이지 위치를 얻을 수 있습니다. 49 | 50 | ```java 51 | /** 52 | * Get the page position by given date 53 | * @param year 4 digits number of year 54 | * @param month month number 55 | * @return page position 56 | */ 57 | public int getPosition(int year, int month) { 58 | Calendar cal = Calendar.getInstance(); 59 | cal.set(year, month, 1); 60 | return BASE_POSITION + howFarFromBase(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)); 61 | } 62 | 63 | /** 64 | * How many months exist from the base month to the given values? 65 | * @param year the year to compare with the base year 66 | * @param month the month to compare with the base month 67 | * @return counts of month 68 | */ 69 | private int howFarFromBase(int year, int month) { 70 | int disY = (year - BASE_YEAR) * 12; 71 | int disM = month - BASE_MONTH; 72 | return disY + disM; 73 | } 74 | ``` 75 | 76 | 77 | In this project, I embed [`VerticalViewPager`](https://github.com/castorflex/VerticalViewPager) for just vertical view pager. And I refer to ['SimpleInfiniteCarousel'](https://github.com/mrleolink/SimpleInfiniteCarousel) to make a simple infinite carousel with ViewPager on Android. 78 | 79 | You can see sample [video](https://youtu.be/sHpk8f0WY7U). 80 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace "com.hansoolabs.calendarproto" 5 | compileSdk 34 6 | 7 | defaultConfig { 8 | applicationId "com.hansoolabs.calendarproto" 9 | minSdkVersion 24 10 | targetSdkVersion 34 11 | versionCode 100 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 19 | } 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_17 24 | targetCompatibility = JavaVersion.VERSION_17 25 | } 26 | 27 | } 28 | 29 | dependencies { 30 | implementation 'androidx.appcompat:appcompat:1.6.1' 31 | } 32 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/HLog.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto; 2 | 3 | import android.util.Log; 4 | 5 | /** @noinspection unused*/ 6 | public class HLog { 7 | 8 | public static boolean isDebugMode = true; 9 | public static void e(String TAG, String CLASS, String msg) { 10 | if(isDebugMode) { 11 | String THREAD = Thread.currentThread().getName(); 12 | String text = "[" + THREAD + "] " + CLASS + " " + msg; 13 | Log.e(TAG, text); 14 | } 15 | } 16 | 17 | public static void w(String TAG, String CLASS, String msg) { 18 | if(isDebugMode) { 19 | String THREAD = Thread.currentThread().getName(); 20 | String text = "[" + THREAD + "] " + CLASS + " " + msg; 21 | Log.w(TAG, text); 22 | } 23 | } 24 | 25 | public static void i(String TAG, String CLASS, String msg) { 26 | if(isDebugMode) { 27 | String THREAD = Thread.currentThread().getName(); 28 | String text = "[" + THREAD + "] " + CLASS + " " + msg; 29 | Log.i(TAG, text); 30 | } 31 | } 32 | 33 | public static void d(String TAG, String CLASS, String msg) { 34 | if(isDebugMode) { 35 | String THREAD = Thread.currentThread().getName(); 36 | String text = "[" + THREAD + "] " + CLASS + " " + msg; 37 | Log.d(TAG, text); 38 | } 39 | } 40 | 41 | public static void v(String TAG, String CLASS, String msg) { 42 | if(isDebugMode) { 43 | String THREAD = Thread.currentThread().getName(); 44 | String text = "[" + THREAD + "] " + CLASS + " " + msg; 45 | Log.v(TAG, text); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/MConfig.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto; 2 | 3 | /** 4 | * Default App Config 5 | * Created by brownsoo on 2014. 10. 4.. 6 | */ 7 | public class MConfig { 8 | 9 | public static final String TAG = "proto"; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto; 2 | 3 | import android.os.Bundle; 4 | import androidx.fragment.app.FragmentActivity; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | import android.widget.TextView; 8 | import android.widget.Toast; 9 | 10 | import com.hansoolabs.calendarproto.cal.OneDayView; 11 | 12 | import java.util.Calendar; 13 | 14 | 15 | public class MainActivity extends FragmentActivity { 16 | 17 | private static final String TAG = MConfig.TAG; 18 | private static final String NAME = "MainActivity"; 19 | private final String CLASS = NAME + "@" + Integer.toHexString(hashCode()); 20 | 21 | private TextView thisMonthTv; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_main); 27 | 28 | thisMonthTv = findViewById(R.id.this_month_tv); 29 | 30 | MonthlyFragment mf = (MonthlyFragment) getSupportFragmentManager().findFragmentById(R.id.monthly); 31 | assert mf != null; 32 | mf.setOnMonthChangeListener(new MonthlyFragment.MonthlyFragmentListener() { 33 | 34 | @Override 35 | public void onChange(int year, int month) { 36 | HLog.d(TAG, CLASS, "onChange " + year + "." + month); 37 | thisMonthTv.setText(year + "." + (month + 1)); 38 | } 39 | 40 | @Override 41 | public void onDayClick(OneDayView dayView) { 42 | Toast.makeText(MainActivity.this, "Click " + dayView.get(Calendar.MONTH) + "/" + dayView.get(Calendar.DAY_OF_MONTH), Toast.LENGTH_SHORT) 43 | .show(); 44 | } 45 | }); 46 | 47 | } 48 | 49 | 50 | @Override 51 | public boolean onCreateOptionsMenu(Menu menu) { 52 | getMenuInflater().inflate(R.menu.main, menu); 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean onOptionsItemSelected(MenuItem item) { 58 | int id = item.getItemId(); 59 | return id == R.id.action_settings || super.onOptionsItemSelected(item); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/MonthlyFragment.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | import androidx.fragment.app.Fragment; 9 | 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | import com.hansoolabs.calendarproto.cal.OneDayView; 15 | 16 | import java.util.Calendar; 17 | 18 | import fr.castorflex.android.verticalviewpager.VerticalViewPager; 19 | 20 | 21 | 22 | /** 23 | * Fragment for displaying the monthly-calendar 24 | * @author brownsoo 25 | * 26 | * @noinspection unused 27 | */ 28 | public class MonthlyFragment extends Fragment implements MonthlyView { 29 | 30 | private static final String TAG = MConfig.TAG; 31 | private final String klass = "MonthlyFragment@" + Integer.toHexString(hashCode()); 32 | 33 | private static final String ARG_YEAR = "year"; 34 | private static final String ARG_MONTH = "month"; 35 | 36 | /** 37 | * Callback when current month is changed 38 | * @author Brownsoo 39 | * 40 | */ 41 | public interface MonthlyFragmentListener { 42 | /** 43 | * Notify current month is changed 44 | * @param year 4 digits number of year 45 | * @param month number of month (0~11) 46 | */ 47 | void onChange(int year, int month); 48 | 49 | /** 50 | * Callback for clicking on a day cell 51 | * @param dayView OneDayView instance that dispatching this callback 52 | */ 53 | void onDayClick(OneDayView dayView); 54 | } 55 | 56 | @Nullable 57 | private MonthlyFragmentListener listener = null; 58 | private VerticalViewPager vvPager; 59 | private MonthlyPagerAdapter adapter; 60 | private int mYear = -1; 61 | private int mMonth = -1; 62 | 63 | /** 64 | * Make new month view via given year and month 65 | * @param year YYYY 66 | * @param month m 67 | * @return new fragment 68 | */ 69 | public static MonthlyFragment newInstance(int year, int month) { 70 | MonthlyFragment fragment = new MonthlyFragment(); 71 | Bundle args = new Bundle(); 72 | args.putInt(ARG_YEAR, year); 73 | args.putInt(ARG_MONTH, month); 74 | fragment.setArguments(args); 75 | return fragment; 76 | } 77 | public MonthlyFragment() { 78 | // Required empty public constructor 79 | } 80 | 81 | @Override 82 | public void onCreate(Bundle savedInstanceState) { 83 | super.onCreate(savedInstanceState); 84 | 85 | if (getArguments() != null) { 86 | mYear = getArguments().getInt(ARG_YEAR); 87 | mMonth = getArguments().getInt(ARG_MONTH); 88 | } 89 | else { 90 | Calendar now = Calendar.getInstance(); 91 | mYear = now.get(Calendar.YEAR); 92 | mMonth = now.get(Calendar.MONTH); 93 | } 94 | 95 | HLog.d(TAG, klass, "onCreate " + mYear + "." + mMonth); 96 | 97 | } 98 | 99 | @Override 100 | public void onAttach(@NonNull Context context) { 101 | super.onAttach(context); 102 | adapter = new MonthlyPagerAdapter(context, this); 103 | } 104 | 105 | @Override 106 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 107 | Bundle savedInstanceState) { 108 | 109 | View v = inflater.inflate(R.layout.fragment_monthly, container, false); 110 | vvPager = v.findViewById(R.id.vviewPager); 111 | vvPager.setAdapter(adapter); 112 | vvPager.setOnPageChangeListener(adapter); 113 | vvPager.setCurrentItem(adapter.getPosition(mYear, mMonth)); 114 | vvPager.setOffscreenPageLimit(1); 115 | 116 | return v; 117 | } 118 | 119 | @Override 120 | public void onDetach() { 121 | setOnMonthChangeListener(null); 122 | super.onDetach(); 123 | } 124 | 125 | public void setOnMonthChangeListener(MonthlyFragmentListener listener) { 126 | this.listener = listener; 127 | } 128 | 129 | // implements MonthlyView 130 | 131 | @Override 132 | public void onClickDay(@NonNull OneDayView odv) { 133 | if (listener != null) listener.onDayClick(odv); 134 | } 135 | 136 | @Override 137 | public void onMonthChanged(int year, int month) { 138 | if (listener != null) listener.onChange(year, month); 139 | } 140 | 141 | @Override 142 | public int getCurrentPosition() { 143 | return vvPager.getCurrentItem(); 144 | } 145 | // -- 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/MonthlyPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | import com.hansoolabs.calendarproto.cal.OneDayView; 8 | import com.hansoolabs.calendarproto.cal.OneMonthView; 9 | import com.hansoolabs.calendarproto.cal.YearMonth; 10 | 11 | import java.util.Calendar; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.viewpager.widget.PagerAdapter; 16 | import androidx.viewpager.widget.ViewPager; 17 | 18 | /** 19 | * 20 | * PagerAdapter to view calendar monthly 21 | * 22 | * @author Brownsoo 23 | * 24 | */ 25 | class MonthlyPagerAdapter extends PagerAdapter 26 | implements ViewPager.OnPageChangeListener, OneMonthView.OnClickDayListener { 27 | 28 | private static final String TAG = MConfig.TAG; 29 | private final String klass = "MonthlyPagerAdapter@" + Integer.toHexString(hashCode()); 30 | @Nullable 31 | private final MonthlyView monthlyView; 32 | @NonNull 33 | private final OneMonthView[] monthViews; 34 | /** Default year to calculate the page position */ 35 | private final static int BASE_YEAR = 2018; 36 | /** Default month to calculate the page position */ 37 | private final static int BASE_MONTH = Calendar.JANUARY; 38 | /** Calendar instance based on default year and month */ 39 | @NonNull 40 | private final Calendar BASE_CAL; 41 | /** Page numbers to reuse */ 42 | private final static int PAGES = 5; 43 | /** Inner virtual pages, I think it may be infinite scroll. */ 44 | private final static int TOTAL_PAGES = Integer.MAX_VALUE; 45 | /** position basis */ 46 | private final static int BASE_POSITION = TOTAL_PAGES / 2; 47 | /** previous position */ 48 | private int previousPosition; 49 | 50 | public MonthlyPagerAdapter(@NonNull Context context, @Nullable MonthlyView monthlyView) { 51 | this.monthlyView = monthlyView; 52 | Calendar base = Calendar.getInstance(); 53 | base.set(BASE_YEAR, BASE_MONTH, 1); 54 | BASE_CAL = base; 55 | 56 | monthViews = new OneMonthView[PAGES]; 57 | for(int i = 0; i < PAGES; i++) { 58 | monthViews[i] = new OneMonthView(context); 59 | } 60 | } 61 | 62 | /** 63 | * Get the particular date by page position 64 | * @param position page position 65 | * @return YearMonth 66 | */ 67 | public YearMonth getYearMonth(int position) { 68 | Calendar cal = (Calendar)BASE_CAL.clone(); 69 | cal.add(Calendar.MONTH, position - BASE_POSITION); 70 | return new YearMonth(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)); 71 | } 72 | 73 | /** 74 | * Get the page position by given date 75 | * @param year 4 digits number of year 76 | * @param month month number 77 | * @return page position 78 | */ 79 | public int getPosition(int year, int month) { 80 | Calendar cal = Calendar.getInstance(); 81 | cal.set(year, month, 1); 82 | return BASE_POSITION + howFarFromBase(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)); 83 | } 84 | 85 | /** 86 | * How many months exist from the base month to the given values? 87 | * @param year the year to compare with the base year 88 | * @param month the month to compare with the base month 89 | * @return counts of month 90 | */ 91 | private int howFarFromBase(int year, int month) { 92 | 93 | int disY = (year - BASE_YEAR) * 12; 94 | int disM = month - BASE_MONTH; 95 | 96 | return disY + disM; 97 | } 98 | 99 | @NonNull 100 | @Override 101 | public Object instantiateItem(@NonNull ViewGroup container, int position) { 102 | 103 | HLog.d(TAG, klass, "instantiateItem " + position); 104 | 105 | int howFarFromBase = position - BASE_POSITION; 106 | Calendar cal = (Calendar) BASE_CAL.clone(); 107 | cal.add(Calendar.MONTH, howFarFromBase); 108 | 109 | position = position % PAGES; 110 | 111 | container.addView(monthViews[position]); 112 | 113 | monthViews[position].make(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)); 114 | monthViews[position].setOnClickDayListener(this); 115 | 116 | return monthViews[position]; 117 | } 118 | 119 | @Override 120 | public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 121 | HLog.d(TAG, klass, "destroyItem " + position); 122 | ((OneMonthView) object).setOnClickDayListener(null); 123 | container.removeView((View) object); 124 | } 125 | 126 | @Override 127 | public int getCount() { 128 | return TOTAL_PAGES; 129 | } 130 | 131 | @Override 132 | public boolean isViewFromObject(@NonNull View view, @NonNull Object obj) { 133 | return view == obj; 134 | } 135 | 136 | @Override 137 | public void onPageScrollStateChanged(int state) { 138 | switch(state) { 139 | case ViewPager.SCROLL_STATE_IDLE: 140 | //HLog.d(TAG, CLASS, "SCROLL_STATE_IDLE"); 141 | break; 142 | case ViewPager.SCROLL_STATE_DRAGGING: 143 | //HLog.d(TAG, CLASS, "SCROLL_STATE_DRAGGING"); 144 | previousPosition = monthlyView != null ? monthlyView.getCurrentPosition() : 0; 145 | break; 146 | case ViewPager.SCROLL_STATE_SETTLING: 147 | //HLog.d(TAG, CLASS, "SCROLL_STATE_SETTLING"); 148 | break; 149 | } 150 | } 151 | 152 | @Override 153 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 154 | 155 | //HLog.d(TAG, CLASS, position + "- " + positionOffset); 156 | if(previousPosition != position) { 157 | previousPosition = position; 158 | 159 | YearMonth ym = getYearMonth(position); 160 | if (monthlyView != null) { 161 | monthlyView.onMonthChanged(ym.year, ym.month); 162 | } 163 | HLog.d(TAG, klass, position + " onPageScrolled- " + ym.year + "." + ym.month); 164 | } 165 | } 166 | 167 | @Override 168 | public void onPageSelected(int position) { 169 | } 170 | 171 | // implements OneMonthView.OnClickDayListener 172 | 173 | @Override 174 | public void onClick(OneDayView odv) { 175 | if (monthlyView != null) monthlyView.onClickDay(odv); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/MonthlyView.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto; 2 | 3 | import com.hansoolabs.calendarproto.cal.OneDayView; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | public interface MonthlyView { 8 | void onClickDay(@NonNull OneDayView odv); 9 | void onMonthChanged(int year, int month); 10 | int getCurrentPosition(); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/cal/OneDayData.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto.cal; 2 | 3 | import java.util.Calendar; 4 | 5 | import com.hansoolabs.calendarproto.cal.WeatherInfo.Weather; 6 | 7 | 8 | 9 | /** 10 | * Value object for a day 11 | * @author brownsoo 12 | * 13 | * @noinspection unused 14 | */ 15 | public class OneDayData { 16 | 17 | Calendar cal; 18 | Weather weather; 19 | private CharSequence msg = ""; 20 | 21 | /** 22 | * OneDayData Constructor 23 | */ 24 | public OneDayData() { 25 | this.cal = Calendar.getInstance(); 26 | this.weather = Weather.SUNSHINE; 27 | } 28 | 29 | /** 30 | * Set day info with given data 31 | * @param year 4 digits of year 32 | * @param month month Calendar.JANUARY ~ Calendar.DECEMBER 33 | * @param day day of month (1~#) 34 | */ 35 | public void setDay(int year, int month, int day) { 36 | cal = Calendar.getInstance(); 37 | cal.set(year, month, day); 38 | } 39 | 40 | /** 41 | * Set day info with cloning calendar 42 | * @param cal calendar to clone 43 | */ 44 | public void setDay(Calendar cal) { 45 | this.cal = (Calendar) cal.clone(); 46 | } 47 | 48 | /** 49 | * Get calendar 50 | * @return Calendar instance 51 | */ 52 | public Calendar getDay() { 53 | return cal; 54 | } 55 | 56 | /** 57 | * Same function with {@link Calendar#get(int)}
58 | *
59 | * 60 | * Returns the value of the given field after computing the field values by 61 | * calling {@code complete()} first. 62 | * 63 | * @throws IllegalArgumentException 64 | * if the fields are not set, the time is not set, and the 65 | * time cannot be computed from the current field values. 66 | * @throws ArrayIndexOutOfBoundsException 67 | * if the field is not inside the range of possible fields. 68 | * The range is starting at 0 up to {@code FIELD_COUNT}. 69 | */ 70 | public int get(int field) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { 71 | return cal.get(field); 72 | } 73 | 74 | /** 75 | * Set weather info 76 | * @param weather Weather instance 77 | */ 78 | public void setWeather(Weather weather) { 79 | this.weather = weather; 80 | } 81 | 82 | /** 83 | * Get weather info 84 | * @return Weather 85 | */ 86 | public Weather getWeather() { 87 | return this.weather; 88 | } 89 | 90 | /** 91 | * Get message 92 | * @return message 93 | */ 94 | public CharSequence getMessage() { 95 | return msg; 96 | } 97 | 98 | /** 99 | * Set message 100 | * @param msg message to display 101 | */ 102 | public void setMessage(CharSequence msg) { 103 | this.msg = msg; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/cal/OneDayView.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto.cal; 2 | 3 | import java.util.Calendar; 4 | 5 | import android.content.Context; 6 | import android.graphics.Color; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.widget.ImageView; 10 | import android.widget.RelativeLayout; 11 | import android.widget.TextView; 12 | 13 | import com.hansoolabs.calendarproto.R; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.annotation.Nullable; 17 | 18 | /** 19 | * View to display a day 20 | * @author Brownsoo 21 | * 22 | * @noinspection unused 23 | */ 24 | public class OneDayView extends RelativeLayout { 25 | 26 | /** number text field */ 27 | @NonNull 28 | private final TextView dayTv; 29 | /** message text field*/ 30 | @NonNull 31 | private final TextView msgTv; 32 | /** Weather icon */ 33 | @NonNull 34 | private final ImageView weatherIv; 35 | /** Value object for a day info */ 36 | @NonNull 37 | private OneDayData one; 38 | 39 | /** 40 | * OneDayView constructor 41 | * @param context context 42 | */ 43 | public OneDayView(@NonNull Context context) { 44 | this(context, null); 45 | } 46 | 47 | /** 48 | * OneDayView constructor for xml 49 | * @param context context 50 | * @param attrs AttributeSet 51 | */ 52 | public OneDayView(@NonNull Context context, @Nullable AttributeSet attrs) { 53 | super(context, attrs); 54 | View v = View.inflate(context, R.layout.oneday, this); 55 | dayTv = v.findViewById(R.id.onday_dayTv); 56 | weatherIv = v.findViewById(R.id.onday_weatherIv); 57 | msgTv = v.findViewById(R.id.onday_msgTv); 58 | one = new OneDayData(); 59 | 60 | } 61 | 62 | /** 63 | * Set the day to display 64 | * @param year 4 digits of year 65 | * @param month Calendar.JANUARY ~ Calendar.DECEMBER 66 | * @param day day of month 67 | */ 68 | public void setDay(int year, int month, int day) { 69 | this.one.cal.set(year, month, day); 70 | } 71 | 72 | /** 73 | * Set the day to display 74 | * @param cal Calendar instance 75 | */ 76 | public void setDay(Calendar cal) { 77 | this.one.setDay((Calendar) cal.clone()); 78 | } 79 | 80 | /** 81 | * Set the day to display 82 | * @param one OneDayData instance 83 | */ 84 | public void setDay(OneDayData one) { 85 | this.one = one; 86 | } 87 | 88 | /** 89 | * Get the day to display 90 | * @return OneDayData instance 91 | */ 92 | public OneDayData getDay() { 93 | return one; 94 | } 95 | 96 | /** 97 | * Set the message to display 98 | * @param msg message 99 | */ 100 | public void setMessage(String msg){ 101 | one.setMessage(msg); 102 | } 103 | 104 | /** 105 | * Get the message 106 | * @return message 107 | */ 108 | public CharSequence getMessage(){ 109 | return one.getMessage(); 110 | } 111 | 112 | /** 113 | * Same function with {@link Calendar#get(int)}
114 | *
115 | * Returns the value of the given field after computing the field values by 116 | * calling {@code complete()} first. 117 | * 118 | * @param field Calendar.YEAR or Calendar.MONTH or Calendar.DAY_OF_MONTH 119 | * 120 | * @throws IllegalArgumentException 121 | * if the fields are not set, the time is not set, and the 122 | * time cannot be computed from the current field values. 123 | * @throws ArrayIndexOutOfBoundsException 124 | * if the field is not inside the range of possible fields. 125 | * The range is starting at 0 up to {@code FIELD_COUNT}. 126 | */ 127 | public int get(int field) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { 128 | return one.get(field); 129 | } 130 | 131 | /** 132 | * Set weather 133 | * @param weather Weather instance 134 | */ 135 | public void setWeather(WeatherInfo.Weather weather) { 136 | this.one.setWeather(weather); 137 | } 138 | 139 | /** 140 | * Updates UI upon the value object. 141 | */ 142 | public void refresh() { 143 | 144 | //HLog.d(TAG, CLASS, "refresh"); 145 | 146 | dayTv.setText(String.valueOf(one.get(Calendar.DAY_OF_MONTH))); 147 | 148 | if(one.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) { 149 | dayTv.setTextColor(Color.RED); 150 | } 151 | else if(one.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) { 152 | dayTv.setTextColor(Color.BLUE); 153 | } 154 | else { 155 | dayTv.setTextColor(Color.BLACK); 156 | } 157 | 158 | msgTv.setText((one.getMessage()==null)?"":one.getMessage()); 159 | switch (one.weather) { 160 | case CLOUDY, SUN_CLOUDY -> weatherIv.setImageResource(R.drawable.cloudy); 161 | case RAINY -> weatherIv.setImageResource(R.drawable.rainy); 162 | case SNOW -> weatherIv.setImageResource(R.drawable.snowy); 163 | case SUNSHINE -> weatherIv.setImageResource(R.drawable.sunny); 164 | } 165 | 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/cal/OneMonthView.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto.cal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Calendar; 5 | 6 | import com.hansoolabs.calendarproto.HLog; 7 | import com.hansoolabs.calendarproto.MConfig; 8 | 9 | import android.content.Context; 10 | import android.util.AttributeSet; 11 | import android.view.View; 12 | import android.widget.LinearLayout; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | 17 | /** 18 | * View to display a month 19 | * @noinspection unused 20 | */ 21 | public class OneMonthView extends LinearLayout implements View.OnClickListener { 22 | 23 | private static final String TAG = MConfig.TAG; 24 | private final String klass = "OneMonthView@" + Integer.toHexString(hashCode()); 25 | 26 | public interface OnClickDayListener { 27 | void onClick(OneDayView odv); 28 | } 29 | 30 | private int mYear; 31 | private int mMonth; 32 | @NonNull 33 | final private ArrayList weeks = new ArrayList<>(6); //Max 6 weeks in a month 34 | @NonNull 35 | final private ArrayList dayViews = new ArrayList<>(42); // 7 days * 6 weeks = 42 days 36 | @Nullable 37 | private OnClickDayListener onClickDayListener = null; 38 | 39 | public void setOnClickDayListener(@Nullable OnClickDayListener listener) { 40 | this.onClickDayListener = listener; 41 | } 42 | 43 | public OneMonthView(Context context) { 44 | this(context, null); 45 | } 46 | 47 | public OneMonthView(Context context, AttributeSet attrs) { 48 | this(context, attrs, 0); 49 | } 50 | 51 | public OneMonthView(Context context, AttributeSet attrs, int defStyle) { 52 | super(context, attrs, defStyle); 53 | setOrientation(LinearLayout.VERTICAL); 54 | //Prepare many day-views enough to prevent recreation. 55 | LinearLayout ll = null; 56 | for(int i=0; i<42; i++) { 57 | 58 | if(i % 7 == 0) { 59 | //Create new week layout 60 | ll = new LinearLayout(context); 61 | LinearLayout.LayoutParams params 62 | = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0); 63 | params.weight = 1; 64 | ll.setOrientation(LinearLayout.HORIZONTAL); 65 | ll.setLayoutParams(params); 66 | ll.setWeightSum(7); 67 | 68 | weeks.add(ll); 69 | } 70 | 71 | LinearLayout.LayoutParams params 72 | = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT); 73 | params.weight = 1; 74 | 75 | OneDayView ov = new OneDayView(context); 76 | ov.setLayoutParams(params); 77 | ov.setOnClickListener(this); 78 | 79 | ll.addView(ov); 80 | dayViews.add(ov); 81 | } 82 | 83 | //for Preview of Graphic editor 84 | if(isInEditMode()) { 85 | Calendar cal = Calendar.getInstance(); 86 | make(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)); 87 | } 88 | 89 | HLog.i(TAG, klass, "new instance"); 90 | } 91 | 92 | /** 93 | * Get current year 94 | * @return 4 digits number of year 95 | */ 96 | public int getYear() { 97 | return mYear; 98 | } 99 | 100 | /** 101 | * Get current month 102 | * @return 0~11 (Calendar.JANUARY ~ Calendar.DECEMBER) 103 | */ 104 | public int getMonth() { 105 | return mMonth; 106 | } 107 | 108 | 109 | /** 110 | * Any layout manager that doesn't scroll will want this. 111 | */ 112 | @Override 113 | public boolean shouldDelayChildPressedState() { 114 | return false; 115 | } 116 | 117 | 118 | /** 119 | * Make a Month view 120 | * @param year year of this month view (4 digits number) 121 | * @param month month of this month view (0~11) 122 | */ 123 | public void make(int year, int month) 124 | { 125 | if(mYear == year && mMonth == month) { 126 | return; 127 | } 128 | 129 | long makeTime = System.currentTimeMillis(); 130 | 131 | this.mYear = year; 132 | this.mMonth = month; 133 | 134 | Calendar cal = Calendar.getInstance(); 135 | cal.set(year, month, 1); 136 | cal.setFirstDayOfWeek(Calendar.SUNDAY);//Sunday is first day of week in this sample 137 | 138 | int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);//Get day of the week in first day of this month 139 | int maxOfMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);//Get max day number of this month 140 | ArrayList oneDayDataList = new ArrayList<>(); 141 | 142 | cal.add(Calendar.DAY_OF_MONTH, Calendar.SUNDAY - dayOfWeek);//Move to first day of first week 143 | 144 | /* add previous month */ 145 | int seekDay; 146 | for(;;) { 147 | seekDay = cal.get(Calendar.DAY_OF_WEEK); 148 | if(dayOfWeek == seekDay) break; 149 | 150 | OneDayData one = new OneDayData(); 151 | one.setDay(cal); 152 | oneDayDataList.add(one); 153 | //하루 증가 154 | cal.add(Calendar.DAY_OF_MONTH, 1); 155 | } 156 | 157 | /* add this month */ 158 | for(int i=0; i < maxOfMonth; i++) { 159 | OneDayData one = new OneDayData(); 160 | one.setDay(cal); 161 | oneDayDataList.add(one); 162 | //add one day 163 | cal.add(Calendar.DAY_OF_MONTH, 1); 164 | } 165 | 166 | /* add next month */ 167 | while (cal.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) { 168 | OneDayData one = new OneDayData(); 169 | one.setDay(cal); 170 | oneDayDataList.add(one); 171 | //add one day 172 | cal.add(Calendar.DAY_OF_MONTH, 1); 173 | } 174 | 175 | if(oneDayDataList.size() == 0) return; 176 | 177 | //Remove all day-views 178 | this.removeAllViews(); 179 | 180 | int count = 0; 181 | for(OneDayData one : oneDayDataList) { 182 | if(count % 7 == 0) { 183 | addView(weeks.get(count / 7)); 184 | } 185 | OneDayView ov = dayViews.get(count); 186 | ov.setDay(one); 187 | ov.setMessage(""); 188 | ov.refresh(); 189 | count++; 190 | } 191 | 192 | //Set the weight-sum of LinearLayout to week counts 193 | this.setWeightSum(getChildCount()); 194 | 195 | 196 | HLog.d(TAG, klass, "<<<<< making timeMillis : " + (System.currentTimeMillis() - makeTime)); 197 | 198 | } 199 | 200 | 201 | @Override 202 | public void onClick(View v) { 203 | OneDayView odv = (OneDayView) v; 204 | HLog.d(TAG, klass, "click " + odv.get(Calendar.MONTH) + "/" + odv.get(Calendar.DAY_OF_MONTH)); 205 | if (onClickDayListener != null) { 206 | this.onClickDayListener.onClick(odv); 207 | } 208 | 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/cal/WeatherInfo.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto.cal; 2 | 3 | /** 4 | * Weather Info.
5 | * 6 | * Created by brownsoo 7 | */ 8 | public class WeatherInfo { 9 | 10 | public enum Weather { 11 | SUNSHINE, 12 | SUN_CLOUDY, 13 | CLOUDY, 14 | RAINY, 15 | SNOW 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/hansoolabs/calendarproto/cal/YearMonth.java: -------------------------------------------------------------------------------- 1 | package com.hansoolabs.calendarproto.cal; 2 | 3 | /** 4 | * Object to preserve year and month 5 | * @author Brownsoo 6 | * 7 | */ 8 | public class YearMonth { 9 | public int year; 10 | public int month; 11 | 12 | public YearMonth(int year, int month) { 13 | this.year = year; 14 | this.month = month; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/fr/castorflex/android/verticalviewpager/VerticalViewPager.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.content.res.TypedArray; 7 | import android.database.DataSetObserver; 8 | import android.graphics.Canvas; 9 | import android.graphics.Rect; 10 | import android.graphics.drawable.Drawable; 11 | import android.os.Bundle; 12 | import android.os.Parcel; 13 | import android.os.Parcelable; 14 | import android.os.SystemClock; 15 | import androidx.annotation.DrawableRes; 16 | import androidx.annotation.NonNull; 17 | import androidx.core.content.res.ResourcesCompat; 18 | import androidx.core.view.AccessibilityDelegateCompat; 19 | import androidx.viewpager.widget.PagerAdapter; 20 | import androidx.core.view.ViewCompat; 21 | import androidx.viewpager.widget.ViewPager; 22 | import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 23 | import android.util.AttributeSet; 24 | import android.util.Log; 25 | import android.view.FocusFinder; 26 | import android.view.Gravity; 27 | import android.view.KeyEvent; 28 | import android.view.MotionEvent; 29 | import android.view.SoundEffectConstants; 30 | import android.view.VelocityTracker; 31 | import android.view.View; 32 | import android.view.ViewConfiguration; 33 | import android.view.ViewGroup; 34 | import android.view.ViewParent; 35 | import android.view.accessibility.AccessibilityEvent; 36 | import android.view.accessibility.AccessibilityRecord; 37 | import android.view.animation.Interpolator; 38 | import android.widget.EdgeEffect; 39 | import android.widget.Scroller; 40 | 41 | import java.lang.reflect.Method; 42 | import java.util.ArrayList; 43 | import java.util.Comparator; 44 | 45 | /** 46 | * Created by castorflex on 12/29/13. 47 | * Updated by brownsoo on 11/01/23. 48 | * Just a copy of the original ViewPager modified to support vertical Scrolling 49 | */ 50 | 51 | @SuppressWarnings("unused") 52 | public class VerticalViewPager extends ViewGroup { 53 | 54 | private static final String TAG = "ViewPager"; 55 | private static final boolean DEBUG = false; 56 | 57 | private static final boolean USE_CACHE = false; 58 | 59 | private static final int DEFAULT_OFFSCREEN_PAGES = 1; 60 | private static final int MAX_SETTLE_DURATION = 600; // ms 61 | private static final int MIN_DISTANCE_FOR_FLING = 25; // dips 62 | 63 | private static final int DEFAULT_GUTTER_SIZE = 16; // dips 64 | 65 | private static final int MIN_FLING_VELOCITY = 400; // dips 66 | 67 | private static final int[] LAYOUT_ATTRS = new int[]{ 68 | android.R.attr.layout_gravity 69 | }; 70 | 71 | /** 72 | * Used to track what the expected number of items in the adapter should be. 73 | * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. 74 | */ 75 | private int mExpectedAdapterCount; 76 | 77 | static class ItemInfo { 78 | Object object; 79 | int position; 80 | boolean scrolling; 81 | float heightFactor; 82 | float offset; 83 | } 84 | 85 | private static final Comparator COMPARATOR = Comparator.comparingInt(lhs -> lhs.position); 86 | 87 | private static final Interpolator sInterpolator = t -> { 88 | t -= 1.0f; 89 | return t * t * t * t * t + 1.0f; 90 | }; 91 | 92 | private final ArrayList mItems = new ArrayList<>(); 93 | private final ItemInfo mTempItem = new ItemInfo(); 94 | 95 | private final Rect mTempRect = new Rect(); 96 | 97 | private PagerAdapter mAdapter; 98 | private int mCurItem; // Index of currently displayed page. 99 | private int mRestoredCurItem = -1; 100 | private Parcelable mRestoredAdapterState = null; 101 | private ClassLoader mRestoredClassLoader = null; 102 | private Scroller mScroller; 103 | private PagerObserver mObserver; 104 | 105 | private int mPageMargin; 106 | private Drawable mMarginDrawable; 107 | private int mLeftPageBounds; 108 | private int mRightPageBounds; 109 | 110 | // Offsets of the first and last items, if known. 111 | // Set during population, used to determine if we are at the beginning 112 | // or end of the pager data set during touch scrolling. 113 | private float mFirstOffset = -Float.MAX_VALUE; 114 | private float mLastOffset = Float.MAX_VALUE; 115 | 116 | private boolean mInLayout; 117 | 118 | private boolean mScrollingCacheEnabled; 119 | 120 | private boolean mPopulatePending; 121 | private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; 122 | 123 | private boolean mIsBeingDragged; 124 | private boolean mIsUnableToDrag; 125 | // private boolean mIgnoreGutter; 126 | private int mDefaultGutterSize; 127 | private int mGutterSize; 128 | private int mTouchSlop; 129 | /** 130 | * Position of the last motion event. 131 | */ 132 | private float mLastMotionX; 133 | private float mLastMotionY; 134 | private float mInitialMotionX; 135 | private float mInitialMotionY; 136 | /** 137 | * ID of the active pointer. This is used to retain consistency during 138 | * drags/flings if multiple pointers are used. 139 | */ 140 | private int mActivePointerId = INVALID_POINTER; 141 | /** 142 | * Sentinel value for no current active pointer. 143 | * Used by {@link #mActivePointerId}. 144 | */ 145 | private static final int INVALID_POINTER = -1; 146 | 147 | /** 148 | * Determines speed during touch scrolling 149 | */ 150 | private VelocityTracker mVelocityTracker; 151 | private int mMinimumVelocity; 152 | private int mMaximumVelocity; 153 | private int mFlingDistance; 154 | private int mCloseEnough; 155 | 156 | // If the pager is at least this close to its final position, complete the scroll 157 | // on touch down and let the user interact with the content inside instead of 158 | // "catching" the flinging pager. 159 | private static final int CLOSE_ENOUGH = 2; // dp 160 | 161 | private boolean mFakeDragging; 162 | private long mFakeDragBeginTime; 163 | 164 | private EdgeEffect mTopEdge; 165 | private EdgeEffect mBottomEdge; 166 | 167 | private boolean mFirstLayout = true; 168 | private boolean mCalledSuper; 169 | private int mDecorChildCount; 170 | 171 | private ViewPager.OnPageChangeListener mOnPageChangeListener; 172 | private ViewPager.OnPageChangeListener mInternalPageChangeListener; 173 | private OnAdapterChangeListener mAdapterChangeListener; 174 | private ViewPager.PageTransformer mPageTransformer; 175 | private Method mSetChildrenDrawingOrderEnabled; 176 | 177 | private static final int DRAW_ORDER_DEFAULT = 0; 178 | private static final int DRAW_ORDER_FORWARD = 1; 179 | private static final int DRAW_ORDER_REVERSE = 2; 180 | private int mDrawingOrder; 181 | @NonNull 182 | final private ArrayList mDrawingOrderedChildren = new ArrayList<>(); 183 | private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); 184 | 185 | /** 186 | * Indicates that the pager is in an idle, settled state. The current page 187 | * is fully in view and no animation is in progress. 188 | */ 189 | public static final int SCROLL_STATE_IDLE = 0; 190 | 191 | /** 192 | * Indicates that the pager is currently being dragged by the user. 193 | */ 194 | public static final int SCROLL_STATE_DRAGGING = 1; 195 | 196 | /** 197 | * Indicates that the pager is in the process of settling to a final position. 198 | */ 199 | public static final int SCROLL_STATE_SETTLING = 2; 200 | 201 | private final Runnable mEndScrollRunnable = () -> { 202 | setScrollState(SCROLL_STATE_IDLE); 203 | populate(); 204 | }; 205 | 206 | private int mScrollState = SCROLL_STATE_IDLE; 207 | 208 | /** 209 | * Used internally to monitor when adapters are switched. 210 | */ 211 | interface OnAdapterChangeListener { 212 | void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); 213 | } 214 | 215 | /** 216 | * Used internally to tag special types of child views that should be added as 217 | * pager decorations by default. 218 | */ 219 | interface Decor { 220 | } 221 | 222 | public VerticalViewPager(Context context) { 223 | super(context); 224 | initViewPager(); 225 | } 226 | 227 | public VerticalViewPager(Context context, AttributeSet attrs) { 228 | super(context, attrs); 229 | initViewPager(); 230 | } 231 | 232 | void initViewPager() { 233 | setWillNotDraw(false); 234 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 235 | setFocusable(true); 236 | final Context context = getContext(); 237 | mScroller = new Scroller(context, sInterpolator); 238 | final ViewConfiguration configuration = ViewConfiguration.get(context); 239 | final float density = context.getResources().getDisplayMetrics().density; 240 | 241 | mTouchSlop = configuration.getScaledPagingTouchSlop(); 242 | mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); 243 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 244 | mTopEdge = new EdgeEffect(context); 245 | mBottomEdge = new EdgeEffect(context); 246 | 247 | mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); 248 | mCloseEnough = (int) (CLOSE_ENOUGH * density); 249 | mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); 250 | 251 | ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); 252 | 253 | if (ViewCompat.getImportantForAccessibility(this) 254 | == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 255 | ViewCompat.setImportantForAccessibility(this, 256 | ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 257 | } 258 | } 259 | 260 | @Override 261 | protected void onDetachedFromWindow() { 262 | removeCallbacks(mEndScrollRunnable); 263 | super.onDetachedFromWindow(); 264 | } 265 | 266 | private void setScrollState(int newState) { 267 | if (mScrollState == newState) { 268 | return; 269 | } 270 | 271 | mScrollState = newState; 272 | if (mPageTransformer != null) { 273 | // PageTransformers can do complex things that benefit from hardware layers. 274 | enableLayers(newState != SCROLL_STATE_IDLE); 275 | } 276 | if (mOnPageChangeListener != null) { 277 | mOnPageChangeListener.onPageScrollStateChanged(newState); 278 | } 279 | } 280 | 281 | /** 282 | * Set a PagerAdapter that will supply views for this pager as needed. 283 | * 284 | * @param adapter Adapter to use 285 | */ 286 | public void setAdapter(PagerAdapter adapter) { 287 | if (mAdapter != null) { 288 | mAdapter.unregisterDataSetObserver(mObserver); 289 | mAdapter.startUpdate(this); 290 | for (int i = 0; i < mItems.size(); i++) { 291 | final ItemInfo ii = mItems.get(i); 292 | mAdapter.destroyItem(this, ii.position, ii.object); 293 | } 294 | mAdapter.finishUpdate(this); 295 | mItems.clear(); 296 | removeNonDecorViews(); 297 | mCurItem = 0; 298 | scrollTo(0, 0); 299 | } 300 | 301 | final PagerAdapter oldAdapter = mAdapter; 302 | mAdapter = adapter; 303 | mExpectedAdapterCount = 0; 304 | 305 | if (mAdapter != null) { 306 | if (mObserver == null) { 307 | mObserver = new PagerObserver(); 308 | } 309 | mAdapter.registerDataSetObserver(mObserver); 310 | mPopulatePending = false; 311 | final boolean wasFirstLayout = mFirstLayout; 312 | mFirstLayout = true; 313 | mExpectedAdapterCount = mAdapter.getCount(); 314 | if (mRestoredCurItem >= 0) { 315 | mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 316 | setCurrentItemInternal(mRestoredCurItem, false, true); 317 | mRestoredCurItem = -1; 318 | mRestoredAdapterState = null; 319 | mRestoredClassLoader = null; 320 | } else if (!wasFirstLayout) { 321 | populate(); 322 | } else { 323 | requestLayout(); 324 | } 325 | } 326 | 327 | if (mAdapterChangeListener != null && oldAdapter != adapter) { 328 | mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 329 | } 330 | } 331 | 332 | private void removeNonDecorViews() { 333 | for (int i = 0; i < getChildCount(); i++) { 334 | final View child = getChildAt(i); 335 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 336 | if (!lp.isDecor) { 337 | removeViewAt(i); 338 | i--; 339 | } 340 | } 341 | } 342 | 343 | /** 344 | * Retrieve the current adapter supplying pages. 345 | * 346 | * @return The currently registered PagerAdapter 347 | */ 348 | public PagerAdapter getAdapter() { 349 | return mAdapter; 350 | } 351 | 352 | void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 353 | mAdapterChangeListener = listener; 354 | } 355 | 356 | private int getClientWidth() { 357 | return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 358 | } 359 | 360 | private int getClientHeight() { 361 | return getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 362 | } 363 | 364 | 365 | /** 366 | * Set the currently selected page. If the ViewPager has already been through its first 367 | * layout with its current adapter there will be a smooth animated transition between 368 | * the current item and the specified item. 369 | * 370 | * @param item Item index to select 371 | */ 372 | public void setCurrentItem(int item) { 373 | mPopulatePending = false; 374 | setCurrentItemInternal(item, !mFirstLayout, false); 375 | } 376 | 377 | /** 378 | * Set the currently selected page. 379 | * 380 | * @param item Item index to select 381 | * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 382 | */ 383 | @SuppressWarnings("SameParameterValue") 384 | public void setCurrentItem(int item, boolean smoothScroll) { 385 | mPopulatePending = false; 386 | setCurrentItemInternal(item, smoothScroll, false); 387 | } 388 | 389 | public int getCurrentItem() { 390 | return mCurItem; 391 | } 392 | 393 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 394 | setCurrentItemInternal(item, smoothScroll, always, 0); 395 | } 396 | 397 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 398 | if (mAdapter == null || mAdapter.getCount() <= 0) { 399 | setScrollingCacheEnabled(false); 400 | return; 401 | } 402 | if (!always && mCurItem == item && mItems.size() != 0) { 403 | setScrollingCacheEnabled(false); 404 | return; 405 | } 406 | 407 | if (item < 0) { 408 | item = 0; 409 | } else if (item >= mAdapter.getCount()) { 410 | item = mAdapter.getCount() - 1; 411 | } 412 | final int pageLimit = mOffscreenPageLimit; 413 | if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 414 | // We are doing a jump by more than one page. To avoid 415 | // glitches, we want to keep all current pages in the view 416 | // until the scroll ends. 417 | for (int i = 0; i < mItems.size(); i++) { 418 | mItems.get(i).scrolling = true; 419 | } 420 | } 421 | final boolean dispatchSelected = mCurItem != item; 422 | 423 | if (mFirstLayout) { 424 | // We don't have any idea how big we are yet and shouldn't have any pages either. 425 | // Just set things up and let the pending layout handle things. 426 | mCurItem = item; 427 | if (dispatchSelected && mOnPageChangeListener != null) { 428 | mOnPageChangeListener.onPageSelected(item); 429 | } 430 | if (dispatchSelected && mInternalPageChangeListener != null) { 431 | mInternalPageChangeListener.onPageSelected(item); 432 | } 433 | requestLayout(); 434 | } else { 435 | populate(item); 436 | scrollToItem(item, smoothScroll, velocity, dispatchSelected); 437 | } 438 | } 439 | 440 | private void scrollToItem(int item, boolean smoothScroll, int velocity, 441 | boolean dispatchSelected) { 442 | final ItemInfo curInfo = infoForPosition(item); 443 | int destY = 0; 444 | if (curInfo != null) { 445 | final int height = getClientHeight(); 446 | destY = (int) (height * Math.max(mFirstOffset, 447 | Math.min(curInfo.offset, mLastOffset))); 448 | } 449 | if (smoothScroll) { 450 | smoothScrollTo(0, destY, velocity); 451 | if (dispatchSelected && mOnPageChangeListener != null) { 452 | mOnPageChangeListener.onPageSelected(item); 453 | } 454 | if (dispatchSelected && mInternalPageChangeListener != null) { 455 | mInternalPageChangeListener.onPageSelected(item); 456 | } 457 | } else { 458 | if (dispatchSelected && mOnPageChangeListener != null) { 459 | mOnPageChangeListener.onPageSelected(item); 460 | } 461 | if (dispatchSelected && mInternalPageChangeListener != null) { 462 | mInternalPageChangeListener.onPageSelected(item); 463 | } 464 | completeScroll(false); 465 | scrollTo(0, destY); 466 | pageScrolled(destY); 467 | } 468 | } 469 | 470 | /** 471 | * Set a listener that will be invoked whenever the page changes or is incrementally 472 | * scrolled. See {@link ViewPager.OnPageChangeListener}. 473 | * 474 | * @param listener Listener to set 475 | */ 476 | public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { 477 | mOnPageChangeListener = listener; 478 | } 479 | 480 | /** 481 | * Set a {@link ViewPager.PageTransformer} that will be called for each attached page whenever 482 | * the scroll position is changed. This allows the application to apply custom property 483 | * transformations to each page, overriding the default sliding look and feel. 484 | *

485 | *

Note: Prior to Android 3.0 the property animation APIs did not exist. 486 | * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

487 | * 488 | * @param reverseDrawingOrder true if the supplied PageTransformer requires page views 489 | * to be drawn from last to first instead of first to last. 490 | * @param transformer PageTransformer that will modify each page's animation properties 491 | */ 492 | public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) { 493 | final boolean hasTransformer = transformer != null; 494 | final boolean hasPageTransformer = mPageTransformer != null; 495 | final boolean needsPopulate = hasTransformer != hasPageTransformer; 496 | mPageTransformer = transformer; 497 | setChildrenDrawingOrderEnabledCompat(hasTransformer); 498 | if (hasTransformer) { 499 | mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; 500 | } else { 501 | mDrawingOrder = DRAW_ORDER_DEFAULT; 502 | } 503 | if (needsPopulate) populate(); 504 | } 505 | 506 | void setChildrenDrawingOrderEnabledCompat(boolean enable) { 507 | if (mSetChildrenDrawingOrderEnabled == null) { 508 | try { 509 | mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( 510 | "setChildrenDrawingOrderEnabled", Boolean.TYPE); 511 | } catch (NoSuchMethodException e) { 512 | Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); 513 | } 514 | } 515 | try { 516 | mSetChildrenDrawingOrderEnabled.invoke(this, enable); 517 | } catch (Exception e) { 518 | Log.e(TAG, "Error changing children drawing order", e); 519 | } 520 | } 521 | 522 | @Override 523 | protected int getChildDrawingOrder(int childCount, int i) { 524 | final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; 525 | return ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; 526 | } 527 | 528 | /** 529 | * Set a separate OnPageChangeListener for internal use by the support library. 530 | * 531 | * @param listener Listener to set 532 | * @return The old listener that was set, if any. 533 | */ 534 | ViewPager.OnPageChangeListener setInternalPageChangeListener(ViewPager.OnPageChangeListener listener) { 535 | ViewPager.OnPageChangeListener oldListener = mInternalPageChangeListener; 536 | mInternalPageChangeListener = listener; 537 | return oldListener; 538 | } 539 | 540 | /** 541 | * Returns the number of pages that will be retained to either side of the 542 | * current page in the view hierarchy in an idle state. Defaults to 1. 543 | * 544 | * @return How many pages will be kept offscreen on either side 545 | * @see #setOffscreenPageLimit(int) 546 | */ 547 | public int getOffscreenPageLimit() { 548 | return mOffscreenPageLimit; 549 | } 550 | 551 | /** 552 | * Set the number of pages that should be retained to either side of the 553 | * current page in the view hierarchy in an idle state. Pages beyond this 554 | * limit will be recreated from the adapter when needed. 555 | *

556 | *

This is offered as an optimization. If you know in advance the number 557 | * of pages you will need to support or have lazy-loading mechanisms in place 558 | * on your pages, tweaking this setting can have benefits in perceived smoothness 559 | * of paging animations and interaction. If you have a small number of pages (3-4) 560 | * that you can keep active all at once, less time will be spent in layout for 561 | * newly created view subtrees as the user pages back and forth.

562 | *

563 | *

You should keep this limit low, especially if your pages have complex layouts. 564 | * This setting defaults to 1.

565 | * 566 | * @param limit How many pages will be kept offscreen in an idle state. 567 | */ 568 | public void setOffscreenPageLimit(int limit) { 569 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 570 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 571 | DEFAULT_OFFSCREEN_PAGES); 572 | limit = DEFAULT_OFFSCREEN_PAGES; 573 | } 574 | if (limit != mOffscreenPageLimit) { 575 | mOffscreenPageLimit = limit; 576 | populate(); 577 | } 578 | } 579 | 580 | /** 581 | * Set the margin between pages. 582 | * 583 | * @param marginPixels Distance between adjacent pages in pixels 584 | * @see #getPageMargin() 585 | * @see #setPageMarginDrawable(Drawable) 586 | * @see #setPageMarginDrawable(int) 587 | */ 588 | public void setPageMargin(int marginPixels) { 589 | final int oldMargin = mPageMargin; 590 | mPageMargin = marginPixels; 591 | 592 | final int height = getHeight(); 593 | recomputeScrollPosition(height, height, marginPixels, oldMargin); 594 | 595 | requestLayout(); 596 | } 597 | 598 | /** 599 | * Return the margin between pages. 600 | * 601 | * @return The size of the margin in pixels 602 | */ 603 | public int getPageMargin() { 604 | return mPageMargin; 605 | } 606 | 607 | /** 608 | * Set a drawable that will be used to fill the margin between pages. 609 | * 610 | * @param d Drawable to display between pages 611 | */ 612 | public void setPageMarginDrawable(Drawable d) { 613 | mMarginDrawable = d; 614 | if (d != null) refreshDrawableState(); 615 | setWillNotDraw(d == null); 616 | invalidate(); 617 | } 618 | 619 | /** 620 | * Set a drawable that will be used to fill the margin between pages. 621 | * 622 | * @param resId Resource ID of a drawable to display between pages 623 | */ 624 | public void setPageMarginDrawable(@DrawableRes int resId) { 625 | Context context = getContext(); 626 | Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), resId, context.getTheme()); 627 | if (drawable != null) { 628 | setPageMarginDrawable(drawable); 629 | } 630 | } 631 | 632 | @Override 633 | protected boolean verifyDrawable(@NonNull Drawable who) { 634 | return super.verifyDrawable(who) || who == mMarginDrawable; 635 | } 636 | 637 | @Override 638 | protected void drawableStateChanged() { 639 | super.drawableStateChanged(); 640 | final Drawable d = mMarginDrawable; 641 | if (d != null && d.isStateful()) { 642 | d.setState(getDrawableState()); 643 | } 644 | } 645 | 646 | // We want the duration of the page snap animation to be influenced by the distance that 647 | // the screen has to travel, however, we don't want this duration to be effected in a 648 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 649 | // of travel has on the overall snap duration. 650 | float distanceInfluenceForSnapDuration(float f) { 651 | f -= 0.5f; // center the values about 0. 652 | f *= 0.3f * Math.PI / 2.0f; 653 | return (float) Math.sin(f); 654 | } 655 | 656 | /** 657 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 658 | * 659 | * @param x the number of pixels to scroll by on the X axis 660 | * @param y the number of pixels to scroll by on the Y axis 661 | */ 662 | void smoothScrollTo(int x, int y) { 663 | smoothScrollTo(x, y, 0); 664 | } 665 | 666 | /** 667 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 668 | * 669 | * @param x the number of pixels to scroll by on the X axis 670 | * @param y the number of pixels to scroll by on the Y axis 671 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 672 | */ 673 | void smoothScrollTo(int x, int y, int velocity) { 674 | if (getChildCount() == 0) { 675 | // Nothing to do. 676 | setScrollingCacheEnabled(false); 677 | return; 678 | } 679 | int sx = getScrollX(); 680 | int sy = getScrollY(); 681 | int dx = x - sx; 682 | int dy = y - sy; 683 | if (dx == 0 && dy == 0) { 684 | completeScroll(false); 685 | populate(); 686 | setScrollState(SCROLL_STATE_IDLE); 687 | return; 688 | } 689 | 690 | setScrollingCacheEnabled(true); 691 | setScrollState(SCROLL_STATE_SETTLING); 692 | 693 | final int height = getClientHeight(); 694 | final int halfHeight = height / 2; 695 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height); 696 | final float distance = halfHeight + halfHeight * 697 | distanceInfluenceForSnapDuration(distanceRatio); 698 | 699 | int duration; 700 | velocity = Math.abs(velocity); 701 | if (velocity > 0) { 702 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 703 | } else { 704 | final float pageHeight = height * mAdapter.getPageWidth(mCurItem); 705 | final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin); 706 | duration = (int) ((pageDelta + 1) * 100); 707 | } 708 | duration = Math.min(duration, MAX_SETTLE_DURATION); 709 | 710 | mScroller.startScroll(sx, sy, dx, dy, duration); 711 | ViewCompat.postInvalidateOnAnimation(this); 712 | } 713 | 714 | ItemInfo addNewItem(int position, int index) { 715 | ItemInfo ii = new ItemInfo(); 716 | ii.position = position; 717 | ii.object = mAdapter.instantiateItem(this, position); 718 | ii.heightFactor = mAdapter.getPageWidth(position); 719 | if (index < 0 || index >= mItems.size()) { 720 | mItems.add(ii); 721 | } else { 722 | mItems.add(index, ii); 723 | } 724 | return ii; 725 | } 726 | 727 | void dataSetChanged() { 728 | // This method only gets called if our observer is attached, so mAdapter is non-null. 729 | 730 | final int adapterCount = mAdapter.getCount(); 731 | mExpectedAdapterCount = adapterCount; 732 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 733 | mItems.size() < adapterCount; 734 | int newCurrItem = mCurItem; 735 | 736 | boolean isUpdating = false; 737 | for (int i = 0; i < mItems.size(); i++) { 738 | final ItemInfo ii = mItems.get(i); 739 | final int newPos = mAdapter.getItemPosition(ii.object); 740 | 741 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 742 | continue; 743 | } 744 | 745 | if (newPos == PagerAdapter.POSITION_NONE) { 746 | mItems.remove(i); 747 | i--; 748 | 749 | if (!isUpdating) { 750 | mAdapter.startUpdate(this); 751 | isUpdating = true; 752 | } 753 | 754 | mAdapter.destroyItem(this, ii.position, ii.object); 755 | needPopulate = true; 756 | 757 | if (mCurItem == ii.position) { 758 | // Keep the current item in the valid range 759 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 760 | } 761 | continue; 762 | } 763 | 764 | if (ii.position != newPos) { 765 | if (ii.position == mCurItem) { 766 | // Our current item changed position. Follow it. 767 | newCurrItem = newPos; 768 | } 769 | 770 | ii.position = newPos; 771 | needPopulate = true; 772 | } 773 | } 774 | 775 | if (isUpdating) { 776 | mAdapter.finishUpdate(this); 777 | } 778 | 779 | mItems.sort(COMPARATOR); 780 | 781 | if (needPopulate) { 782 | // Reset our known page widths; populate will recompute them. 783 | final int childCount = getChildCount(); 784 | for (int i = 0; i < childCount; i++) { 785 | final View child = getChildAt(i); 786 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 787 | if (!lp.isDecor) { 788 | lp.heightFactor = 0.f; 789 | } 790 | } 791 | 792 | setCurrentItemInternal(newCurrItem, false, true); 793 | requestLayout(); 794 | } 795 | } 796 | 797 | void populate() { 798 | populate(mCurItem); 799 | } 800 | 801 | void populate(int newCurrentItem) { 802 | ItemInfo oldCurInfo = null; 803 | int focusDirection = View.FOCUS_FORWARD; 804 | if (mCurItem != newCurrentItem) { 805 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP; 806 | oldCurInfo = infoForPosition(mCurItem); 807 | mCurItem = newCurrentItem; 808 | } 809 | 810 | if (mAdapter == null) { 811 | sortChildDrawingOrder(); 812 | return; 813 | } 814 | 815 | // Bail now if we are waiting to populate. This is to hold off 816 | // on creating views from the time the user releases their finger to 817 | // fling to a new position until we have finished the scroll to 818 | // that position, avoiding glitches from happening at that point. 819 | if (mPopulatePending) { 820 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 821 | sortChildDrawingOrder(); 822 | return; 823 | } 824 | 825 | // Also, don't populate until we are attached to a window. This is to 826 | // avoid trying to populate before we have restored our view hierarchy 827 | // state and conflicting with what is restored. 828 | if (getWindowToken() == null) { 829 | return; 830 | } 831 | 832 | mAdapter.startUpdate(this); 833 | 834 | final int pageLimit = mOffscreenPageLimit; 835 | final int startPos = Math.max(0, mCurItem - pageLimit); 836 | final int N = mAdapter.getCount(); 837 | final int endPos = Math.min(N - 1, mCurItem + pageLimit); 838 | 839 | if (N != mExpectedAdapterCount) { 840 | String resName; 841 | try { 842 | resName = getResources().getResourceName(getId()); 843 | } catch (Resources.NotFoundException e) { 844 | resName = Integer.toHexString(getId()); 845 | } 846 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 847 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 848 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 849 | " Pager id: " + resName + 850 | " Pager class: " + getClass() + 851 | " Problematic adapter: " + mAdapter.getClass()); 852 | } 853 | 854 | // Locate the currently focused item or add it if needed. 855 | int curIndex; 856 | ItemInfo curItem = null; 857 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 858 | final ItemInfo ii = mItems.get(curIndex); 859 | if (ii.position >= mCurItem) { 860 | if (ii.position == mCurItem) curItem = ii; 861 | break; 862 | } 863 | } 864 | 865 | if (curItem == null && N > 0) { 866 | curItem = addNewItem(mCurItem, curIndex); 867 | } 868 | 869 | // Fill 3x the available width or up to the number of offscreen 870 | // pages requested to either side, whichever is larger. 871 | // If we have no current item we have no work to do. 872 | if (curItem != null) { 873 | float extraHeightTop = 0.f; 874 | int itemIndex = curIndex - 1; 875 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 876 | final int clientHeight = getClientHeight(); 877 | final float topHeightNeeded = clientHeight <= 0 ? 0 : 878 | 2.f - curItem.heightFactor + (float) getPaddingLeft() / (float) clientHeight; 879 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 880 | if (extraHeightTop >= topHeightNeeded && pos < startPos) { 881 | if (ii == null) { 882 | break; 883 | } 884 | if (pos == ii.position && !ii.scrolling) { 885 | mItems.remove(itemIndex); 886 | mAdapter.destroyItem(this, pos, ii.object); 887 | if (DEBUG) { 888 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 889 | " view: " + ((View) ii.object)); 890 | } 891 | itemIndex--; 892 | curIndex--; 893 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 894 | } 895 | } else if (ii != null && pos == ii.position) { 896 | extraHeightTop += ii.heightFactor; 897 | itemIndex--; 898 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 899 | } else { 900 | ii = addNewItem(pos, itemIndex + 1); 901 | extraHeightTop += ii.heightFactor; 902 | curIndex++; 903 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 904 | } 905 | } 906 | 907 | float extraHeightBottom = curItem.heightFactor; 908 | itemIndex = curIndex + 1; 909 | if (extraHeightBottom < 2.f) { 910 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 911 | final float bottomHeightNeeded = clientHeight <= 0 ? 0 : 912 | (float) getPaddingRight() / (float) clientHeight + 2.f; 913 | for (int pos = mCurItem + 1; pos < N; pos++) { 914 | if (extraHeightBottom >= bottomHeightNeeded && pos > endPos) { 915 | if (ii == null) { 916 | break; 917 | } 918 | if (pos == ii.position && !ii.scrolling) { 919 | mItems.remove(itemIndex); 920 | mAdapter.destroyItem(this, pos, ii.object); 921 | if (DEBUG) { 922 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 923 | " view: " + ((View) ii.object)); 924 | } 925 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 926 | } 927 | } else if (ii != null && pos == ii.position) { 928 | extraHeightBottom += ii.heightFactor; 929 | itemIndex++; 930 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 931 | } else { 932 | ii = addNewItem(pos, itemIndex); 933 | itemIndex++; 934 | extraHeightBottom += ii.heightFactor; 935 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 936 | } 937 | } 938 | } 939 | 940 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 941 | } 942 | 943 | if (DEBUG) { 944 | Log.i(TAG, "Current page list:"); 945 | for (int i = 0; i < mItems.size(); i++) { 946 | Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); 947 | } 948 | } 949 | 950 | if (curItem != null) { 951 | Object obj = curItem.object; 952 | if (obj != null) { 953 | mAdapter.setPrimaryItem(this, mCurItem, obj); 954 | } 955 | } 956 | 957 | mAdapter.finishUpdate(this); 958 | 959 | // Check width measurement of current pages and drawing sort order. 960 | // Update LayoutParams as needed. 961 | final int childCount = getChildCount(); 962 | for (int i = 0; i < childCount; i++) { 963 | final View child = getChildAt(i); 964 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 965 | lp.childIndex = i; 966 | if (!lp.isDecor && lp.heightFactor == 0.f) { 967 | // 0 means requery the adapter for this, it doesn't have a valid width. 968 | final ItemInfo ii = infoForChild(child); 969 | if (ii != null) { 970 | lp.heightFactor = ii.heightFactor; 971 | lp.position = ii.position; 972 | } 973 | } 974 | } 975 | sortChildDrawingOrder(); 976 | 977 | if (hasFocus()) { 978 | View currentFocused = findFocus(); 979 | ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; 980 | if (ii == null || ii.position != mCurItem) { 981 | for (int i = 0; i < getChildCount(); i++) { 982 | View child = getChildAt(i); 983 | ii = infoForChild(child); 984 | if (ii != null && ii.position == mCurItem) { 985 | if (child.requestFocus(focusDirection)) { 986 | break; 987 | } 988 | } 989 | } 990 | } 991 | } 992 | } 993 | 994 | private void sortChildDrawingOrder() { 995 | if (mDrawingOrder != DRAW_ORDER_DEFAULT) { 996 | mDrawingOrderedChildren.clear(); 997 | final int childCount = getChildCount(); 998 | for (int i = 0; i < childCount; i++) { 999 | final View child = getChildAt(i); 1000 | mDrawingOrderedChildren.add(child); 1001 | } 1002 | mDrawingOrderedChildren.sort(sPositionComparator); 1003 | } 1004 | } 1005 | 1006 | private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { 1007 | final int N = mAdapter.getCount(); 1008 | final int height = getClientHeight(); 1009 | final float marginOffset = height > 0 ? (float) mPageMargin / height : 0; 1010 | // Fix up offsets for later layout. 1011 | if (oldCurInfo != null) { 1012 | final int oldCurPosition = oldCurInfo.position; 1013 | // Base offsets off of oldCurInfo. 1014 | if (oldCurPosition < curItem.position) { 1015 | int itemIndex = 0; 1016 | ItemInfo ii; 1017 | float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset; 1018 | for (int pos = oldCurPosition + 1; 1019 | pos <= curItem.position && itemIndex < mItems.size(); pos++) { 1020 | ii = mItems.get(itemIndex); 1021 | while (pos > ii.position && itemIndex < mItems.size() - 1) { 1022 | itemIndex++; 1023 | ii = mItems.get(itemIndex); 1024 | } 1025 | while (pos < ii.position) { 1026 | // We don't have an item populated for this, 1027 | // ask the adapter for an offset. 1028 | offset += mAdapter.getPageWidth(pos) + marginOffset; 1029 | pos++; 1030 | } 1031 | ii.offset = offset; 1032 | offset += ii.heightFactor + marginOffset; 1033 | } 1034 | } else if (oldCurPosition > curItem.position) { 1035 | int itemIndex = mItems.size() - 1; 1036 | ItemInfo ii; 1037 | float offset = oldCurInfo.offset; 1038 | for (int pos = oldCurPosition - 1; 1039 | pos >= curItem.position && itemIndex >= 0; pos--) { 1040 | ii = mItems.get(itemIndex); 1041 | while (pos < ii.position && itemIndex > 0) { 1042 | itemIndex--; 1043 | ii = mItems.get(itemIndex); 1044 | } 1045 | while (pos > ii.position) { 1046 | // We don't have an item populated for this, 1047 | // ask the adapter for an offset. 1048 | offset -= mAdapter.getPageWidth(pos) + marginOffset; 1049 | pos--; 1050 | } 1051 | offset -= ii.heightFactor + marginOffset; 1052 | ii.offset = offset; 1053 | } 1054 | } 1055 | } 1056 | 1057 | // Base all offsets off of curItem. 1058 | final int itemCount = mItems.size(); 1059 | float offset = curItem.offset; 1060 | int pos = curItem.position - 1; 1061 | mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; 1062 | mLastOffset = curItem.position == N - 1 ? 1063 | curItem.offset + curItem.heightFactor - 1 : Float.MAX_VALUE; 1064 | // Previous pages 1065 | for (int i = curIndex - 1; i >= 0; i--, pos--) { 1066 | final ItemInfo ii = mItems.get(i); 1067 | while (pos > ii.position) { 1068 | offset -= mAdapter.getPageWidth(pos--) + marginOffset; 1069 | } 1070 | offset -= ii.heightFactor + marginOffset; 1071 | ii.offset = offset; 1072 | if (ii.position == 0) mFirstOffset = offset; 1073 | } 1074 | offset = curItem.offset + curItem.heightFactor + marginOffset; 1075 | pos = curItem.position + 1; 1076 | // Next pages 1077 | for (int i = curIndex + 1; i < itemCount; i++, pos++) { 1078 | final ItemInfo ii = mItems.get(i); 1079 | while (pos < ii.position) { 1080 | offset += mAdapter.getPageWidth(pos++) + marginOffset; 1081 | } 1082 | if (ii.position == N - 1) { 1083 | mLastOffset = offset + ii.heightFactor - 1; 1084 | } 1085 | ii.offset = offset; 1086 | offset += ii.heightFactor + marginOffset; 1087 | } 1088 | 1089 | boolean mNeedCalculatePageOffsets = false; 1090 | } 1091 | 1092 | /** 1093 | * This is the persistent state that is saved by ViewPager. Only needed 1094 | * if you are creating a sublass of ViewPager that must save its own 1095 | * state, in which case it should implement a subclass of this which 1096 | * contains that state. 1097 | */ 1098 | public static class SavedState extends BaseSavedState { 1099 | int position; 1100 | Parcelable adapterState; 1101 | ClassLoader loader; 1102 | 1103 | public SavedState(Parcelable superState) { 1104 | super(superState); 1105 | } 1106 | 1107 | @Override 1108 | public void writeToParcel(Parcel out, int flags) { 1109 | super.writeToParcel(out, flags); 1110 | out.writeInt(position); 1111 | out.writeParcelable(adapterState, flags); 1112 | } 1113 | 1114 | @NonNull 1115 | @Override 1116 | public String toString() { 1117 | return "FragmentPager.SavedState{" 1118 | + Integer.toHexString(System.identityHashCode(this)) 1119 | + " position=" + position + "}"; 1120 | } 1121 | 1122 | public static final Parcelable.ClassLoaderCreator CREATOR 1123 | = new Parcelable.ClassLoaderCreator<>() { 1124 | @Override 1125 | public SavedState createFromParcel(Parcel in, ClassLoader loader) { 1126 | return new SavedState(in, loader); 1127 | } 1128 | 1129 | @Override 1130 | public SavedState createFromParcel(Parcel parcel) { 1131 | return new SavedState(parcel, null); 1132 | } 1133 | 1134 | @Override 1135 | public SavedState[] newArray(int size) { 1136 | return new SavedState[size]; 1137 | } 1138 | }; 1139 | 1140 | SavedState(Parcel in, ClassLoader loader) { 1141 | super(in); 1142 | if (loader == null) { 1143 | loader = getClass().getClassLoader(); 1144 | } 1145 | position = in.readInt(); 1146 | adapterState = in.readParcelable(loader); 1147 | this.loader = loader; 1148 | } 1149 | } 1150 | 1151 | @Override 1152 | public Parcelable onSaveInstanceState() { 1153 | Parcelable superState = super.onSaveInstanceState(); 1154 | SavedState ss = new SavedState(superState); 1155 | ss.position = mCurItem; 1156 | if (mAdapter != null) { 1157 | ss.adapterState = mAdapter.saveState(); 1158 | } 1159 | return ss; 1160 | } 1161 | 1162 | @Override 1163 | public void onRestoreInstanceState(Parcelable state) { 1164 | if (!(state instanceof SavedState ss)) { 1165 | super.onRestoreInstanceState(state); 1166 | return; 1167 | } 1168 | 1169 | super.onRestoreInstanceState(ss.getSuperState()); 1170 | 1171 | if (mAdapter != null) { 1172 | mAdapter.restoreState(ss.adapterState, ss.loader); 1173 | setCurrentItemInternal(ss.position, false, true); 1174 | } else { 1175 | mRestoredCurItem = ss.position; 1176 | mRestoredAdapterState = ss.adapterState; 1177 | mRestoredClassLoader = ss.loader; 1178 | } 1179 | } 1180 | 1181 | @Override 1182 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 1183 | if (!checkLayoutParams(params)) { 1184 | params = generateLayoutParams(params); 1185 | } 1186 | final LayoutParams lp = (LayoutParams) params; 1187 | lp.isDecor |= child instanceof Decor; 1188 | if (mInLayout) { 1189 | if (lp.isDecor) { 1190 | throw new IllegalStateException("Cannot add pager decor view during layout"); 1191 | } 1192 | lp.needsMeasure = true; 1193 | addViewInLayout(child, index, params); 1194 | } else { 1195 | super.addView(child, index, params); 1196 | } 1197 | 1198 | if (USE_CACHE) { 1199 | if (child.getVisibility() != GONE) { 1200 | child.setDrawingCacheEnabled(mScrollingCacheEnabled); 1201 | } else { 1202 | child.setDrawingCacheEnabled(false); 1203 | } 1204 | } 1205 | } 1206 | 1207 | @Override 1208 | public void removeView(View view) { 1209 | if (mInLayout) { 1210 | removeViewInLayout(view); 1211 | } else { 1212 | super.removeView(view); 1213 | } 1214 | } 1215 | 1216 | ItemInfo infoForChild(View child) { 1217 | for (int i = 0; i < mItems.size(); i++) { 1218 | ItemInfo ii = mItems.get(i); 1219 | if (mAdapter.isViewFromObject(child, ii.object)) { 1220 | return ii; 1221 | } 1222 | } 1223 | return null; 1224 | } 1225 | 1226 | ItemInfo infoForAnyChild(View child) { 1227 | ViewParent parent; 1228 | while ((parent = child.getParent()) != this) { 1229 | if (!(parent instanceof View)) { 1230 | return null; 1231 | } 1232 | child = (View) parent; 1233 | } 1234 | return infoForChild(child); 1235 | } 1236 | 1237 | ItemInfo infoForPosition(int position) { 1238 | for (int i = 0; i < mItems.size(); i++) { 1239 | ItemInfo ii = mItems.get(i); 1240 | if (ii.position == position) { 1241 | return ii; 1242 | } 1243 | } 1244 | return null; 1245 | } 1246 | 1247 | @Override 1248 | protected void onAttachedToWindow() { 1249 | super.onAttachedToWindow(); 1250 | mFirstLayout = true; 1251 | } 1252 | 1253 | @SuppressLint("RtlHardcoded") 1254 | private boolean isHorizontalGravity(int hgrav) { 1255 | return hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT || hgrav == Gravity.START || hgrav == Gravity.END; 1256 | } 1257 | 1258 | @Override 1259 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1260 | // For simple implementation, our internal size is always 0. 1261 | // We depend on the container to specify the layout size of 1262 | // our view. We can't really know what it is since we will be 1263 | // adding and removing different arbitrary views and do not 1264 | // want the layout to change as this happens. 1265 | setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); 1266 | 1267 | final int measuredHeight = getMeasuredHeight(); 1268 | final int maxGutterSize = measuredHeight / 10; 1269 | mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); 1270 | 1271 | // Children are just made to fill our space. 1272 | int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 1273 | int childHeightSize = measuredHeight - getPaddingTop() - getPaddingBottom(); 1274 | 1275 | /* 1276 | * Make sure all children have been properly measured. Decor views first. 1277 | * Right now we cheat and make this less complicated by assuming decor 1278 | * views won't intersect. We will pin to edges based on gravity. 1279 | */ 1280 | int size = getChildCount(); 1281 | for (int i = 0; i < size; ++i) { 1282 | final View child = getChildAt(i); 1283 | if (child.getVisibility() != GONE) { 1284 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1285 | if (lp != null && lp.isDecor) { 1286 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1287 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1288 | int widthMode = MeasureSpec.AT_MOST; 1289 | int heightMode = MeasureSpec.AT_MOST; 1290 | boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; 1291 | boolean consumeHorizontal = isHorizontalGravity(hgrav); 1292 | 1293 | if (consumeVertical) { 1294 | widthMode = MeasureSpec.EXACTLY; 1295 | } else if (consumeHorizontal) { 1296 | heightMode = MeasureSpec.EXACTLY; 1297 | } 1298 | 1299 | int widthSize = childWidthSize; 1300 | int heightSize = childHeightSize; 1301 | if (lp.width != LayoutParams.WRAP_CONTENT) { 1302 | widthMode = MeasureSpec.EXACTLY; 1303 | if (lp.width != LayoutParams.MATCH_PARENT) { 1304 | widthSize = lp.width; 1305 | } 1306 | } 1307 | if (lp.height != LayoutParams.WRAP_CONTENT) { 1308 | heightMode = MeasureSpec.EXACTLY; 1309 | if (lp.height != LayoutParams.MATCH_PARENT) { 1310 | heightSize = lp.height; 1311 | } 1312 | } 1313 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); 1314 | final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); 1315 | child.measure(widthSpec, heightSpec); 1316 | 1317 | if (consumeVertical) { 1318 | childHeightSize -= child.getMeasuredHeight(); 1319 | } else if (consumeHorizontal) { 1320 | childWidthSize -= child.getMeasuredWidth(); 1321 | } 1322 | } 1323 | } 1324 | } 1325 | 1326 | int mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); 1327 | int mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); 1328 | 1329 | // Make sure we have created all fragments that we need to have shown. 1330 | mInLayout = true; 1331 | populate(); 1332 | mInLayout = false; 1333 | 1334 | // Page views next. 1335 | size = getChildCount(); 1336 | for (int i = 0; i < size; ++i) { 1337 | final View child = getChildAt(i); 1338 | if (child.getVisibility() != GONE) { 1339 | if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec); 1340 | 1341 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1342 | if (lp == null || !lp.isDecor) { 1343 | final float heightFactor = lp == null ? 0f : lp.heightFactor; 1344 | final int heightSpec = MeasureSpec.makeMeasureSpec( 1345 | (int) (childHeightSize * heightFactor), MeasureSpec.EXACTLY); 1346 | child.measure(mChildWidthMeasureSpec, heightSpec); 1347 | } 1348 | } 1349 | } 1350 | } 1351 | 1352 | @Override 1353 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1354 | super.onSizeChanged(w, h, oldw, oldh); 1355 | 1356 | // Make sure scroll position is set correctly. 1357 | if (h != oldh) { 1358 | recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin); 1359 | } 1360 | } 1361 | 1362 | private void recomputeScrollPosition(int height, int oldHeight, int margin, int oldMargin) { 1363 | if (oldHeight > 0 && !mItems.isEmpty()) { 1364 | final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin; 1365 | final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom() 1366 | + oldMargin; 1367 | final int ypos = getScrollY(); 1368 | final float pageOffset = (float) ypos / oldHeightWithMargin; 1369 | final int newOffsetPixels = (int) (pageOffset * heightWithMargin); 1370 | 1371 | scrollTo(getScrollX(), newOffsetPixels); 1372 | if (!mScroller.isFinished()) { 1373 | // We now return to your regularly scheduled scroll, already in progress. 1374 | final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1375 | ItemInfo targetInfo = infoForPosition(mCurItem); 1376 | mScroller.startScroll(0, newOffsetPixels, 1377 | 0, (int) (targetInfo.offset * height), newDuration); 1378 | } 1379 | } else { 1380 | final ItemInfo ii = infoForPosition(mCurItem); 1381 | final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; 1382 | final int scrollPos = (int) (scrollOffset * 1383 | (height - getPaddingTop() - getPaddingBottom())); 1384 | if (scrollPos != getScrollY()) { 1385 | completeScroll(false); 1386 | scrollTo(getScrollX(), scrollPos); 1387 | } 1388 | } 1389 | } 1390 | 1391 | @SuppressLint("RtlHardcoded") 1392 | @Override 1393 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1394 | final int count = getChildCount(); 1395 | int width = r - l; 1396 | int height = b - t; 1397 | int paddingLeft = getPaddingLeft(); 1398 | int paddingTop = getPaddingTop(); 1399 | int paddingRight = getPaddingRight(); 1400 | int paddingBottom = getPaddingBottom(); 1401 | final int scrollY = getScrollY(); 1402 | 1403 | int decorCount = 0; 1404 | 1405 | // First pass - decor views. We need to do this in two passes so that 1406 | // we have the proper offsets for non-decor views later. 1407 | for (int i = 0; i < count; i++) { 1408 | final View child = getChildAt(i); 1409 | if (child.getVisibility() != GONE) { 1410 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1411 | int childLeft; 1412 | int childTop; 1413 | if (lp.isDecor) { 1414 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1415 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1416 | switch (hgrav) { 1417 | //noinspection DataFlowIssue 1418 | case Gravity.LEFT, Gravity.START -> { 1419 | childLeft = paddingLeft; 1420 | paddingLeft += child.getMeasuredWidth(); 1421 | } 1422 | case Gravity.CENTER_HORIZONTAL -> childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); 1423 | //noinspection DataFlowIssue 1424 | case Gravity.RIGHT, Gravity.END -> { 1425 | childLeft = width - paddingRight - child.getMeasuredWidth(); 1426 | paddingRight += child.getMeasuredWidth(); 1427 | } 1428 | default -> childLeft = paddingLeft; 1429 | } 1430 | switch (vgrav) { 1431 | case Gravity.TOP -> { 1432 | childTop = paddingTop; 1433 | paddingTop += child.getMeasuredHeight(); 1434 | } 1435 | case Gravity.CENTER_VERTICAL -> childTop = Math.max((height - child.getMeasuredHeight()) / 2, paddingTop); 1436 | case Gravity.BOTTOM -> { 1437 | childTop = height - paddingBottom - child.getMeasuredHeight(); 1438 | paddingBottom += child.getMeasuredHeight(); 1439 | } 1440 | default -> childTop = paddingTop; 1441 | } 1442 | childTop += scrollY; 1443 | child.layout(childLeft, childTop, 1444 | childLeft + child.getMeasuredWidth(), 1445 | childTop + child.getMeasuredHeight()); 1446 | decorCount++; 1447 | } 1448 | } 1449 | } 1450 | 1451 | final int childHeight = height - paddingTop - paddingBottom; 1452 | // Page views. Do this once we have the right padding offsets from above. 1453 | for (int i = 0; i < count; i++) { 1454 | final View child = getChildAt(i); 1455 | if (child.getVisibility() != GONE) { 1456 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1457 | ItemInfo ii; 1458 | if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1459 | int toff = (int) (childHeight * ii.offset); 1460 | //noinspection UnnecessaryLocalVariable 1461 | int childLeft = paddingLeft; 1462 | int childTop = paddingTop + toff; 1463 | if (lp.needsMeasure) { 1464 | // This was added during layout and needs measurement. 1465 | // Do it now that we know what we're working with. 1466 | lp.needsMeasure = false; 1467 | final int widthSpec = MeasureSpec.makeMeasureSpec( 1468 | (int) (width - paddingLeft - paddingRight), 1469 | MeasureSpec.EXACTLY); 1470 | final int heightSpec = MeasureSpec.makeMeasureSpec( 1471 | (int) (childHeight * lp.heightFactor), 1472 | MeasureSpec.EXACTLY); 1473 | child.measure(widthSpec, heightSpec); 1474 | } 1475 | if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1476 | + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1477 | + "x" + child.getMeasuredHeight()); 1478 | child.layout(childLeft, childTop, 1479 | childLeft + child.getMeasuredWidth(), 1480 | childTop + child.getMeasuredHeight()); 1481 | } 1482 | } 1483 | } 1484 | mLeftPageBounds = paddingLeft; 1485 | mRightPageBounds = width - paddingRight; 1486 | mDecorChildCount = decorCount; 1487 | 1488 | if (mFirstLayout) { 1489 | scrollToItem(mCurItem, false, 0, false); 1490 | } 1491 | mFirstLayout = false; 1492 | } 1493 | 1494 | @Override 1495 | public void computeScroll() { 1496 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1497 | int oldX = getScrollX(); 1498 | int oldY = getScrollY(); 1499 | int x = mScroller.getCurrX(); 1500 | int y = mScroller.getCurrY(); 1501 | 1502 | if (oldX != x || oldY != y) { 1503 | scrollTo(x, y); 1504 | if (!pageScrolled(y)) { 1505 | mScroller.abortAnimation(); 1506 | scrollTo(x, 0); 1507 | } 1508 | } 1509 | 1510 | // Keep on drawing until the animation has finished. 1511 | ViewCompat.postInvalidateOnAnimation(this); 1512 | return; 1513 | } 1514 | 1515 | // Done with scroll, clean up state. 1516 | completeScroll(true); 1517 | } 1518 | 1519 | private boolean pageScrolled(int ypos) { 1520 | if (mItems.size() == 0) { 1521 | mCalledSuper = false; 1522 | onPageScrolled(0, 0, 0); 1523 | if (!mCalledSuper) { 1524 | throw new IllegalStateException( 1525 | "onPageScrolled did not call superclass implementation"); 1526 | } 1527 | return false; 1528 | } 1529 | final ItemInfo ii = infoForCurrentScrollPosition(); 1530 | final int height = getClientHeight(); 1531 | final int heightWithMargin = height + mPageMargin; 1532 | final float marginOffset = (float) mPageMargin / height; 1533 | final int currentPage = ii.position; 1534 | final float pageOffset = (((float) ypos / height) - ii.offset) / 1535 | (ii.heightFactor + marginOffset); 1536 | final int offsetPixels = (int) (pageOffset * heightWithMargin); 1537 | 1538 | mCalledSuper = false; 1539 | onPageScrolled(currentPage, pageOffset, offsetPixels); 1540 | if (!mCalledSuper) { 1541 | throw new IllegalStateException( 1542 | "onPageScrolled did not call superclass implementation"); 1543 | } 1544 | return true; 1545 | } 1546 | 1547 | /** 1548 | * This method will be invoked when the current page is scrolled, either as part 1549 | * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1550 | * If you override this method you must call through to the superclass implementation 1551 | * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1552 | * returns. 1553 | * 1554 | * @param position Position index of the first page currently being displayed. 1555 | * Page position+1 will be visible if positionOffset is nonzero. 1556 | * @param offset Value from [0, 1) indicating the offset from the page at position. 1557 | * @param offsetPixels Value in pixels indicating the offset from position. 1558 | */ 1559 | protected void onPageScrolled(int position, float offset, int offsetPixels) { 1560 | // Offset any decor views if needed - keep them on-screen at all times. 1561 | if (mDecorChildCount > 0) { 1562 | final int scrollY = getScrollY(); 1563 | int paddingTop = getPaddingTop(); 1564 | int paddingBottom = getPaddingBottom(); 1565 | final int height = getHeight(); 1566 | final int childCount = getChildCount(); 1567 | for (int i = 0; i < childCount; i++) { 1568 | final View child = getChildAt(i); 1569 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1570 | if (!lp.isDecor) continue; 1571 | 1572 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1573 | int childTop; 1574 | switch (vgrav) { 1575 | case Gravity.TOP -> { 1576 | childTop = paddingTop; 1577 | paddingTop += child.getHeight(); 1578 | } 1579 | case Gravity.CENTER_VERTICAL -> childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1580 | paddingTop); 1581 | case Gravity.BOTTOM -> { 1582 | childTop = height - paddingBottom - child.getMeasuredHeight(); 1583 | paddingBottom += child.getMeasuredHeight(); 1584 | } 1585 | default -> childTop = paddingTop; 1586 | } 1587 | childTop += scrollY; 1588 | 1589 | final int childOffset = childTop - child.getTop(); 1590 | if (childOffset != 0) { 1591 | child.offsetTopAndBottom(childOffset); 1592 | } 1593 | } 1594 | } 1595 | 1596 | if (mOnPageChangeListener != null) { 1597 | mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1598 | } 1599 | if (mInternalPageChangeListener != null) { 1600 | mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1601 | } 1602 | 1603 | if (mPageTransformer != null) { 1604 | final int scrollY = getScrollY(); 1605 | final int childCount = getChildCount(); 1606 | for (int i = 0; i < childCount; i++) { 1607 | final View child = getChildAt(i); 1608 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1609 | 1610 | if (lp.isDecor) continue; 1611 | 1612 | final float transformPos = (float) (child.getTop() - scrollY) / getClientHeight(); 1613 | mPageTransformer.transformPage(child, transformPos); 1614 | } 1615 | } 1616 | 1617 | mCalledSuper = true; 1618 | } 1619 | 1620 | private void completeScroll(boolean postEvents) { 1621 | boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; 1622 | if (needPopulate) { 1623 | // Done with scroll, no longer want to cache view drawing. 1624 | setScrollingCacheEnabled(false); 1625 | mScroller.abortAnimation(); 1626 | int oldX = getScrollX(); 1627 | int oldY = getScrollY(); 1628 | int x = mScroller.getCurrX(); 1629 | int y = mScroller.getCurrY(); 1630 | if (oldX != x || oldY != y) { 1631 | scrollTo(x, y); 1632 | } 1633 | } 1634 | mPopulatePending = false; 1635 | for (int i = 0; i < mItems.size(); i++) { 1636 | ItemInfo ii = mItems.get(i); 1637 | if (ii.scrolling) { 1638 | needPopulate = true; 1639 | ii.scrolling = false; 1640 | } 1641 | } 1642 | if (needPopulate) { 1643 | if (postEvents) { 1644 | ViewCompat.postOnAnimation(this, mEndScrollRunnable); 1645 | } else { 1646 | mEndScrollRunnable.run(); 1647 | } 1648 | } 1649 | } 1650 | 1651 | private boolean isGutterDrag(float y, float dy) { 1652 | return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0); 1653 | } 1654 | 1655 | private void enableLayers(boolean enable) { 1656 | final int childCount = getChildCount(); 1657 | for (int i = 0; i < childCount; i++) { 1658 | final int layerType = enable ? 1659 | View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; 1660 | getChildAt(i).setLayerType(layerType, null); 1661 | } 1662 | } 1663 | 1664 | @Override 1665 | public boolean onInterceptTouchEvent(MotionEvent ev) { 1666 | /* 1667 | * This method JUST determines whether we want to intercept the motion. 1668 | * If we return true, onMotionEvent will be called and we do the actual 1669 | * scrolling there. 1670 | */ 1671 | 1672 | final int action = ev.getAction() & MotionEvent.ACTION_MASK; 1673 | 1674 | // Always take care of the touch gesture being complete. 1675 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1676 | // Release the drag. 1677 | if (DEBUG) Log.v(TAG, "Intercept done!"); 1678 | mIsBeingDragged = false; 1679 | mIsUnableToDrag = false; 1680 | mActivePointerId = INVALID_POINTER; 1681 | if (mVelocityTracker != null) { 1682 | mVelocityTracker.recycle(); 1683 | mVelocityTracker = null; 1684 | } 1685 | return false; 1686 | } 1687 | 1688 | // Nothing more to do here if we have decided whether or not we 1689 | // are dragging. 1690 | if (action != MotionEvent.ACTION_DOWN) { 1691 | if (mIsBeingDragged) { 1692 | if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1693 | return true; 1694 | } 1695 | if (mIsUnableToDrag) { 1696 | if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1697 | return false; 1698 | } 1699 | } 1700 | 1701 | switch (action) { 1702 | case MotionEvent.ACTION_MOVE -> { 1703 | /* 1704 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1705 | * whether the user has moved far enough from his original down touch. 1706 | */ 1707 | 1708 | /* 1709 | * Locally do absolute value. mLastMotionY is set to the y value 1710 | * of the down event. 1711 | */ 1712 | final int activePointerId = mActivePointerId; 1713 | if (activePointerId == INVALID_POINTER) { 1714 | // If we don't have a valid id, the touch down wasn't on content. 1715 | break; 1716 | } 1717 | 1718 | final int pointerIndex = ev.findPointerIndex(activePointerId); 1719 | final float y = ev.getY(pointerIndex); 1720 | final float dy = y - mLastMotionY; 1721 | final float yDiff = Math.abs(dy); 1722 | final float x = ev.getX(pointerIndex); 1723 | final float xDiff = Math.abs(x - mInitialMotionX); 1724 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1725 | 1726 | if (dy != 0 && !isGutterDrag(mLastMotionY, dy) && 1727 | canScroll(this, false, (int) dy, (int) x, (int) y)) { 1728 | // Nested view has scrollable area under this point. Let it be handled there. 1729 | mLastMotionX = x; 1730 | mLastMotionY = y; 1731 | mIsUnableToDrag = true; 1732 | return false; 1733 | } 1734 | if (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) { 1735 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1736 | mIsBeingDragged = true; 1737 | requestParentDisallowInterceptTouchEvent(true); 1738 | setScrollState(SCROLL_STATE_DRAGGING); 1739 | mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop : 1740 | mInitialMotionY - mTouchSlop; 1741 | mLastMotionX = x; 1742 | setScrollingCacheEnabled(true); 1743 | } else if (xDiff > mTouchSlop) { 1744 | // The finger has moved enough in the vertical 1745 | // direction to be counted as a drag... abort 1746 | // any attempt to drag horizontally, to work correctly 1747 | // with children that have scrolling containers. 1748 | if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1749 | mIsUnableToDrag = true; 1750 | } 1751 | if (mIsBeingDragged) { 1752 | // Scroll to follow the motion event 1753 | if (performDrag(y)) { 1754 | ViewCompat.postInvalidateOnAnimation(this); 1755 | } 1756 | } 1757 | } 1758 | case MotionEvent.ACTION_DOWN -> { 1759 | /* 1760 | * Remember location of down touch. 1761 | * ACTION_DOWN always refers to pointer index 0. 1762 | */ 1763 | mLastMotionX = mInitialMotionX = ev.getX(); 1764 | mLastMotionY = mInitialMotionY = ev.getY(); 1765 | mActivePointerId = ev.getPointerId(0); 1766 | mIsUnableToDrag = false; 1767 | 1768 | mScroller.computeScrollOffset(); 1769 | if (mScrollState == SCROLL_STATE_SETTLING && 1770 | Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) { 1771 | // Let the user 'catch' the pager as it animates. 1772 | mScroller.abortAnimation(); 1773 | mPopulatePending = false; 1774 | populate(); 1775 | mIsBeingDragged = true; 1776 | requestParentDisallowInterceptTouchEvent(true); 1777 | setScrollState(SCROLL_STATE_DRAGGING); 1778 | } else { 1779 | completeScroll(false); 1780 | mIsBeingDragged = false; 1781 | } 1782 | 1783 | if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1784 | + " mIsBeingDragged=" + mIsBeingDragged 1785 | + "mIsUnableToDrag=" + mIsUnableToDrag); 1786 | } 1787 | case MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev); 1788 | } 1789 | 1790 | if (mVelocityTracker == null) { 1791 | mVelocityTracker = VelocityTracker.obtain(); 1792 | } 1793 | mVelocityTracker.addMovement(ev); 1794 | 1795 | /* 1796 | * The only time we want to intercept motion events is if we are in the 1797 | * drag mode. 1798 | */ 1799 | return mIsBeingDragged; 1800 | } 1801 | 1802 | @Override 1803 | public boolean onTouchEvent(MotionEvent ev) { 1804 | if (mFakeDragging) { 1805 | // A fake drag is in progress already, ignore this real one 1806 | // but still eat the touch events. 1807 | // (It is likely that the user is multi-touching the screen.) 1808 | return true; 1809 | } 1810 | 1811 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 1812 | // Don't handle edge touches immediately -- they may actually belong to one of our 1813 | // descendants. 1814 | return false; 1815 | } 1816 | 1817 | if (mAdapter == null || mAdapter.getCount() == 0) { 1818 | // Nothing to present or scroll; nothing to touch. 1819 | return false; 1820 | } 1821 | 1822 | if (mVelocityTracker == null) { 1823 | mVelocityTracker = VelocityTracker.obtain(); 1824 | } 1825 | mVelocityTracker.addMovement(ev); 1826 | 1827 | final int action = ev.getAction(); 1828 | boolean needsInvalidate = false; 1829 | 1830 | switch (action & MotionEvent.ACTION_MASK) { 1831 | case MotionEvent.ACTION_DOWN -> { 1832 | mScroller.abortAnimation(); 1833 | mPopulatePending = false; 1834 | populate(); 1835 | 1836 | // Remember where the motion event started 1837 | mLastMotionX = mInitialMotionX = ev.getX(); 1838 | mLastMotionY = mInitialMotionY = ev.getY(); 1839 | mActivePointerId = ev.getPointerId(0); 1840 | } 1841 | case MotionEvent.ACTION_MOVE -> { 1842 | if (!mIsBeingDragged) { 1843 | final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1844 | final float y = ev.getY(pointerIndex); 1845 | final float yDiff = Math.abs(y - mLastMotionY); 1846 | final float x = ev.getX(pointerIndex); 1847 | final float xDiff = Math.abs(x - mLastMotionX); 1848 | if (DEBUG) 1849 | Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1850 | if (yDiff > mTouchSlop && yDiff > xDiff) { 1851 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1852 | mIsBeingDragged = true; 1853 | requestParentDisallowInterceptTouchEvent(true); 1854 | mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop : 1855 | mInitialMotionY - mTouchSlop; 1856 | mLastMotionX = x; 1857 | setScrollState(SCROLL_STATE_DRAGGING); 1858 | setScrollingCacheEnabled(true); 1859 | 1860 | // Disallow Parent Intercept, just in case 1861 | ViewParent parent = getParent(); 1862 | if (parent != null) { 1863 | parent.requestDisallowInterceptTouchEvent(true); 1864 | } 1865 | } 1866 | } 1867 | // Not else! Note that mIsBeingDragged can be set above. 1868 | if (mIsBeingDragged) { 1869 | // Scroll to follow the motion event 1870 | final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 1871 | final float y = ev.getY(activePointerIndex); 1872 | needsInvalidate = performDrag(y); 1873 | } 1874 | } 1875 | case MotionEvent.ACTION_UP -> { 1876 | if (mIsBeingDragged) { 1877 | final VelocityTracker velocityTracker = mVelocityTracker; 1878 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1879 | int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 1880 | mPopulatePending = true; 1881 | final int height = getClientHeight(); 1882 | final int scrollY = getScrollY(); 1883 | final ItemInfo ii = infoForCurrentScrollPosition(); 1884 | final int currentPage = ii.position; 1885 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor; 1886 | final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 1887 | final float y = ev.getY(activePointerIndex); 1888 | final int totalDelta = (int) (y - mInitialMotionY); 1889 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); 1890 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 1891 | 1892 | mActivePointerId = INVALID_POINTER; 1893 | endDrag(); 1894 | mTopEdge.onRelease(); 1895 | mBottomEdge.onRelease(); 1896 | } 1897 | } 1898 | case MotionEvent.ACTION_CANCEL -> { 1899 | if (mIsBeingDragged) { 1900 | scrollToItem(mCurItem, true, 0, false); 1901 | mActivePointerId = INVALID_POINTER; 1902 | endDrag(); 1903 | mTopEdge.onRelease(); 1904 | mBottomEdge.onRelease(); 1905 | } 1906 | } 1907 | case MotionEvent.ACTION_POINTER_DOWN -> { 1908 | final int index = ev.getActionIndex(); 1909 | mLastMotionY = ev.getY(index); 1910 | mActivePointerId = ev.getPointerId(index); 1911 | } 1912 | case MotionEvent.ACTION_POINTER_UP -> { 1913 | onSecondaryPointerUp(ev); 1914 | mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId)); 1915 | } 1916 | } 1917 | if (needsInvalidate) { 1918 | ViewCompat.postInvalidateOnAnimation(this); 1919 | } 1920 | return true; 1921 | } 1922 | 1923 | /** @noinspection SameParameterValue*/ 1924 | private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { 1925 | final ViewParent parent = getParent(); 1926 | if (parent != null) { 1927 | parent.requestDisallowInterceptTouchEvent(disallowIntercept); 1928 | } 1929 | } 1930 | 1931 | private boolean performDrag(float y) { 1932 | boolean needsInvalidate = false; 1933 | 1934 | final float deltaY = mLastMotionY - y; 1935 | mLastMotionY = y; 1936 | 1937 | float oldScrollY = getScrollY(); 1938 | float scrollY = oldScrollY + deltaY; 1939 | final int height = getClientHeight(); 1940 | 1941 | float topBound = height * mFirstOffset; 1942 | float bottomBound = height * mLastOffset; 1943 | boolean topAbsolute = true; 1944 | boolean bottomAbsolute = true; 1945 | 1946 | final ItemInfo firstItem = mItems.get(0); 1947 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1948 | if (firstItem.position != 0) { 1949 | topAbsolute = false; 1950 | topBound = firstItem.offset * height; 1951 | } 1952 | if (lastItem.position != mAdapter.getCount() - 1) { 1953 | bottomAbsolute = false; 1954 | bottomBound = lastItem.offset * height; 1955 | } 1956 | 1957 | if (scrollY < topBound) { 1958 | if (topAbsolute) { 1959 | float over = topBound - scrollY; 1960 | mTopEdge.onPull(Math.abs(over) / height); 1961 | } 1962 | scrollY = topBound; 1963 | } else if (scrollY > bottomBound) { 1964 | if (bottomAbsolute) { 1965 | float over = scrollY - bottomBound; 1966 | mBottomEdge.onPull(Math.abs(over) / height); 1967 | } 1968 | scrollY = bottomBound; 1969 | } 1970 | // Don't lose the rounded component 1971 | mLastMotionX += scrollY - (int) scrollY; 1972 | scrollTo(getScrollX(), (int) scrollY); 1973 | pageScrolled((int) scrollY); 1974 | 1975 | return needsInvalidate; 1976 | } 1977 | 1978 | /** 1979 | * @return Info about the page at the current scroll position. 1980 | * This can be synthetic for a missing middle page; the 'object' field can be null. 1981 | */ 1982 | private ItemInfo infoForCurrentScrollPosition() { 1983 | final int height = getClientHeight(); 1984 | final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0; 1985 | final float marginOffset = height > 0 ? (float) mPageMargin / height : 0; 1986 | int lastPos = -1; 1987 | float lastOffset = 0.f; 1988 | float lastHeight = 0.f; 1989 | boolean first = true; 1990 | 1991 | ItemInfo lastItem = null; 1992 | for (int i = 0; i < mItems.size(); i++) { 1993 | ItemInfo ii = mItems.get(i); 1994 | float offset; 1995 | if (!first && ii.position != lastPos + 1) { 1996 | // Create a synthetic item for a missing page. 1997 | ii = mTempItem; 1998 | ii.offset = lastOffset + lastHeight + marginOffset; 1999 | ii.position = lastPos + 1; 2000 | ii.heightFactor = mAdapter.getPageWidth(ii.position); 2001 | i--; 2002 | } 2003 | offset = ii.offset; 2004 | 2005 | final float topBound = offset; 2006 | final float bottomBound = offset + ii.heightFactor + marginOffset; 2007 | if (first || scrollOffset >= topBound) { 2008 | if (scrollOffset < bottomBound || i == mItems.size() - 1) { 2009 | return ii; 2010 | } 2011 | } else { 2012 | return lastItem; 2013 | } 2014 | first = false; 2015 | lastPos = ii.position; 2016 | lastOffset = offset; 2017 | lastHeight = ii.heightFactor; 2018 | lastItem = ii; 2019 | } 2020 | 2021 | return lastItem; 2022 | } 2023 | 2024 | private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaY) { 2025 | int targetPage; 2026 | if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 2027 | targetPage = velocity > 0 ? currentPage : currentPage + 1; 2028 | } else { 2029 | final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; 2030 | targetPage = (int) (currentPage + pageOffset + truncator); 2031 | } 2032 | 2033 | if (mItems.size() > 0) { 2034 | final ItemInfo firstItem = mItems.get(0); 2035 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2036 | 2037 | // Only let the user target pages we have items for 2038 | targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 2039 | } 2040 | 2041 | return targetPage; 2042 | } 2043 | 2044 | @Override 2045 | public void draw(@NonNull Canvas canvas) { 2046 | super.draw(canvas); 2047 | boolean needsInvalidate = false; 2048 | 2049 | final int overScrollMode = this.getOverScrollMode(); 2050 | if (overScrollMode == View.OVER_SCROLL_ALWAYS || 2051 | (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && 2052 | mAdapter != null && mAdapter.getCount() > 1)) { 2053 | if (!mTopEdge.isFinished()) { 2054 | final int restoreCount = canvas.save(); 2055 | final int height = getHeight(); 2056 | final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 2057 | 2058 | canvas.translate(getPaddingLeft(), mFirstOffset * height); 2059 | mTopEdge.setSize(width, height); 2060 | needsInvalidate = mTopEdge.draw(canvas); 2061 | canvas.restoreToCount(restoreCount); 2062 | } 2063 | if (!mBottomEdge.isFinished()) { 2064 | final int restoreCount = canvas.save(); 2065 | final int height = getHeight(); 2066 | final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 2067 | 2068 | canvas.rotate(180); 2069 | canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height); 2070 | mBottomEdge.setSize(width, height); 2071 | needsInvalidate |= mBottomEdge.draw(canvas); 2072 | canvas.restoreToCount(restoreCount); 2073 | } 2074 | } else { 2075 | mTopEdge.finish(); 2076 | mBottomEdge.finish(); 2077 | } 2078 | 2079 | if (needsInvalidate) { 2080 | // Keep animating 2081 | ViewCompat.postInvalidateOnAnimation(this); 2082 | } 2083 | } 2084 | 2085 | @Override 2086 | protected void onDraw(@NonNull Canvas canvas) { 2087 | super.onDraw(canvas); 2088 | 2089 | // Draw the margin drawable between pages if needed. 2090 | if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 2091 | final int scrollY = getScrollY(); 2092 | final int height = getHeight(); 2093 | 2094 | final float marginOffset = (float) mPageMargin / height; 2095 | int itemIndex = 0; 2096 | ItemInfo ii = mItems.get(0); 2097 | float offset = ii.offset; 2098 | final int itemCount = mItems.size(); 2099 | final int firstPos = ii.position; 2100 | final int lastPos = mItems.get(itemCount - 1).position; 2101 | for (int pos = firstPos; pos < lastPos; pos++) { 2102 | while (pos > ii.position && itemIndex < itemCount) { 2103 | ii = mItems.get(++itemIndex); 2104 | } 2105 | 2106 | float drawAt; 2107 | if (pos == ii.position) { 2108 | drawAt = (ii.offset + ii.heightFactor) * height; 2109 | offset = ii.offset + ii.heightFactor + marginOffset; 2110 | } else { 2111 | float heightFactor = mAdapter.getPageWidth(pos); 2112 | drawAt = (offset + heightFactor) * height; 2113 | offset += heightFactor + marginOffset; 2114 | } 2115 | 2116 | if (drawAt + mPageMargin > scrollY) { 2117 | mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt, 2118 | mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f)); 2119 | mMarginDrawable.draw(canvas); 2120 | } 2121 | 2122 | if (drawAt > scrollY + height) { 2123 | break; // No more visible, no sense in continuing 2124 | } 2125 | } 2126 | } 2127 | } 2128 | 2129 | /** 2130 | * Start a fake drag of the pager. 2131 | *

2132 | *

A fake drag can be useful if you want to synchronize the motion of the ViewPager 2133 | * with the touch scrolling of another view, while still letting the ViewPager 2134 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 2135 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 2136 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 2137 | *

2138 | *

During a fake drag the ViewPager will ignore all touch events. If a real drag 2139 | * is already in progress, this method will return false. 2140 | * 2141 | * @return true if the fake drag began successfully, false if it could not be started. 2142 | * @see #fakeDragBy(float) 2143 | * @see #endFakeDrag() 2144 | */ 2145 | public boolean beginFakeDrag() { 2146 | if (mIsBeingDragged) { 2147 | return false; 2148 | } 2149 | mFakeDragging = true; 2150 | setScrollState(SCROLL_STATE_DRAGGING); 2151 | mInitialMotionY = mLastMotionY = 0; 2152 | if (mVelocityTracker == null) { 2153 | mVelocityTracker = VelocityTracker.obtain(); 2154 | } else { 2155 | mVelocityTracker.clear(); 2156 | } 2157 | final long time = SystemClock.uptimeMillis(); 2158 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 2159 | mVelocityTracker.addMovement(ev); 2160 | ev.recycle(); 2161 | mFakeDragBeginTime = time; 2162 | return true; 2163 | } 2164 | 2165 | /** 2166 | * End a fake drag of the pager. 2167 | * 2168 | * @see #beginFakeDrag() 2169 | * @see #fakeDragBy(float) 2170 | */ 2171 | public void endFakeDrag() { 2172 | if (!mFakeDragging) { 2173 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2174 | } 2175 | 2176 | final VelocityTracker velocityTracker = mVelocityTracker; 2177 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2178 | int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 2179 | mPopulatePending = true; 2180 | final int height = getClientHeight(); 2181 | final int scrollY = getScrollY(); 2182 | final ItemInfo ii = infoForCurrentScrollPosition(); 2183 | final int currentPage = ii.position; 2184 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor; 2185 | final int totalDelta = (int) (mLastMotionY - mInitialMotionY); 2186 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2187 | totalDelta); 2188 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 2189 | endDrag(); 2190 | 2191 | mFakeDragging = false; 2192 | } 2193 | 2194 | /** 2195 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 2196 | * 2197 | * @param yOffset Offset in pixels to drag by. 2198 | * @see #beginFakeDrag() 2199 | * @see #endFakeDrag() 2200 | */ 2201 | public void fakeDragBy(float yOffset) { 2202 | if (!mFakeDragging) { 2203 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2204 | } 2205 | 2206 | mLastMotionY += yOffset; 2207 | 2208 | float oldScrollY = getScrollY(); 2209 | float scrollY = oldScrollY - yOffset; 2210 | final int height = getClientHeight(); 2211 | 2212 | float topBound = height * mFirstOffset; 2213 | float bottomBound = height * mLastOffset; 2214 | 2215 | final ItemInfo firstItem = mItems.get(0); 2216 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2217 | if (firstItem.position != 0) { 2218 | topBound = firstItem.offset * height; 2219 | } 2220 | if (lastItem.position != mAdapter.getCount() - 1) { 2221 | bottomBound = lastItem.offset * height; 2222 | } 2223 | 2224 | if (scrollY < topBound) { 2225 | scrollY = topBound; 2226 | } else if (scrollY > bottomBound) { 2227 | scrollY = bottomBound; 2228 | } 2229 | // Don't lose the rounded component 2230 | mLastMotionY += scrollY - (int) scrollY; 2231 | scrollTo(getScrollX(), (int) scrollY); 2232 | pageScrolled((int) scrollY); 2233 | 2234 | // Synthesize an event for the VelocityTracker. 2235 | final long time = SystemClock.uptimeMillis(); 2236 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 2237 | 0, mLastMotionY, 0); 2238 | mVelocityTracker.addMovement(ev); 2239 | ev.recycle(); 2240 | } 2241 | 2242 | /** 2243 | * Returns true if a fake drag is in progress. 2244 | * 2245 | * @return true if currently in a fake drag, false otherwise. 2246 | * @see #beginFakeDrag() 2247 | * @see #fakeDragBy(float) 2248 | * @see #endFakeDrag() 2249 | */ 2250 | public boolean isFakeDragging() { 2251 | return mFakeDragging; 2252 | } 2253 | 2254 | private void onSecondaryPointerUp(MotionEvent ev) { 2255 | final int pointerIndex = ev.getActionIndex(); 2256 | final int pointerId = ev.getPointerId(pointerIndex); 2257 | if (pointerId == mActivePointerId) { 2258 | // This was our active pointer going up. Choose a new 2259 | // active pointer and adjust accordingly. 2260 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2261 | mLastMotionY = ev.getY(newPointerIndex); 2262 | mActivePointerId = ev.getPointerId(newPointerIndex); 2263 | if (mVelocityTracker != null) { 2264 | mVelocityTracker.clear(); 2265 | } 2266 | } 2267 | } 2268 | 2269 | private void endDrag() { 2270 | mIsBeingDragged = false; 2271 | mIsUnableToDrag = false; 2272 | 2273 | if (mVelocityTracker != null) { 2274 | mVelocityTracker.recycle(); 2275 | mVelocityTracker = null; 2276 | } 2277 | } 2278 | 2279 | private void setScrollingCacheEnabled(boolean enabled) { 2280 | if (mScrollingCacheEnabled != enabled) { 2281 | mScrollingCacheEnabled = enabled; 2282 | if (USE_CACHE) { 2283 | final int size = getChildCount(); 2284 | for (int i = 0; i < size; ++i) { 2285 | final View child = getChildAt(i); 2286 | if (child.getVisibility() != GONE) { 2287 | child.setDrawingCacheEnabled(enabled); 2288 | } 2289 | } 2290 | } 2291 | } 2292 | } 2293 | 2294 | public boolean internalCanScrollVertically(int direction) { 2295 | if (mAdapter == null) { 2296 | return false; 2297 | } 2298 | 2299 | final int height = getClientHeight(); 2300 | final int scrollY = getScrollY(); 2301 | if (direction < 0) { 2302 | return (scrollY > (int) (height * mFirstOffset)); 2303 | } else if (direction > 0) { 2304 | return (scrollY < (int) (height * mLastOffset)); 2305 | } else { 2306 | return false; 2307 | } 2308 | } 2309 | 2310 | /** 2311 | * Tests scrollability within child views of v given a delta of dx. 2312 | * 2313 | * @param v View to test for horizontal scrollability 2314 | * @param checkV Whether the view v passed should itself be checked for scrollability (true), 2315 | * or just its children (false). 2316 | * @param dy Delta scrolled in pixels 2317 | * @param x X coordinate of the active touch point 2318 | * @param y Y coordinate of the active touch point 2319 | * @return true if child views of v can be scrolled by delta of dx. 2320 | */ 2321 | protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) { 2322 | if (v instanceof ViewGroup group) { 2323 | final int scrollX = v.getScrollX(); 2324 | final int scrollY = v.getScrollY(); 2325 | final int count = group.getChildCount(); 2326 | // Count backwards - let topmost views consume scroll distance first. 2327 | for (int i = count - 1; i >= 0; i--) { 2328 | // TODO: Add versioned support here for transformed views. 2329 | // This will not work for transformed views in Honeycomb+ 2330 | final View child = group.getChildAt(i); 2331 | if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 2332 | x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 2333 | canScroll(child, true, dy, x + scrollX - child.getLeft(), 2334 | y + scrollY - child.getTop())) { 2335 | return true; 2336 | } 2337 | } 2338 | } 2339 | 2340 | return checkV && v.canScrollVertically(-dy); 2341 | } 2342 | 2343 | @Override 2344 | public boolean dispatchKeyEvent(KeyEvent event) { 2345 | // Let the focused view and/or our descendants get the key first 2346 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 2347 | } 2348 | 2349 | /** 2350 | * You can call this function yourself to have the scroll view perform 2351 | * scrolling from a key event, just as if the event had been dispatched to 2352 | * it by the view hierarchy. 2353 | * 2354 | * @param event The key event to execute. 2355 | * @return Return true if the event was handled, else false. 2356 | */ 2357 | public boolean executeKeyEvent(KeyEvent event) { 2358 | boolean handled = false; 2359 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 2360 | switch (event.getKeyCode()) { 2361 | case KeyEvent.KEYCODE_DPAD_LEFT -> handled = arrowScroll(FOCUS_LEFT); 2362 | case KeyEvent.KEYCODE_DPAD_RIGHT -> handled = arrowScroll(FOCUS_RIGHT); 2363 | case KeyEvent.KEYCODE_TAB -> { 2364 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD 2365 | // before Android 3.0. Ignore the tab key on those devices. 2366 | if (event.hasNoModifiers()) { 2367 | handled = arrowScroll(FOCUS_FORWARD); 2368 | } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 2369 | handled = arrowScroll(FOCUS_BACKWARD); 2370 | } 2371 | } 2372 | } 2373 | } 2374 | return handled; 2375 | } 2376 | 2377 | public boolean arrowScroll(int direction) { 2378 | View currentFocused = findFocus(); 2379 | if (currentFocused == this) { 2380 | currentFocused = null; 2381 | } else if (currentFocused != null) { 2382 | boolean isChild = false; 2383 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2384 | parent = parent.getParent()) { 2385 | if (parent == this) { 2386 | isChild = true; 2387 | break; 2388 | } 2389 | } 2390 | if (!isChild) { 2391 | // This would cause the focus search down below to fail in fun ways. 2392 | final StringBuilder sb = new StringBuilder(); 2393 | sb.append(currentFocused.getClass().getSimpleName()); 2394 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2395 | parent = parent.getParent()) { 2396 | sb.append(" => ").append(parent.getClass().getSimpleName()); 2397 | } 2398 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " + 2399 | "current focused view " + sb); 2400 | currentFocused = null; 2401 | } 2402 | } 2403 | 2404 | boolean handled = false; 2405 | 2406 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, 2407 | direction); 2408 | if (nextFocused != null && nextFocused != currentFocused) { 2409 | if (direction == View.FOCUS_UP) { 2410 | // If there is nothing to the left, or this is causing us to 2411 | // jump to the right, then what we really want to do is page left. 2412 | final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top; 2413 | final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top; 2414 | if (currentFocused != null && nextTop >= currTop) { 2415 | handled = pageUp(); 2416 | } else { 2417 | handled = nextFocused.requestFocus(); 2418 | } 2419 | } else if (direction == View.FOCUS_DOWN) { 2420 | // If there is nothing to the right, or this is causing us to 2421 | // jump to the left, then what we really want to do is page right. 2422 | final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom; 2423 | final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom; 2424 | if (currentFocused != null && nextDown <= currDown) { 2425 | handled = pageDown(); 2426 | } else { 2427 | handled = nextFocused.requestFocus(); 2428 | } 2429 | } 2430 | } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) { 2431 | // Trying to move left and nothing there; try to page. 2432 | handled = pageUp(); 2433 | } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) { 2434 | // Trying to move right and nothing there; try to page. 2435 | handled = pageDown(); 2436 | } 2437 | if (handled) { 2438 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2439 | } 2440 | return handled; 2441 | } 2442 | 2443 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { 2444 | if (outRect == null) { 2445 | outRect = new Rect(); 2446 | } 2447 | if (child == null) { 2448 | outRect.set(0, 0, 0, 0); 2449 | return outRect; 2450 | } 2451 | outRect.left = child.getLeft(); 2452 | outRect.right = child.getRight(); 2453 | outRect.top = child.getTop(); 2454 | outRect.bottom = child.getBottom(); 2455 | 2456 | ViewParent parent = child.getParent(); 2457 | while (parent instanceof ViewGroup group && parent != this) { 2458 | outRect.left += group.getLeft(); 2459 | outRect.right += group.getRight(); 2460 | outRect.top += group.getTop(); 2461 | outRect.bottom += group.getBottom(); 2462 | 2463 | parent = group.getParent(); 2464 | } 2465 | return outRect; 2466 | } 2467 | 2468 | boolean pageUp() { 2469 | if (mCurItem > 0) { 2470 | setCurrentItem(mCurItem - 1, true); 2471 | return true; 2472 | } 2473 | return false; 2474 | } 2475 | 2476 | boolean pageDown() { 2477 | if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) { 2478 | setCurrentItem(mCurItem + 1, true); 2479 | return true; 2480 | } 2481 | return false; 2482 | } 2483 | 2484 | /** 2485 | * We only want the current page that is being shown to be focusable. 2486 | */ 2487 | @Override 2488 | public void addFocusables(@NonNull ArrayList views, int direction, int focusableMode) { 2489 | final int focusableCount = views.size(); 2490 | 2491 | final int descendantFocusability = getDescendantFocusability(); 2492 | 2493 | if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { 2494 | for (int i = 0; i < getChildCount(); i++) { 2495 | final View child = getChildAt(i); 2496 | if (child.getVisibility() == VISIBLE) { 2497 | ItemInfo ii = infoForChild(child); 2498 | if (ii != null && ii.position == mCurItem) { 2499 | child.addFocusables(views, direction, focusableMode); 2500 | } 2501 | } 2502 | } 2503 | } 2504 | 2505 | // we add ourselves (if focusable) in all cases except for when we are 2506 | // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 2507 | // to avoid the focus search finding layouts when a more precise search 2508 | // among the focusable children would be more interesting. 2509 | if ( 2510 | descendantFocusability != FOCUS_AFTER_DESCENDANTS || 2511 | // No focusable descendants 2512 | (focusableCount == views.size())) { 2513 | // Note that we can't call the superclass here, because it will 2514 | // add all views in. So we need to do the same thing View does. 2515 | if (!isFocusable()) { 2516 | return; 2517 | } 2518 | if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && 2519 | isInTouchMode() && !isFocusableInTouchMode()) { 2520 | return; 2521 | } 2522 | views.add(this); 2523 | } 2524 | } 2525 | 2526 | /** 2527 | * We only want the current page that is being shown to be touchable. 2528 | */ 2529 | @Override 2530 | public void addTouchables(ArrayList views) { 2531 | // Note that we don't call super.addTouchables(), which means that 2532 | // we don't call View.addTouchables(). This is okay because a ViewPager 2533 | // is itself not touchable. 2534 | for (int i = 0; i < getChildCount(); i++) { 2535 | final View child = getChildAt(i); 2536 | if (child.getVisibility() == VISIBLE) { 2537 | ItemInfo ii = infoForChild(child); 2538 | if (ii != null && ii.position == mCurItem) { 2539 | child.addTouchables(views); 2540 | } 2541 | } 2542 | } 2543 | } 2544 | 2545 | /** 2546 | * We only want the current page that is being shown to be focusable. 2547 | */ 2548 | @Override 2549 | protected boolean onRequestFocusInDescendants(int direction, 2550 | Rect previouslyFocusedRect) { 2551 | int index; 2552 | int increment; 2553 | int end; 2554 | int count = getChildCount(); 2555 | if ((direction & FOCUS_FORWARD) != 0) { 2556 | index = 0; 2557 | increment = 1; 2558 | end = count; 2559 | } else { 2560 | index = count - 1; 2561 | increment = -1; 2562 | end = -1; 2563 | } 2564 | for (int i = index; i != end; i += increment) { 2565 | View child = getChildAt(i); 2566 | if (child.getVisibility() == VISIBLE) { 2567 | ItemInfo ii = infoForChild(child); 2568 | if (ii != null && ii.position == mCurItem) { 2569 | if (child.requestFocus(direction, previouslyFocusedRect)) { 2570 | return true; 2571 | } 2572 | } 2573 | } 2574 | } 2575 | return false; 2576 | } 2577 | 2578 | @Override 2579 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 2580 | // Dispatch scroll events from this ViewPager. 2581 | if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { 2582 | return super.dispatchPopulateAccessibilityEvent(event); 2583 | } 2584 | 2585 | // Dispatch all other accessibility events from the current page. 2586 | final int childCount = getChildCount(); 2587 | for (int i = 0; i < childCount; i++) { 2588 | final View child = getChildAt(i); 2589 | if (child.getVisibility() == VISIBLE) { 2590 | final ItemInfo ii = infoForChild(child); 2591 | if (ii != null && ii.position == mCurItem && 2592 | child.dispatchPopulateAccessibilityEvent(event)) { 2593 | return true; 2594 | } 2595 | } 2596 | } 2597 | 2598 | return false; 2599 | } 2600 | 2601 | @Override 2602 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 2603 | return new LayoutParams(); 2604 | } 2605 | 2606 | @Override 2607 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2608 | return generateDefaultLayoutParams(); 2609 | } 2610 | 2611 | @Override 2612 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2613 | return p instanceof LayoutParams && super.checkLayoutParams(p); 2614 | } 2615 | 2616 | @Override 2617 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2618 | return new LayoutParams(getContext(), attrs); 2619 | } 2620 | 2621 | class MyAccessibilityDelegate extends AccessibilityDelegateCompat { 2622 | 2623 | @Override 2624 | public void onInitializeAccessibilityEvent(@NonNull View host, @NonNull AccessibilityEvent event) { 2625 | super.onInitializeAccessibilityEvent(host, event); 2626 | event.setClassName(ViewPager.class.getName()); 2627 | final AccessibilityRecord record = AccessibilityRecord.obtain(); 2628 | record.setScrollable(canScroll()); 2629 | if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED 2630 | && mAdapter != null) { 2631 | record.setItemCount(mAdapter.getCount()); 2632 | record.setFromIndex(mCurItem); 2633 | record.setToIndex(mCurItem); 2634 | } 2635 | } 2636 | 2637 | @Override 2638 | public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { 2639 | super.onInitializeAccessibilityNodeInfo(host, info); 2640 | info.setClassName(ViewPager.class.getName()); 2641 | info.setScrollable(canScroll()); 2642 | if (internalCanScrollVertically(1)) { 2643 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2644 | } 2645 | if (internalCanScrollVertically(-1)) { 2646 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2647 | } 2648 | } 2649 | 2650 | @Override 2651 | public boolean performAccessibilityAction(@NonNull View host, int action, Bundle args) { 2652 | if (super.performAccessibilityAction(host, action, args)) { 2653 | return true; 2654 | } 2655 | switch (action) { 2656 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD -> { 2657 | { 2658 | if (internalCanScrollVertically(1)) { 2659 | setCurrentItem(mCurItem + 1); 2660 | return true; 2661 | } 2662 | } 2663 | return false; 2664 | } 2665 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD -> { 2666 | { 2667 | if (internalCanScrollVertically(-1)) { 2668 | setCurrentItem(mCurItem - 1); 2669 | return true; 2670 | } 2671 | } 2672 | return false; 2673 | } 2674 | } 2675 | return false; 2676 | } 2677 | 2678 | private boolean canScroll() { 2679 | return (mAdapter != null) && (mAdapter.getCount() > 1); 2680 | } 2681 | } 2682 | 2683 | private class PagerObserver extends DataSetObserver { 2684 | @Override 2685 | public void onChanged() { 2686 | dataSetChanged(); 2687 | } 2688 | 2689 | @Override 2690 | public void onInvalidated() { 2691 | dataSetChanged(); 2692 | } 2693 | } 2694 | 2695 | /** 2696 | * Layout parameters that should be supplied for views added to a 2697 | * ViewPager. 2698 | */ 2699 | public static class LayoutParams extends ViewGroup.LayoutParams { 2700 | /** 2701 | * true if this view is a decoration on the pager itself and not 2702 | * a view supplied by the adapter. 2703 | */ 2704 | public boolean isDecor; 2705 | 2706 | /** 2707 | * Gravity setting for use on decor views only: 2708 | * Where to position the view page within the overall ViewPager 2709 | * container; constants are defined in {@link android.view.Gravity}. 2710 | */ 2711 | public int gravity; 2712 | 2713 | /** 2714 | * Width as a 0-1 multiplier of the measured pager width 2715 | */ 2716 | float heightFactor = 0.f; 2717 | 2718 | /** 2719 | * true if this view was added during layout and needs to be measured 2720 | * before being positioned. 2721 | */ 2722 | boolean needsMeasure; 2723 | 2724 | /** 2725 | * Adapter position this view is for if !isDecor 2726 | */ 2727 | int position; 2728 | 2729 | /** 2730 | * Current child index within the ViewPager that this view occupies 2731 | */ 2732 | int childIndex; 2733 | 2734 | public LayoutParams() { 2735 | super(MATCH_PARENT, MATCH_PARENT); 2736 | } 2737 | 2738 | public LayoutParams(Context context, AttributeSet attrs) { 2739 | super(context, attrs); 2740 | 2741 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2742 | gravity = a.getInteger(0, Gravity.TOP); 2743 | a.recycle(); 2744 | } 2745 | } 2746 | 2747 | static class ViewPositionComparator implements Comparator { 2748 | @Override 2749 | public int compare(View lhs, View rhs) { 2750 | final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); 2751 | final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); 2752 | if (llp.isDecor != rlp.isDecor) { 2753 | return llp.isDecor ? 1 : -1; 2754 | } 2755 | return llp.position - rlp.position; 2756 | } 2757 | } 2758 | } 2759 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/drawable-xhdpi/cloudy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/rainy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/drawable-xhdpi/rainy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/snowy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/drawable-xhdpi/snowy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/sunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/drawable-xhdpi/sunny.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/day_cell_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lineframe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 |