blob: 5fd92e567975dad45a1579a3b3fa1f77ebf3cddc [file] [log] [blame]
/*
* 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.util.TypedValue;
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;
/** The maximum height ratio of PreviewPager and its parent view. */
private float mMaxHeightRatio;
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();
TypedValue ratioValue = new TypedValue();
res.getValue(R.dimen.preview_pager_maximum_height_ratio, ratioValue, true);
mMaxHeightRatio = ratioValue.getFloat();
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);
}
View cover = view.findViewById(R.id.fade_cover);
if (cover != null) {
view.findViewById(R.id.fade_cover).setAlpha(alpha);
}
}, 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) {
View parentView = (View) getParent();
float parentHeight = parentView != null ? parentView.getMeasuredHeight() : 0;
if (parentHeight > 0) {
int maxHeight = Math.min(MeasureSpec.getSize(heightMeasureSpec),
(int) (parentHeight * mMaxHeightRatio));
heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
}
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);
}
}
}