├── .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 | [](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 | 
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 |
24 |
25 |
29 |
30 |
38 |
39 |
47 |
48 |
56 |
57 |
58 |
63 |
64 |
70 |
71 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_monthly.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/oneday.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
26 |
27 |
35 |
36 |
37 |
38 |
45 |
46 |
53 |
54 |
59 |
60 |
61 |
62 |
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/calendar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #00000000
5 | #780000FF
6 | #32FFFFFF
7 | #16000000
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #D9B569
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Calendar Proto
5 | Hello world!
6 | Settings
7 | Weather icon
8 | Event icon
9 | 일정추가
10 | 일간
11 | 주간
12 | 월간
13 | Hello blank fragment
14 | 월
15 | Vertical View Pager
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:8.1.2'
9 | }
10 | }
11 |
12 | allprojects {
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.enableJetifier=false
2 | android.useAndroidX=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 27 09:40:54 KST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/proto.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brownsoo/Android-Vertically-Scrollable-Calendar-Prototype/5374e7d53a8a606385c8cf664cc455e9fd30e864/proto.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------