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