Profiles
This commit is contained in:
@@ -5,7 +5,6 @@ import android.app.Fragment;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class CoverOverlayGradientDrawable extends Drawable{
|
||||
private LinearGradient gradient=new LinearGradient(0f, 0f, 0f, 100f, 0xB0000000, 0, Shader.TileMode.CLAMP);
|
||||
private Matrix gradientMatrix=new Matrix();
|
||||
private int topPadding, topOffset;
|
||||
private Paint paint=new Paint();
|
||||
|
||||
public CoverOverlayGradientDrawable(){
|
||||
paint.setShader(gradient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
gradientMatrix.setScale(1f, (bounds.height()-V.dp(40)-topPadding)/100f);
|
||||
gradientMatrix.postTranslate(0, topPadding+topOffset);
|
||||
gradient.setLocalMatrix(gradientMatrix);
|
||||
canvas.drawRect(bounds, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
public void setTopPadding(int topPadding){
|
||||
this.topPadding=topPadding;
|
||||
}
|
||||
|
||||
public void setTopOffset(int topOffset){
|
||||
this.topOffset=topOffset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 org.joinmastodon.android.ui.tabs;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.lerp;
|
||||
|
||||
/**
|
||||
* An implementation of {@link TabIndicatorInterpolator} that translates the left and right sides of
|
||||
* a selected tab indicator independently to make the indicator grow and shrink between
|
||||
* destinations.
|
||||
*/
|
||||
class ElasticTabIndicatorInterpolator extends TabIndicatorInterpolator {
|
||||
|
||||
/** Fit a linear 0F - 1F curve to an ease out sine (decelerating) curve. */
|
||||
private static float decInterp(@FloatRange(from = 0.0, to = 1.0) float fraction) {
|
||||
// Ease out sine
|
||||
return (float) Math.sin((fraction * Math.PI) / 2.0);
|
||||
}
|
||||
|
||||
/** Fit a linear 0F - 1F curve to an ease in sine (accelerating) curve. */
|
||||
private static float accInterp(@FloatRange(from = 0.0, to = 1.0) float fraction) {
|
||||
// Ease in sine
|
||||
return (float) (1.0 - Math.cos((fraction * Math.PI) / 2.0));
|
||||
}
|
||||
|
||||
@Override
|
||||
void setIndicatorBoundsForOffset(
|
||||
TabLayout tabLayout,
|
||||
View startTitle,
|
||||
View endTitle,
|
||||
float offset,
|
||||
@NonNull Drawable indicator) {
|
||||
// The indicator should be positioned somewhere between start and end title. Override the
|
||||
// super implementation and adjust the indicator's left and right bounds independently.
|
||||
RectF startIndicator = calculateIndicatorWidthForTab(tabLayout, startTitle);
|
||||
RectF endIndicator = calculateIndicatorWidthForTab(tabLayout, endTitle);
|
||||
|
||||
float leftFraction;
|
||||
float rightFraction;
|
||||
|
||||
final boolean isMovingRight = startIndicator.left < endIndicator.left;
|
||||
// If the selection indicator should grow and shrink during the animation, interpolate
|
||||
// the left and right bounds of the indicator using separate easing functions.
|
||||
// The side in which the indicator is moving should always be the accelerating
|
||||
// side.
|
||||
if (isMovingRight) {
|
||||
leftFraction = accInterp(offset);
|
||||
rightFraction = decInterp(offset);
|
||||
} else {
|
||||
leftFraction = decInterp(offset);
|
||||
rightFraction = accInterp(offset);
|
||||
}
|
||||
indicator.setBounds(
|
||||
lerp((int) startIndicator.left, (int) endIndicator.left, leftFraction),
|
||||
indicator.getBounds().top,
|
||||
lerp((int) startIndicator.right, (int) endIndicator.right, rightFraction),
|
||||
indicator.getBounds().bottom);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.ui.tabs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
class MaterialResources{
|
||||
public static Drawable getDrawable(Context context, TypedArray a, int attr){
|
||||
return a.getDrawable(attr);
|
||||
}
|
||||
|
||||
public static ColorStateList getColorStateList(Context context, TypedArray a, int attr){
|
||||
return a.getColorStateList(attr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 org.joinmastodon.android.ui.tabs;
|
||||
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.lerp;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import androidx.annotation.Dimension;
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
/**
|
||||
* A class used to manipulate the {@link SlidingTabIndicator}'s indicator {@link Drawable} at any
|
||||
* point at or between tabs.
|
||||
*
|
||||
* <p>By default, this class will size the indicator according to {@link
|
||||
* TabLayout#isTabIndicatorFullWidth()} and linearly move the indicator between tabs.
|
||||
*
|
||||
* <p>Subclasses can override {@link #setIndicatorBoundsForTab(TabLayout, View, Drawable)} and
|
||||
* {@link #setIndicatorBoundsForOffset(TabLayout, View, View, float, Drawable)} (TabLayout, View,
|
||||
* View, float, Drawable)} to define how the indicator should be drawn for a single tab or at any
|
||||
* point between two tabs.
|
||||
*
|
||||
* <p>Additionally, subclasses can use the provided helpers {@link
|
||||
* #calculateIndicatorWidthForTab(TabLayout, View)} and {@link
|
||||
* #calculateTabViewContentBounds(TabView, int)} to capture the bounds of the tab or tab's content.
|
||||
*/
|
||||
class TabIndicatorInterpolator {
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private static final int MIN_INDICATOR_WIDTH = 24;
|
||||
|
||||
/**
|
||||
* A helper method that calculates the bounds of a {@link TabView}'s content.
|
||||
*
|
||||
* <p>For width, if only text label is present, calculates the width of the text label. If only
|
||||
* icon is present, calculates the width of the icon. If both are present, the text label bounds
|
||||
* take precedence. If both are present and inline mode is enabled, the sum of the bounds of the
|
||||
* both the text label and icon are calculated. If neither are present or if the calculated
|
||||
* difference between the left and right bounds is less than 24dp, then left and right bounds are
|
||||
* adjusted such that the difference between them is equal to 24dp.
|
||||
*
|
||||
* <p>For height, this method calculates the combined height of the icon (if present) and label
|
||||
* (if present).
|
||||
*
|
||||
* @param tabView {@link TabView} for which to calculate left and right content bounds.
|
||||
* @param minWidth the min width between the returned RectF's left and right bounds. Useful if
|
||||
* enforcing a min width of the indicator.
|
||||
*/
|
||||
static RectF calculateTabViewContentBounds(
|
||||
@NonNull TabLayout.TabView tabView, @Dimension(unit = Dimension.DP) int minWidth) {
|
||||
int tabViewContentWidth = tabView.getContentWidth();
|
||||
int tabViewContentHeight = tabView.getContentHeight();
|
||||
int minWidthPx = (int) V.dp(minWidth);
|
||||
|
||||
if (tabViewContentWidth < minWidthPx) {
|
||||
tabViewContentWidth = minWidthPx;
|
||||
}
|
||||
|
||||
int tabViewCenterX = (tabView.getLeft() + tabView.getRight()) / 2;
|
||||
int tabViewCenterY = (tabView.getTop() + tabView.getBottom()) / 2;
|
||||
int contentLeftBounds = tabViewCenterX - (tabViewContentWidth / 2);
|
||||
int contentTopBounds = tabViewCenterY - (tabViewContentHeight / 2);
|
||||
int contentRightBounds = tabViewCenterX + (tabViewContentWidth / 2);
|
||||
int contentBottomBounds = tabViewCenterY + (tabViewCenterX / 2);
|
||||
|
||||
return new RectF(contentLeftBounds, contentTopBounds, contentRightBounds, contentBottomBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to calculate the left and right bounds of an indicator when {@code tab} is
|
||||
* selected.
|
||||
*
|
||||
* <p>This method accounts for {@link TabLayout#isTabIndicatorFullWidth()}'s value. If true, the
|
||||
* returned left and right bounds will span the full width of {@code tab}. If false, the returned
|
||||
* bounds will span the width of the {@code tab}'s content.
|
||||
*
|
||||
* @param tabLayout The tab's parent {@link TabLayout}
|
||||
* @param tab The view of the tab under which the indicator will be positioned
|
||||
* @return A {@link RectF} containing the left and right bounds that the indicator should span
|
||||
* when {@code tab} is selected.
|
||||
*/
|
||||
static RectF calculateIndicatorWidthForTab(TabLayout tabLayout, @Nullable View tab) {
|
||||
if (tab == null) {
|
||||
return new RectF();
|
||||
}
|
||||
|
||||
// If the indicator should fit to the tab's content, calculate the content's widtd
|
||||
if (!tabLayout.isTabIndicatorFullWidth() && tab instanceof TabLayout.TabView) {
|
||||
return calculateTabViewContentBounds((TabLayout.TabView) tab, MIN_INDICATOR_WIDTH);
|
||||
}
|
||||
|
||||
// Return the entire width of the tab
|
||||
return new RectF(tab.getLeft(), tab.getTop(), tab.getRight(), tab.getBottom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever {@code indicator} should be drawn to show the given {@code tab} as selected.
|
||||
*
|
||||
* <p>This method should update the bounds of indicator to be correctly positioned to indicate
|
||||
* {@code tab} as selected.
|
||||
*
|
||||
* @param tabLayout The {@link TabLayout} parent of the tab and indicator being drawn.
|
||||
* @param tab The tab that should be marked as selected
|
||||
* @param indicator The drawable to be drawn to indicate the selected tab. Update the drawable's
|
||||
* bounds, color, etc to mark the given tab as selected.
|
||||
*/
|
||||
void setIndicatorBoundsForTab(TabLayout tabLayout, View tab, @NonNull Drawable indicator) {
|
||||
RectF startIndicator = calculateIndicatorWidthForTab(tabLayout, tab);
|
||||
indicator.setBounds(
|
||||
(int) startIndicator.left,
|
||||
indicator.getBounds().top,
|
||||
(int) startIndicator.right,
|
||||
indicator.getBounds().bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the {@code indicator} should be drawn between two destinations and the {@link
|
||||
* Drawable}'s bounds should be changed. When {@code offset} is 0.0, the tab {@code indicator}
|
||||
* should indicate that the {@code startTitle} tab is selected. When {@code offset} is 1.0, the
|
||||
* tab {@code indicator} should indicate that the {@code endTitle} tab is selected. When offset is
|
||||
* between 0.0 and 1.0, the {@code indicator} is moving between the startTitle and endTitle and
|
||||
* the indicator should reflect this movement.
|
||||
*
|
||||
* <p>By default, this class will move the indicator linearly between tab destinations.
|
||||
*
|
||||
* @param tabLayout The TabLayout parent of the indicator being drawn.
|
||||
* @param startTitle The title that should be indicated as selected when offset is 0.0.
|
||||
* @param endTitle The title that should be indicated as selected when offset is 1.0.
|
||||
* @param offset The fraction between startTitle and endTitle where the indicator is for a given
|
||||
* frame
|
||||
* @param indicator The drawable to be drawn to indicate the selected tab. Update the drawable's
|
||||
* bounds, color, etc as {@code offset} changes to show the indicator in the correct position.
|
||||
*/
|
||||
void setIndicatorBoundsForOffset(
|
||||
TabLayout tabLayout,
|
||||
View startTitle,
|
||||
View endTitle,
|
||||
@FloatRange(from = 0.0, to = 1.0) float offset,
|
||||
@NonNull Drawable indicator) {
|
||||
RectF startIndicator = calculateIndicatorWidthForTab(tabLayout, startTitle);
|
||||
// Linearly interpolate the indicator's position, using it's left and right bounds, between the
|
||||
// two destinations.
|
||||
RectF endIndicator = calculateIndicatorWidthForTab(tabLayout, endTitle);
|
||||
indicator.setBounds(
|
||||
lerp((int) startIndicator.left, (int) endIndicator.left, offset),
|
||||
indicator.getBounds().top,
|
||||
lerp((int) startIndicator.right, (int) endIndicator.right, offset),
|
||||
indicator.getBounds().bottom);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 org.joinmastodon.android.ui.tabs;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
/**
|
||||
* TabItem is a special 'view' which allows you to declare tab items for a {@link TabLayout} within
|
||||
* a layout. This view is not actually added to TabLayout, it is just a dummy which allows setting
|
||||
* of a tab items's text, icon and custom layout. See TabLayout for more information on how to use
|
||||
* it.
|
||||
*
|
||||
* @attr ref com.google.android.material.R.styleable#TabItem_android_icon
|
||||
* @attr ref com.google.android.material.R.styleable#TabItem_android_text
|
||||
* @attr ref com.google.android.material.R.styleable#TabItem_android_layout
|
||||
* @see TabLayout
|
||||
*/
|
||||
//TODO(b/76413401): make class final after the widget migration
|
||||
public class TabItem extends View {
|
||||
//TODO(b/76413401): make package private after the widget migration
|
||||
public final CharSequence text;
|
||||
//TODO(b/76413401): make package private after the widget migration
|
||||
public final Drawable icon;
|
||||
//TODO(b/76413401): make package private after the widget migration
|
||||
public final int customLayout;
|
||||
|
||||
public TabItem(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TabItem(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
final TypedArray a =
|
||||
context.obtainStyledAttributes(attrs, R.styleable.TabItem);
|
||||
text = a.getText(R.styleable.TabItem_android_text);
|
||||
icon = a.getDrawable(R.styleable.TabItem_android_icon);
|
||||
customLayout = a.getResourceId(R.styleable.TabItem_android_layout, 0);
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright 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 org.joinmastodon.android.ui.tabs;
|
||||
|
||||
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING;
|
||||
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE;
|
||||
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* A mediator to link a TabLayout with a ViewPager2. The mediator will synchronize the ViewPager2's
|
||||
* position with the selected tab when a tab is selected, and the TabLayout's scroll position when
|
||||
* the user drags the ViewPager2. TabLayoutMediator will listen to ViewPager2's OnPageChangeCallback
|
||||
* to adjust tab when ViewPager2 moves. TabLayoutMediator listens to TabLayout's
|
||||
* OnTabSelectedListener to adjust VP2 when tab moves. TabLayoutMediator listens to RecyclerView's
|
||||
* AdapterDataObserver to recreate tab content when dataset changes.
|
||||
*
|
||||
* <p>Establish the link by creating an instance of this class, make sure the ViewPager2 has an
|
||||
* adapter and then call {@link #attach()} on it. Instantiating a TabLayoutMediator will only create
|
||||
* the mediator object, {@link #attach()} will link the TabLayout and the ViewPager2 together. When
|
||||
* creating an instance of this class, you must supply an implementation of {@link
|
||||
* TabConfigurationStrategy} in which you set the text of the tab, and/or perform any styling of the
|
||||
* tabs that you require. Changing ViewPager2's adapter will require a {@link #detach()} followed by
|
||||
* {@link #attach()} call. Changing the ViewPager2 or TabLayout will require a new instantiation of
|
||||
* TabLayoutMediator.
|
||||
*/
|
||||
public final class TabLayoutMediator {
|
||||
@NonNull private final TabLayout tabLayout;
|
||||
@NonNull private final ViewPager2 viewPager;
|
||||
private final boolean autoRefresh;
|
||||
private final boolean smoothScroll;
|
||||
private final TabConfigurationStrategy tabConfigurationStrategy;
|
||||
@Nullable private RecyclerView.Adapter<?> adapter;
|
||||
private boolean attached;
|
||||
|
||||
@Nullable private TabLayoutOnPageChangeCallback onPageChangeCallback;
|
||||
@Nullable private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
@Nullable private RecyclerView.AdapterDataObserver pagerAdapterObserver;
|
||||
|
||||
/**
|
||||
* A callback interface that must be implemented to set the text and styling of newly created
|
||||
* tabs.
|
||||
*/
|
||||
public interface TabConfigurationStrategy {
|
||||
/**
|
||||
* Called to configure the tab for the page at the specified position. Typically calls {@link
|
||||
* TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
|
||||
*
|
||||
* @param tab The Tab which should be configured to represent the title of the item at the given
|
||||
* position in the data set.
|
||||
* @param position The position of the item within the adapter's data set.
|
||||
*/
|
||||
void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
|
||||
}
|
||||
|
||||
public TabLayoutMediator(
|
||||
@NonNull TabLayout tabLayout,
|
||||
@NonNull ViewPager2 viewPager,
|
||||
@NonNull TabConfigurationStrategy tabConfigurationStrategy) {
|
||||
this(tabLayout, viewPager, /* autoRefresh= */ true, tabConfigurationStrategy);
|
||||
}
|
||||
|
||||
public TabLayoutMediator(
|
||||
@NonNull TabLayout tabLayout,
|
||||
@NonNull ViewPager2 viewPager,
|
||||
boolean autoRefresh,
|
||||
@NonNull TabConfigurationStrategy tabConfigurationStrategy) {
|
||||
this(tabLayout, viewPager, autoRefresh, /* smoothScroll= */ true, tabConfigurationStrategy);
|
||||
}
|
||||
|
||||
public TabLayoutMediator(
|
||||
@NonNull TabLayout tabLayout,
|
||||
@NonNull ViewPager2 viewPager,
|
||||
boolean autoRefresh,
|
||||
boolean smoothScroll,
|
||||
@NonNull TabConfigurationStrategy tabConfigurationStrategy) {
|
||||
this.tabLayout = tabLayout;
|
||||
this.viewPager = viewPager;
|
||||
this.autoRefresh = autoRefresh;
|
||||
this.smoothScroll = smoothScroll;
|
||||
this.tabConfigurationStrategy = tabConfigurationStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link the TabLayout and the ViewPager2 together. Must be called after ViewPager2 has an adapter
|
||||
* set. To be called on a new instance of TabLayoutMediator or if the ViewPager2's adapter
|
||||
* changes.
|
||||
*
|
||||
* @throws IllegalStateException If the mediator is already attached, or the ViewPager2 has no
|
||||
* adapter.
|
||||
*/
|
||||
public void attach() {
|
||||
if (attached) {
|
||||
throw new IllegalStateException("TabLayoutMediator is already attached");
|
||||
}
|
||||
adapter = viewPager.getAdapter();
|
||||
if (adapter == null) {
|
||||
throw new IllegalStateException(
|
||||
"TabLayoutMediator attached before ViewPager2 has an " + "adapter");
|
||||
}
|
||||
attached = true;
|
||||
|
||||
// Add our custom OnPageChangeCallback to the ViewPager
|
||||
onPageChangeCallback = new TabLayoutOnPageChangeCallback(tabLayout);
|
||||
viewPager.registerOnPageChangeCallback(onPageChangeCallback);
|
||||
|
||||
// Now we'll add a tab selected listener to set ViewPager's current item
|
||||
onTabSelectedListener = new ViewPagerOnTabSelectedListener(viewPager, smoothScroll);
|
||||
tabLayout.addOnTabSelectedListener(onTabSelectedListener);
|
||||
|
||||
// Now we'll populate ourselves from the pager adapter, adding an observer if
|
||||
// autoRefresh is enabled
|
||||
if (autoRefresh) {
|
||||
// Register our observer on the new adapter
|
||||
pagerAdapterObserver = new PagerAdapterObserver();
|
||||
adapter.registerAdapterDataObserver(pagerAdapterObserver);
|
||||
}
|
||||
|
||||
populateTabsFromPagerAdapter();
|
||||
|
||||
// Now update the scroll position to match the ViewPager's current item
|
||||
tabLayout.setScrollPosition(viewPager.getCurrentItem(), 0f, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink the TabLayout and the ViewPager. To be called on a stale TabLayoutMediator if a new one
|
||||
* is instantiated, to prevent holding on to a view that should be garbage collected. Also to be
|
||||
* called before {@link #attach()} when a ViewPager2's adapter is changed.
|
||||
*/
|
||||
public void detach() {
|
||||
if (autoRefresh && adapter != null) {
|
||||
adapter.unregisterAdapterDataObserver(pagerAdapterObserver);
|
||||
pagerAdapterObserver = null;
|
||||
}
|
||||
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
||||
viewPager.unregisterOnPageChangeCallback(onPageChangeCallback);
|
||||
onTabSelectedListener = null;
|
||||
onPageChangeCallback = null;
|
||||
adapter = null;
|
||||
attached = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link TabLayout} and the {@link ViewPager2} are linked together.
|
||||
*/
|
||||
public boolean isAttached() {
|
||||
return attached;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
void populateTabsFromPagerAdapter() {
|
||||
tabLayout.removeAllTabs();
|
||||
|
||||
if (adapter != null) {
|
||||
int adapterCount = adapter.getItemCount();
|
||||
for (int i = 0; i < adapterCount; i++) {
|
||||
TabLayout.Tab tab = tabLayout.newTab();
|
||||
tabConfigurationStrategy.onConfigureTab(tab, i);
|
||||
tabLayout.addTab(tab, false);
|
||||
}
|
||||
// Make sure we reflect the currently set ViewPager item
|
||||
if (adapterCount > 0) {
|
||||
int lastItem = tabLayout.getTabCount() - 1;
|
||||
int currItem = Math.min(viewPager.getCurrentItem(), lastItem);
|
||||
if (currItem != tabLayout.getSelectedTabPosition()) {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(currItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ViewPager2.OnPageChangeCallback} class which contains the necessary calls back to the
|
||||
* provided {@link TabLayout} so that the tab position is kept in sync.
|
||||
*
|
||||
* <p>This class stores the provided TabLayout weakly, meaning that you can use {@link
|
||||
* ViewPager2#registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback)} without removing the
|
||||
* callback and not cause a leak.
|
||||
*/
|
||||
private static class TabLayoutOnPageChangeCallback extends ViewPager2.OnPageChangeCallback {
|
||||
@NonNull private final WeakReference<TabLayout> tabLayoutRef;
|
||||
private int previousScrollState;
|
||||
private int scrollState;
|
||||
|
||||
TabLayoutOnPageChangeCallback(TabLayout tabLayout) {
|
||||
tabLayoutRef = new WeakReference<>(tabLayout);
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(final int state) {
|
||||
previousScrollState = scrollState;
|
||||
scrollState = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
TabLayout tabLayout = tabLayoutRef.get();
|
||||
if (tabLayout != null) {
|
||||
// Only update the text selection if we're not settling, or we are settling after
|
||||
// being dragged
|
||||
boolean updateText =
|
||||
scrollState != SCROLL_STATE_SETTLING || previousScrollState == SCROLL_STATE_DRAGGING;
|
||||
// Update the indicator if we're not settling after being idle. This is caused
|
||||
// from a setCurrentItem() call and will be handled by an animation from
|
||||
// onPageSelected() instead.
|
||||
boolean updateIndicator =
|
||||
!(scrollState == SCROLL_STATE_SETTLING && previousScrollState == SCROLL_STATE_IDLE);
|
||||
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(final int position) {
|
||||
TabLayout tabLayout = tabLayoutRef.get();
|
||||
if (tabLayout != null
|
||||
&& tabLayout.getSelectedTabPosition() != position
|
||||
&& position < tabLayout.getTabCount()) {
|
||||
// Select the tab, only updating the indicator if we're not being dragged/settled
|
||||
// (since onPageScrolled will handle that).
|
||||
boolean updateIndicator =
|
||||
scrollState == SCROLL_STATE_IDLE
|
||||
|| (scrollState == SCROLL_STATE_SETTLING
|
||||
&& previousScrollState == SCROLL_STATE_IDLE);
|
||||
tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
previousScrollState = scrollState = SCROLL_STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back to the
|
||||
* provided {@link ViewPager2} so that the tab position is kept in sync.
|
||||
*/
|
||||
private static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
|
||||
private final ViewPager2 viewPager;
|
||||
private final boolean smoothScroll;
|
||||
|
||||
ViewPagerOnTabSelectedListener(ViewPager2 viewPager, boolean smoothScroll) {
|
||||
this.viewPager = viewPager;
|
||||
this.smoothScroll = smoothScroll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(@NonNull TabLayout.Tab tab) {
|
||||
viewPager.setCurrentItem(tab.getPosition(), smoothScroll);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
private class PagerAdapterObserver extends RecyclerView.AdapterDataObserver {
|
||||
PagerAdapterObserver() {}
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
populateTabsFromPagerAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(int positionStart, int itemCount) {
|
||||
populateTabsFromPagerAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
|
||||
populateTabsFromPagerAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
populateTabsFromPagerAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
populateTabsFromPagerAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
populateTabsFromPagerAdapter();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -43,6 +44,16 @@ public class UiUtils{
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static String abbreviateNumber(int n){
|
||||
if(n<1000)
|
||||
return String.format("%,d", n);
|
||||
else if(n<1_000_000)
|
||||
return String.format("%,.1fK", n/1000f);
|
||||
else
|
||||
return String.format("%,.1fM", n/1_000_000f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 6.0 has a bug where start and end compound drawables don't get tinted.
|
||||
* This works around it by setting the tint colors directly to the drawables.
|
||||
@@ -64,4 +75,9 @@ public class UiUtils{
|
||||
public static void runOnUiThread(Runnable runnable){
|
||||
mainHandler.post(runnable);
|
||||
}
|
||||
|
||||
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
|
||||
public static int lerp(int startValue, int endValue, float fraction) {
|
||||
return startValue + Math.round(fraction * (endValue - startValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class CoverImageView extends ImageView{
|
||||
private float imageTranslationY, imageScale=1f;
|
||||
|
||||
public CoverImageView(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CoverImageView(Context context, @Nullable AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CoverImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas){
|
||||
canvas.save();
|
||||
canvas.translate(0, imageTranslationY);
|
||||
super.onDraw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
public void setTransform(float transY, float scale){
|
||||
imageTranslationY=transY;
|
||||
imageScale=scale;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class NestedRecyclerScrollView extends CustomScrollView{
|
||||
private Supplier<RecyclerView> scrollableChildSupplier;
|
||||
|
||||
public NestedRecyclerScrollView(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
|
||||
final RecyclerView rv = (RecyclerView) target;
|
||||
if ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom())) {
|
||||
scrollBy(0, dy);
|
||||
consumed[1] = dy;
|
||||
return;
|
||||
}
|
||||
super.onNestedPreScroll(target, dx, dy, consumed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNestedPreFling(View target, float velX, float velY) {
|
||||
final RecyclerView rv = (RecyclerView) target;
|
||||
if ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom())) {
|
||||
fling((int) velY);
|
||||
return true;
|
||||
}
|
||||
return super.onNestedPreFling(target, velX, velY);
|
||||
}
|
||||
|
||||
private boolean isScrolledToBottom() {
|
||||
return !canScrollVertically(1);
|
||||
}
|
||||
|
||||
private boolean isScrolledToTop(RecyclerView rv) {
|
||||
final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
|
||||
return lm.findFirstVisibleItemPosition() == 0
|
||||
&& lm.findViewByPosition(0).getTop() == 0;
|
||||
}
|
||||
|
||||
public void setScrollableChildSupplier(Supplier<RecyclerView> scrollableChildSupplier){
|
||||
this.scrollableChildSupplier=scrollableChildSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onScrollingHitEdge(float velocity){
|
||||
if(velocity>0){
|
||||
RecyclerView view=scrollableChildSupplier.get();
|
||||
if(view!=null){
|
||||
return view.fling(0, (int)velocity);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user