/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.widget; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Point; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.view.animation.Interpolator; import android.widget.LinearLayout; import android.widget.Scroller; import androidx.annotation.Nullable; import androidx.core.text.TextUtilsCompat; import androidx.core.view.ViewCompat; import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager.OnPageChangeListener; import com.android.wallpaper.R; import com.android.wallpaper.util.ScreenSizeCalculator; import java.lang.reflect.Field; import java.util.Locale; /** * A Widget consisting of a ViewPager linked to a PageIndicator and previous/next arrows that can be * used to page over that ViewPager. * To use it, set a {@link PagerAdapter} using {@link #setAdapter(PagerAdapter)}, and optionally use * a {@link #setOnPageChangeListener(OnPageChangeListener)} to listen for page changes. */ public class PreviewPager extends LinearLayout { private static final String TAG = "PreviewPager"; private static final int STYLE_PEEKING = 0; private static final int STYLE_ASPECT_RATIO = 1; private final ViewPager mViewPager; private final PageIndicator mPageIndicator; private final View mPreviousArrow; private final View mNextArrow; private final ViewPager.OnPageChangeListener mPageListener; private int mPageStyle; private PagerAdapter mAdapter; private ViewPager.OnPageChangeListener mExternalPageListener; private float mScreenAspectRatio; public PreviewPager(Context context) { this(context, null); } public PreviewPager(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PreviewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LayoutInflater.from(context).inflate(R.layout.preview_pager, this); Resources res = context.getResources(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PreviewPager, defStyleAttr, 0); mPageStyle = a.getInteger(R.styleable.PreviewPager_card_style, STYLE_PEEKING); a.recycle(); mViewPager = findViewById(R.id.preview_viewpager); mViewPager.setPageTransformer(false, (view, position) -> { int origin = mViewPager.getPaddingStart(); int leftBoundary = -view.getWidth(); int rightBoundary = mViewPager.getWidth(); int pageWidth = view.getWidth(); int offset = (int) (pageWidth * position); // left origin right // boundary boundary // ---------------|----------|----------|---------- // Cover alpha: 1.0 0 1.0 float alpha; if (offset <= leftBoundary || offset >= rightBoundary) { alpha = 1.0f; } else if (offset <= origin) { // offset in (leftBoundary, origin] alpha = (float) Math.abs(offset - origin) / Math.abs(leftBoundary - origin); } else { // offset in (origin, rightBoundary) alpha = (float) Math.abs(offset - origin) / Math.abs(rightBoundary - origin); } }, LAYER_TYPE_NONE); mViewPager.setPageMargin(res.getDimensionPixelOffset(R.dimen.preview_page_gap)); mViewPager.setClipToPadding(false); if (mPageStyle == STYLE_PEEKING) { int screenWidth = mViewPager.getResources().getDisplayMetrics().widthPixels; int hMargin = res.getDimensionPixelOffset(R.dimen.preview_page_horizontal_margin); hMargin = Math.max(hMargin, screenWidth/8); mViewPager.setPadding( hMargin, res.getDimensionPixelOffset(R.dimen.preview_page_top_margin), hMargin, res.getDimensionPixelOffset(R.dimen.preview_page_bottom_margin)); } else if (mPageStyle == STYLE_ASPECT_RATIO) { WindowManager windowManager = context.getSystemService(WindowManager.class); Point screenSize = ScreenSizeCalculator.getInstance() .getScreenSize(windowManager.getDefaultDisplay()); mScreenAspectRatio = (float) screenSize.y / screenSize.x; mViewPager.setPadding( 0, res.getDimensionPixelOffset(R.dimen.preview_page_top_margin), 0, res.getDimensionPixelOffset(R.dimen.preview_page_bottom_margin)); // Set the default margin to make sure not peeking the second page before calculating // the real margin. mViewPager.setPageMargin(screenSize.x / 2); mViewPager.addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { // Set the minimum margin which can't peek the second page. mViewPager.setPageMargin(view.getPaddingEnd()); mViewPager.removeOnLayoutChangeListener(this); } }); } setupPagerScroller(context); mPageIndicator = findViewById(R.id.page_indicator); mPreviousArrow = findViewById(R.id.arrow_previous); mPreviousArrow.setOnClickListener(v -> { final int previousPos = mViewPager.getCurrentItem() - 1; mViewPager.setCurrentItem(previousPos, true); }); mNextArrow = findViewById(R.id.arrow_next); mNextArrow.setOnClickListener(v -> { final int NextPos = mViewPager.getCurrentItem() + 1; mViewPager.setCurrentItem(NextPos, true); }); mPageListener = createPageListener(); mViewPager.addOnPageChangeListener(mPageListener); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mPageStyle == STYLE_ASPECT_RATIO) { int availableWidth = MeasureSpec.getSize(widthMeasureSpec); int availableHeight = MeasureSpec.getSize(heightMeasureSpec); int indicatorHeight = ((View) mPageIndicator.getParent()).getLayoutParams().height; int pagerHeight = availableHeight - indicatorHeight; if (availableWidth > 0) { int absoluteCardWidth = (int) ((pagerHeight - mViewPager.getPaddingBottom() - mViewPager.getPaddingTop())/ mScreenAspectRatio); int hPadding = (availableWidth / 2) - (absoluteCardWidth / 2); mViewPager.setPaddingRelative( hPadding, mViewPager.getPaddingTop(), hPadding, mViewPager.getPaddingBottom()); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } public void forceCardWidth(int widthPixels) { mViewPager.addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int hPadding = (mViewPager.getWidth() - widthPixels) / 2; mViewPager.setPadding(hPadding, mViewPager.getPaddingTop(), hPadding, mViewPager.getPaddingBottom()); mViewPager.removeOnLayoutChangeListener(this); } }); mViewPager.invalidate(); } /** * Call this method to set the {@link PagerAdapter} backing the {@link ViewPager} in this * widget. */ public void setAdapter(@Nullable PagerAdapter adapter) { if (adapter == null) { mAdapter = null; mViewPager.setAdapter(null); return; } int initialPage = 0; if (mViewPager.getAdapter() != null) { initialPage = isRtl() ? mAdapter.getCount() - 1 - mViewPager.getCurrentItem() : mViewPager.getCurrentItem(); } mAdapter = adapter; mViewPager.setAdapter(adapter); mViewPager.setCurrentItem(isRtl() ? mAdapter.getCount() - 1 - initialPage : initialPage); mAdapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { initIndicator(); } }); initIndicator(); updateIndicator(mViewPager.getCurrentItem()); } /** * Checks if it is in RTL mode. * * @return {@code true} if it's in RTL mode; {@code false} otherwise. */ public boolean isRtl() { if (ViewCompat.isLayoutDirectionResolved(mViewPager)) { return ViewCompat.getLayoutDirection(mViewPager) == ViewCompat.LAYOUT_DIRECTION_RTL; } return TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL; } /** * Set a {@link OnPageChangeListener} to be notified when the ViewPager's page state changes */ public void setOnPageChangeListener(@Nullable ViewPager.OnPageChangeListener listener) { mExternalPageListener = listener; } /** * Switches to the specific preview page. * * @param index preview page index to select */ public void switchPreviewPage(int index) { mViewPager.setCurrentItem(index); } private void initIndicator() { mPageIndicator.setNumPages(mAdapter.getCount()); mPageIndicator.setLocation(mViewPager.getCurrentItem()); } private void setupPagerScroller(Context context) { try { // TODO(b/159082165): Revisit if we can refactor it better. Field scroller = ViewPager.class.getDeclaredField("mScroller"); scroller.setAccessible(true); PreviewPagerScroller previewPagerScroller = new PreviewPagerScroller(context, new LinearOutSlowInInterpolator()); scroller.set(mViewPager, previewPagerScroller); } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { Log.e(TAG, "Failed to setup pager scroller.", e); } } private ViewPager.OnPageChangeListener createPageListener() { return new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled( int position, float positionOffset, int positionOffsetPixels) { // For certain sizes, positionOffset never makes it to 1, so round it as we don't // need that much precision float location = (float) Math.round((position + positionOffset) * 100) / 100; mPageIndicator.setLocation(location); if (mExternalPageListener != null) { mExternalPageListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageSelected(int position) { int adapterCount = mAdapter.getCount(); if (position < 0 || position >= adapterCount) { return; } updateIndicator(position); if (mExternalPageListener != null) { mExternalPageListener.onPageSelected(position); } } @Override public void onPageScrollStateChanged(int state) { if (mExternalPageListener != null) { mExternalPageListener.onPageScrollStateChanged(state); } } }; } private void updateIndicator(int position) { int adapterCount = mAdapter.getCount(); if (adapterCount > 1) { mPreviousArrow.setVisibility(position != 0 ? View.VISIBLE : View.GONE); mNextArrow.setVisibility(position != (adapterCount - 1) ? View.VISIBLE : View.GONE); } else { mPageIndicator.setVisibility(View.GONE); mPreviousArrow.setVisibility(View.GONE); mNextArrow.setVisibility(View.GONE); } } private static class PreviewPagerScroller extends Scroller { private static final int DURATION_MS = 500; PreviewPagerScroller(Context context, Interpolator interpolator) { super(context, interpolator); } @Override public void startScroll(int startX, int startY, int dx, int dy, int duration) { super.startScroll(startX, startY, dx, dy, DURATION_MS); } } }