1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.wallpaper.widget;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.content.res.TypedArray;
21 import android.database.DataSetObserver;
22 import android.graphics.Point;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.WindowManager;
28 import android.view.animation.Interpolator;
29 import android.widget.LinearLayout;
30 import android.widget.Scroller;
31 
32 import androidx.annotation.Nullable;
33 import androidx.core.text.TextUtilsCompat;
34 import androidx.core.view.ViewCompat;
35 import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
36 import androidx.viewpager.widget.PagerAdapter;
37 import androidx.viewpager.widget.ViewPager;
38 import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
39 
40 import com.android.wallpaper.R;
41 import com.android.wallpaper.util.ScreenSizeCalculator;
42 
43 import java.lang.reflect.Field;
44 import java.util.Locale;
45 
46 /**
47  * A Widget consisting of a ViewPager linked to a PageIndicator and previous/next arrows that can be
48  * used to page over that ViewPager.
49  * To use it, set a {@link PagerAdapter} using {@link #setAdapter(PagerAdapter)}, and optionally use
50  * a {@link #setOnPageChangeListener(OnPageChangeListener)} to listen for page changes.
51  */
52 public class PreviewPager extends LinearLayout {
53 
54     private static final String TAG = "PreviewPager";
55     private static final int STYLE_PEEKING = 0;
56     private static final int STYLE_ASPECT_RATIO = 1;
57 
58     private final ViewPager mViewPager;
59     private final PageIndicator mPageIndicator;
60     private final View mPreviousArrow;
61     private final View mNextArrow;
62     private final ViewPager.OnPageChangeListener mPageListener;
63     private int mPageStyle;
64 
65     private PagerAdapter mAdapter;
66     private ViewPager.OnPageChangeListener mExternalPageListener;
67     private float mScreenAspectRatio;
68 
PreviewPager(Context context)69     public PreviewPager(Context context) {
70         this(context, null);
71     }
72 
PreviewPager(Context context, AttributeSet attrs)73     public PreviewPager(Context context, AttributeSet attrs) {
74         this(context, attrs, 0);
75     }
76 
PreviewPager(Context context, AttributeSet attrs, int defStyleAttr)77     public PreviewPager(Context context, AttributeSet attrs, int defStyleAttr) {
78         super(context, attrs, defStyleAttr);
79         LayoutInflater.from(context).inflate(R.layout.preview_pager, this);
80         Resources res = context.getResources();
81         TypedArray a = context.obtainStyledAttributes(attrs,
82                 R.styleable.PreviewPager, defStyleAttr, 0);
83 
84         mPageStyle = a.getInteger(R.styleable.PreviewPager_card_style, STYLE_PEEKING);
85 
86         a.recycle();
87 
88         mViewPager = findViewById(R.id.preview_viewpager);
89         mViewPager.setPageTransformer(false, (view, position) -> {
90             int origin = mViewPager.getPaddingStart();
91             int leftBoundary = -view.getWidth();
92             int rightBoundary = mViewPager.getWidth();
93             int pageWidth = view.getWidth();
94             int offset = (int) (pageWidth * position);
95 
96             //               left      origin     right
97             //             boundary              boundary
98             // ---------------|----------|----------|----------
99             // Cover alpha:  1.0         0         1.0
100             float alpha;
101             if (offset <= leftBoundary || offset >= rightBoundary) {
102                 alpha = 1.0f;
103             } else if (offset <= origin) {
104                 // offset in (leftBoundary, origin]
105                 alpha = (float) Math.abs(offset - origin) / Math.abs(leftBoundary - origin);
106             } else {
107                 // offset in (origin, rightBoundary)
108                 alpha = (float) Math.abs(offset - origin) / Math.abs(rightBoundary - origin);
109             }
110         }, LAYER_TYPE_NONE);
111         mViewPager.setPageMargin(res.getDimensionPixelOffset(R.dimen.preview_page_gap));
112         mViewPager.setClipToPadding(false);
113         if (mPageStyle == STYLE_PEEKING) {
114             int screenWidth = mViewPager.getResources().getDisplayMetrics().widthPixels;
115             int hMargin = res.getDimensionPixelOffset(R.dimen.preview_page_horizontal_margin);
116             hMargin = Math.max(hMargin, screenWidth/8);
117             mViewPager.setPadding(
118                     hMargin,
119                     res.getDimensionPixelOffset(R.dimen.preview_page_top_margin),
120                     hMargin,
121                     res.getDimensionPixelOffset(R.dimen.preview_page_bottom_margin));
122         } else if (mPageStyle == STYLE_ASPECT_RATIO) {
123             WindowManager windowManager = context.getSystemService(WindowManager.class);
124             Point screenSize = ScreenSizeCalculator.getInstance()
125                     .getScreenSize(windowManager.getDefaultDisplay());
126             mScreenAspectRatio = (float) screenSize.y / screenSize.x;
127             mViewPager.setPadding(
128                     0,
129                     res.getDimensionPixelOffset(R.dimen.preview_page_top_margin),
130                     0,
131                     res.getDimensionPixelOffset(R.dimen.preview_page_bottom_margin));
132             // Set the default margin to make sure not peeking the second page before calculating
133             // the real margin.
134             mViewPager.setPageMargin(screenSize.x / 2);
135             mViewPager.addOnLayoutChangeListener(new OnLayoutChangeListener() {
136                 @Override
137                 public void onLayoutChange(View view, int left, int top, int right, int bottom,
138                                            int oldLeft, int oldTop, int oldRight, int oldBottom) {
139                     // Set the minimum margin which can't peek the second page.
140                     mViewPager.setPageMargin(view.getPaddingEnd());
141                     mViewPager.removeOnLayoutChangeListener(this);
142                 }
143             });
144         }
145         setupPagerScroller(context);
146         mPageIndicator = findViewById(R.id.page_indicator);
147         mPreviousArrow = findViewById(R.id.arrow_previous);
148         mPreviousArrow.setOnClickListener(v -> {
149             final int previousPos = mViewPager.getCurrentItem() - 1;
150             mViewPager.setCurrentItem(previousPos, true);
151         });
152         mNextArrow = findViewById(R.id.arrow_next);
153         mNextArrow.setOnClickListener(v -> {
154             final int NextPos = mViewPager.getCurrentItem() + 1;
155             mViewPager.setCurrentItem(NextPos, true);
156         });
157         mPageListener = createPageListener();
158         mViewPager.addOnPageChangeListener(mPageListener);
159     }
160 
161     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)162     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
163         if (mPageStyle == STYLE_ASPECT_RATIO) {
164             int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
165             int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
166             int indicatorHeight = ((View) mPageIndicator.getParent()).getLayoutParams().height;
167             int pagerHeight = availableHeight - indicatorHeight;
168             if (availableWidth > 0) {
169                 int absoluteCardWidth = (int) ((pagerHeight - mViewPager.getPaddingBottom()
170                         - mViewPager.getPaddingTop())/ mScreenAspectRatio);
171                 int hPadding = (availableWidth / 2) - (absoluteCardWidth / 2);
172                 mViewPager.setPaddingRelative(
173                         hPadding,
174                         mViewPager.getPaddingTop(),
175                         hPadding,
176                         mViewPager.getPaddingBottom());
177             }
178         }
179 
180         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
181     }
182 
forceCardWidth(int widthPixels)183     public void forceCardWidth(int widthPixels) {
184         mViewPager.addOnLayoutChangeListener(new OnLayoutChangeListener() {
185             @Override
186             public void onLayoutChange(View v, int left, int top, int right, int bottom,
187                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
188                 int hPadding = (mViewPager.getWidth() - widthPixels) / 2;
189                 mViewPager.setPadding(hPadding, mViewPager.getPaddingTop(),
190                         hPadding, mViewPager.getPaddingBottom());
191                 mViewPager.removeOnLayoutChangeListener(this);
192             }
193         });
194         mViewPager.invalidate();
195     }
196 
197     /**
198      * Call this method to set the {@link PagerAdapter} backing the {@link ViewPager} in this
199      * widget.
200      */
setAdapter(@ullable PagerAdapter adapter)201     public void setAdapter(@Nullable PagerAdapter adapter) {
202         if (adapter == null) {
203             mAdapter = null;
204             mViewPager.setAdapter(null);
205             return;
206         }
207         int initialPage = 0;
208         if (mViewPager.getAdapter() != null) {
209             initialPage = isRtl() ? mAdapter.getCount() - 1 - mViewPager.getCurrentItem()
210                     : mViewPager.getCurrentItem();
211         }
212         mAdapter = adapter;
213         mViewPager.setAdapter(adapter);
214         mViewPager.setCurrentItem(isRtl() ? mAdapter.getCount() - 1 - initialPage : initialPage);
215         mAdapter.registerDataSetObserver(new DataSetObserver() {
216             @Override
217             public void onChanged() {
218                 initIndicator();
219             }
220         });
221         initIndicator();
222         updateIndicator(mViewPager.getCurrentItem());
223     }
224 
225     /**
226      * Checks if it is in RTL mode.
227      *
228      * @return {@code true} if it's in RTL mode; {@code false} otherwise.
229      */
isRtl()230     public boolean isRtl() {
231         if (ViewCompat.isLayoutDirectionResolved(mViewPager)) {
232             return ViewCompat.getLayoutDirection(mViewPager) == ViewCompat.LAYOUT_DIRECTION_RTL;
233         }
234         return TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
235                 == ViewCompat.LAYOUT_DIRECTION_RTL;
236     }
237 
238     /**
239      * Set a {@link OnPageChangeListener} to be notified when the ViewPager's page state changes
240      */
setOnPageChangeListener(@ullable ViewPager.OnPageChangeListener listener)241     public void setOnPageChangeListener(@Nullable ViewPager.OnPageChangeListener listener) {
242         mExternalPageListener = listener;
243     }
244 
245     /**
246      * Switches to the specific preview page.
247      *
248      * @param index preview page index to select
249      */
switchPreviewPage(int index)250     public void switchPreviewPage(int index) {
251         mViewPager.setCurrentItem(index);
252     }
253 
initIndicator()254     private void initIndicator() {
255         mPageIndicator.setNumPages(mAdapter.getCount());
256         mPageIndicator.setLocation(mViewPager.getCurrentItem());
257     }
258 
setupPagerScroller(Context context)259     private void setupPagerScroller(Context context) {
260         try {
261             // TODO(b/159082165): Revisit if we can refactor it better.
262             Field scroller = ViewPager.class.getDeclaredField("mScroller");
263             scroller.setAccessible(true);
264             PreviewPagerScroller previewPagerScroller =
265                     new PreviewPagerScroller(context, new LinearOutSlowInInterpolator());
266             scroller.set(mViewPager, previewPagerScroller);
267         } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
268             Log.e(TAG, "Failed to setup pager scroller.", e);
269         }
270     }
271 
createPageListener()272     private ViewPager.OnPageChangeListener createPageListener() {
273         return new ViewPager.OnPageChangeListener() {
274              @Override
275              public void onPageScrolled(
276                      int position, float positionOffset, int positionOffsetPixels) {
277                  // For certain sizes, positionOffset never makes it to 1, so round it as we don't
278                  // need that much precision
279                  float location = (float) Math.round((position + positionOffset) * 100) / 100;
280                  mPageIndicator.setLocation(location);
281                  if (mExternalPageListener != null) {
282                      mExternalPageListener.onPageScrolled(position, positionOffset,
283                              positionOffsetPixels);
284                  }
285              }
286 
287              @Override
288              public void onPageSelected(int position) {
289                  int adapterCount = mAdapter.getCount();
290                  if (position < 0 || position >= adapterCount) {
291                      return;
292                  }
293 
294                  updateIndicator(position);
295                  if (mExternalPageListener != null) {
296                      mExternalPageListener.onPageSelected(position);
297                  }
298              }
299 
300              @Override
301              public void onPageScrollStateChanged(int state) {
302                  if (mExternalPageListener != null) {
303                      mExternalPageListener.onPageScrollStateChanged(state);
304                  }
305              }
306         };
307     }
308 
309     private void updateIndicator(int position) {
310         int adapterCount = mAdapter.getCount();
311         if (adapterCount > 1) {
312             mPreviousArrow.setVisibility(position != 0 ? View.VISIBLE : View.GONE);
313             mNextArrow.setVisibility(position != (adapterCount - 1) ? View.VISIBLE : View.GONE);
314         } else {
315             mPageIndicator.setVisibility(View.GONE);
316             mPreviousArrow.setVisibility(View.GONE);
317             mNextArrow.setVisibility(View.GONE);
318         }
319     }
320 
321     private static class PreviewPagerScroller extends Scroller {
322 
323         private static final int DURATION_MS = 500;
324 
325         PreviewPagerScroller(Context context, Interpolator interpolator) {
326             super(context, interpolator);
327         }
328 
329         @Override
330         public void startScroll(int startX, int startY, int dx, int dy, int duration) {
331             super.startScroll(startX, startY, dx, dy, DURATION_MS);
332         }
333     }
334 }
335