| /* |
| * Copyright (C) 2017 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 androidx.car.widget; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.PorterDuff; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.GradientDrawable; |
| import android.util.AttributeSet; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.animation.AccelerateDecelerateInterpolator; |
| import android.view.animation.Interpolator; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.annotation.ColorRes; |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.IntRange; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.car.R; |
| import androidx.core.content.ContextCompat; |
| |
| /** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */ |
| public class PagedScrollBarView extends ViewGroup { |
| private static final float BUTTON_DISABLED_ALPHA = 0.2f; |
| private static final int SCROLL_TRANSLATION_ANIM_DURATION_MS = 200; |
| |
| /** Listener for when the list should paginate. */ |
| public interface PaginationListener { |
| int PAGE_UP = 0; |
| int PAGE_DOWN = 1; |
| |
| /** Called when the linked view should be paged in the given direction */ |
| void onPaginate(int direction); |
| |
| /** |
| * Called when the 'alpha jump' button is clicked and the linked view should switch into |
| * alpha jump mode, where we display a list of buttons to allow the user to quickly scroll |
| * to a certain point in the list, bypassing a lot of manual scrolling. |
| */ |
| void onAlphaJump(); |
| } |
| |
| private final ImageView mUpButton; |
| private final PaginateButtonClickListener mUpButtonClickListener; |
| private final ImageView mDownButton; |
| private final PaginateButtonClickListener mDownButtonClickListener; |
| private final TextView mAlphaJumpButton; |
| private final AlphaJumpButtonClickListener mAlphaJumpButtonClickListener; |
| private final View mScrollThumb; |
| |
| private final int mSeparatingMargin; |
| private final int mScrollBarThumbWidth; |
| |
| private boolean mShowScrollBarThumb; |
| /** The amount of space that the scroll thumb is allowed to roam over. */ |
| private int mScrollThumbTrackHeight; |
| |
| private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator(); |
| |
| public PagedScrollBarView(Context context) { |
| this(context, /* attrs= */ null, R.attr.pagedScrollBarViewStyle, |
| R.style.Widget_Car_Scrollbar_Light); |
| } |
| |
| public PagedScrollBarView(Context context, AttributeSet attrs) { |
| this(context, attrs, R.attr.pagedScrollBarViewStyle, R.style.Widget_Car_Scrollbar_Light); |
| } |
| |
| public PagedScrollBarView(Context context, AttributeSet attrs, int defStyleAttrs) { |
| this(context, attrs, defStyleAttrs, R.style.Widget_Car_Scrollbar_Light); |
| } |
| |
| public PagedScrollBarView( |
| Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { |
| super(context, attrs, defStyleAttrs, defStyleRes); |
| |
| Resources res = context.getResources(); |
| mSeparatingMargin = res.getDimensionPixelSize(R.dimen.car_padding_4); |
| mScrollBarThumbWidth = res.getDimensionPixelSize(R.dimen.car_scroll_bar_thumb_width); |
| |
| LayoutInflater.from(context).inflate(R.layout.car_paged_scrollbar_buttons, |
| /* root= */ this, /* attachToRoot= */ true); |
| |
| mUpButtonClickListener = new PaginateButtonClickListener(PaginationListener.PAGE_UP); |
| mDownButtonClickListener = new PaginateButtonClickListener(PaginationListener.PAGE_DOWN); |
| mAlphaJumpButtonClickListener = new AlphaJumpButtonClickListener(); |
| |
| mUpButton = findViewById(R.id.page_up); |
| mUpButton.setOnClickListener(mUpButtonClickListener); |
| mDownButton = findViewById(R.id.page_down); |
| mDownButton.setOnClickListener(mDownButtonClickListener); |
| mAlphaJumpButton = findViewById(R.id.alpha_jump); |
| mAlphaJumpButton.setOnClickListener(mAlphaJumpButtonClickListener); |
| mScrollThumb = findViewById(R.id.scrollbar_thumb); |
| |
| TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PagedScrollBarView, |
| defStyleAttrs, defStyleRes); |
| |
| mShowScrollBarThumb = a.getBoolean(R.styleable.PagedScrollBarView_showScrollBarThumb, true); |
| setScrollbarThumbEnabled(mShowScrollBarThumb); |
| Drawable upButtonIcon = a.getDrawable(R.styleable.PagedScrollBarView_upButtonIcon); |
| if (upButtonIcon != null) { |
| setUpButtonIcon(upButtonIcon); |
| } |
| |
| Drawable downButtonIcon = a.getDrawable(R.styleable.PagedScrollBarView_downButtonIcon); |
| if (downButtonIcon != null) { |
| setDownButtonIcon(downButtonIcon); |
| } |
| |
| int scrollBarColor = a.getResourceId(R.styleable.PagedScrollBarView_scrollBarColor, -1); |
| if (scrollBarColor != -1) { |
| setScrollbarThumbColor(scrollBarColor); |
| } |
| |
| int buttonTintColor = a.getResourceId(R.styleable.PagedScrollBarView_buttonTintColor, -1); |
| if (buttonTintColor != -1) { |
| setButtonTintColor(buttonTintColor); |
| } |
| |
| int buttonRippleBackground = |
| a.getResourceId(R.styleable.PagedScrollBarView_buttonRippleBackground, -1); |
| if (buttonRippleBackground != -1) { |
| setButtonRippleBackground(buttonRippleBackground); |
| } |
| |
| a.recycle(); |
| } |
| |
| /** Sets the icon to be used for the up button. */ |
| public void setUpButtonIcon(Drawable icon) { |
| mUpButton.setImageDrawable(icon); |
| } |
| |
| /** Sets the icon to be used for the down button. */ |
| public void setDownButtonIcon(Drawable icon) { |
| mDownButton.setImageDrawable(icon); |
| } |
| |
| /** |
| * Sets the listener that will be notified when the up and down buttons have been pressed. |
| * |
| * @param listener The listener to set. |
| */ |
| public void setPaginationListener(PaginationListener listener) { |
| mUpButtonClickListener.setPaginationListener(listener); |
| mDownButtonClickListener.setPaginationListener(listener); |
| mAlphaJumpButtonClickListener.setPaginationListener(listener); |
| } |
| |
| /** Returns {@code true} if the "up" button is pressed */ |
| public boolean isUpPressed() { |
| return mUpButton.isPressed(); |
| } |
| |
| /** Returns {@code true} if the "down" button is pressed */ |
| public boolean isDownPressed() { |
| return mDownButton.isPressed(); |
| } |
| |
| void setShowAlphaJump(boolean show) { |
| mAlphaJumpButton.setVisibility(show ? VISIBLE : GONE); |
| } |
| |
| /** Returns {@code true} if the scroll bar thumb is visible */ |
| public boolean isScrollbarThumbEnabled() { |
| return mShowScrollBarThumb; |
| } |
| /** |
| * Sets whether or not the scroll bar thumb is visible, the default value is true. |
| * |
| * @param show {@code true} if the scroll bar thumb is visible. |
| */ |
| public void setScrollbarThumbEnabled(boolean show) { |
| mShowScrollBarThumb = show; |
| mScrollThumb.setVisibility(mShowScrollBarThumb ? VISIBLE : GONE); |
| } |
| |
| /** |
| * Sets the range, offset and extent of the scroll bar. The range represents the size of a |
| * container for the scrollbar thumb; offset is the distance from the start of the container |
| * to where the thumb should be; and finally, extent is the size of the thumb. |
| * |
| * <p>These values can be expressed in arbitrary units, so long as they share the same units. |
| * The values should also be positive. |
| * |
| * @param range The range of the scrollbar's thumb |
| * @param offset The offset of the scrollbar's thumb |
| * @param extent The extent of the scrollbar's thumb |
| * @param animate Whether or not the thumb should animate from its current position to the |
| * position specified by the given range, offset and extent. |
| * |
| * @see View#computeVerticalScrollRange() |
| * @see View#computeVerticalScrollOffset() |
| * @see View#computeVerticalScrollExtent() |
| */ |
| public void setParameters( |
| @IntRange(from = 0) int range, |
| @IntRange(from = 0) int offset, |
| @IntRange(from = 0) int extent, boolean animate) { |
| // Not laid out yet, so values cannot be calculated. |
| if (!isLaidOut()) { |
| return; |
| } |
| |
| // If the scroll bars aren't visible, then no need to update. |
| if (getVisibility() == GONE || range == 0) { |
| return; |
| } |
| |
| int thumbLength = calculateScrollThumbLength(range, extent); |
| |
| // Sets the size of the thumb and request a redraw if needed. |
| ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams(); |
| |
| if (lp.height != thumbLength) { |
| lp.height = thumbLength; |
| mScrollThumb.requestLayout(); |
| } |
| |
| int thumbOffset = calculateScrollThumbOffset(range, offset, thumbLength); |
| moveTranslationY(mScrollThumb, thumbOffset, animate); |
| } |
| |
| /** |
| * An optimized version of {@link #setParameters(int, int, int, boolean)} that is meant to be |
| * called if a view is laying itself out. This method will avoid a complete remeasure of |
| * the views in the {@code PagedScrollBarView} if the scroll thumb's height needs to be changed. |
| * Instead, only the thumb itself will be remeasured and laid out. |
| * |
| * <p>These values can be expressed in arbitrary units, so long as they share the same units. |
| * |
| * @param range The range of the scrollbar's thumb |
| * @param offset The offset of the scrollbar's thumb |
| * @param extent The extent of the scrollbar's thumb |
| * |
| * @see #setParameters(int, int, int, boolean) |
| */ |
| void setParametersInLayout(int range, int offset, int extent) { |
| // If the scroll bars aren't visible, then no need to update. |
| if (getVisibility() == GONE || range == 0) { |
| return; |
| } |
| |
| int thumbLength = calculateScrollThumbLength(range, extent); |
| |
| // Sets the size of the thumb and request a redraw if needed. |
| ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams(); |
| |
| if (lp.height != thumbLength) { |
| lp.height = thumbLength; |
| measureAndLayoutScrollThumb(); |
| } |
| |
| int thumbOffset = calculateScrollThumbOffset(range, offset, thumbLength); |
| moveTranslationY(mScrollThumb, thumbOffset, /* animate= */ false); |
| } |
| |
| /** |
| * Sets whether or not the up button on the scroll bar is clickable. |
| * |
| * @param enabled {@code true} if the up button is enabled. |
| */ |
| public void setUpEnabled(boolean enabled) { |
| mUpButton.setEnabled(enabled); |
| mUpButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA); |
| } |
| |
| /** |
| * Sets whether or not the down button on the scroll bar is clickable. |
| * |
| * @param enabled {@code true} if the down button is enabled. |
| */ |
| public void setDownEnabled(boolean enabled) { |
| mDownButton.setEnabled(enabled); |
| mDownButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA); |
| } |
| |
| /** |
| * Returns whether or not the down button on the scroll bar is clickable. |
| * |
| * @return {@code true} if the down button is enabled. {@code false} otherwise. |
| */ |
| public boolean isDownEnabled() { |
| return mDownButton.isEnabled(); |
| } |
| |
| /** |
| * Sets the color of thumb. |
| * |
| * @param color Resource identifier of the color. |
| */ |
| public void setScrollbarThumbColor(@ColorRes int color) { |
| GradientDrawable background = (GradientDrawable) mScrollThumb.getBackground(); |
| background.setColor(getContext().getColor(color)); |
| } |
| |
| /** |
| * Sets the tint color for the up and down buttons of this view. |
| * |
| * @param tintResId Resource identifier of the tint color. |
| */ |
| public void setButtonTintColor(@ColorRes int tintResId) { |
| int tint = ContextCompat.getColor(getContext(), tintResId); |
| mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN); |
| mDownButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN); |
| } |
| |
| /** |
| * Sets the drawable that will function as the background for the buttons in this view. This |
| * background should provide the ripple. |
| * |
| * @param backgroundResId The drawable resource identifier for the ripple background. |
| */ |
| public void setButtonRippleBackground(@DrawableRes int backgroundResId) { |
| mUpButton.setBackgroundResource(backgroundResId); |
| mDownButton.setBackgroundResource(backgroundResId); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| int requestedWidth = MeasureSpec.getSize(widthMeasureSpec); |
| int requestedHeight = MeasureSpec.getSize(heightMeasureSpec); |
| |
| int wrapMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| |
| mUpButton.measure(wrapMeasureSpec, wrapMeasureSpec); |
| mDownButton.measure(wrapMeasureSpec, wrapMeasureSpec); |
| |
| measureScrollThumb(); |
| |
| if (mAlphaJumpButton.getVisibility() != GONE) { |
| mAlphaJumpButton.measure(wrapMeasureSpec, wrapMeasureSpec); |
| } |
| |
| setMeasuredDimension(requestedWidth, requestedHeight); |
| } |
| |
| @Override |
| public void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| int width = right - left; |
| int height = bottom - top; |
| |
| // This value will keep track of the top of the current view being laid out. |
| int layoutTop = getPaddingTop(); |
| |
| // Lay out the up button at the top of the view. |
| layoutViewCenteredFromTop(mUpButton, layoutTop, width); |
| layoutTop = mUpButton.getBottom(); |
| |
| // Lay out the alpha jump button if it exists. This button goes below the up button. |
| if (mAlphaJumpButton.getVisibility() != GONE) { |
| layoutTop += mSeparatingMargin; |
| |
| layoutViewCenteredFromTop(mAlphaJumpButton, layoutTop, width); |
| |
| layoutTop = mAlphaJumpButton.getBottom(); |
| } |
| |
| // Lay out the scroll thumb |
| layoutTop += mSeparatingMargin; |
| layoutViewCenteredFromTop(mScrollThumb, layoutTop, width); |
| |
| // Lay out the bottom button at the bottom of the view. |
| int downBottom = height - getPaddingBottom(); |
| layoutViewCenteredFromBottom(mDownButton, downBottom, width); |
| |
| calculateScrollThumbTrackHeight(); |
| } |
| |
| /** |
| * Calculate the amount of space that the scroll bar thumb is allowed to roam. The thumb |
| * is allowed to take up the space between the down bottom and the up or alpha jump |
| * button, depending on if the latter is visible. |
| */ |
| private void calculateScrollThumbTrackHeight() { |
| // Subtracting (2 * mSeparatingMargin) for the top/bottom margin above and below the |
| // scroll bar thumb. |
| mScrollThumbTrackHeight = mDownButton.getTop() - (2 * mSeparatingMargin); |
| |
| // If there's an alpha jump button, then the thumb is laid out starting from below that. |
| if (mAlphaJumpButton.getVisibility() != GONE) { |
| mScrollThumbTrackHeight -= mAlphaJumpButton.getBottom(); |
| } else { |
| mScrollThumbTrackHeight -= mUpButton.getBottom(); |
| } |
| } |
| |
| private void measureScrollThumb() { |
| int scrollWidth = MeasureSpec.makeMeasureSpec(mScrollBarThumbWidth, MeasureSpec.EXACTLY); |
| int scrollHeight = MeasureSpec.makeMeasureSpec( |
| mScrollThumb.getLayoutParams().height, |
| MeasureSpec.EXACTLY); |
| mScrollThumb.measure(scrollWidth, scrollHeight); |
| } |
| |
| /** |
| * An optimization method to only remeasure and lay out the scroll thumb. This method should be |
| * used when the height of the thumb has changed, but no other views need to be remeasured. |
| */ |
| private void measureAndLayoutScrollThumb() { |
| measureScrollThumb(); |
| |
| // The top value should not change from what it was before; only the height is assumed to |
| // be changing. |
| int layoutTop = mScrollThumb.getTop(); |
| layoutViewCenteredFromTop(mScrollThumb, layoutTop, getMeasuredWidth()); |
| } |
| |
| /** |
| * Lays out the given View starting from the given {@code top} value downwards and centered |
| * within the given {@code availableWidth}. |
| * |
| * @param view The view to lay out. |
| * @param top The top value to start laying out from. This value will be the resulting top |
| * value of the view. |
| * @param availableWidth The width in which to center the given view. |
| */ |
| private void layoutViewCenteredFromTop(View view, int top, int availableWidth) { |
| int viewWidth = view.getMeasuredWidth(); |
| int viewLeft = (availableWidth - viewWidth) / 2; |
| view.layout(viewLeft, top, viewLeft + viewWidth, |
| top + view.getMeasuredHeight()); |
| } |
| |
| /** |
| * Lays out the given View starting from the given {@code bottom} value upwards and centered |
| * within the given {@code availableSpace}. |
| * |
| * @param view The view to lay out. |
| * @param bottom The bottom value to start laying out from. This value will be the resulting |
| * bottom value of the view. |
| * @param availableWidth The width in which to center the given view. |
| */ |
| private void layoutViewCenteredFromBottom(View view, int bottom, int availableWidth) { |
| int viewWidth = view.getMeasuredWidth(); |
| int viewLeft = (availableWidth - viewWidth) / 2; |
| view.layout(viewLeft, bottom - view.getMeasuredHeight(), |
| viewLeft + viewWidth, bottom); |
| } |
| |
| @VisibleForTesting |
| int getScrollbarThumbColor() { |
| return ((GradientDrawable) mScrollThumb.getBackground()).getColor().getDefaultColor(); |
| } |
| |
| /** |
| * Calculates and returns how big the scroll bar thumb should be based on the given range and |
| * extent. |
| * |
| * @param range The total amount of space the scroll bar is allowed to roam over. |
| * @param extent The amount of space that the scroll bar takes up relative to the range. |
| * @return The height of the scroll bar thumb in pixels. |
| */ |
| private int calculateScrollThumbLength(int range, int extent) { |
| // Scale the length by the available space that the thumb can fill. |
| return Math.round(((float) extent / range) * mScrollThumbTrackHeight); |
| } |
| |
| /** |
| * Calculates and returns how much the scroll thumb should be offset from the top of where it |
| * has been laid out. |
| * |
| * @param range The total amount of space the scroll bar is allowed to roam over. |
| * @param offset The amount the scroll bar should be offset, expressed in the same units as |
| * the given range. |
| * @param thumbLength The current length of the thumb in pixels. |
| * @return The amount the thumb should be offset from its current to position in pixels. |
| */ |
| private int calculateScrollThumbOffset(int range, int offset, int thumbLength) { |
| // Ensure that if the user has reached the bottom of the list, then the scroll bar is |
| // aligned to the bottom as well. Otherwise, scale the offset appropriately. |
| return isDownEnabled() |
| ? Math.round(((float) offset / range) * mScrollThumbTrackHeight) |
| : mScrollThumbTrackHeight - thumbLength; |
| } |
| |
| /** Moves the given view's translationY to the specified position. */ |
| private void moveTranslationY(View view, float translationY, boolean animate) { |
| int duration = animate ? SCROLL_TRANSLATION_ANIM_DURATION_MS : 0; |
| view.animate() |
| .translationY(translationY) |
| .setDuration(duration) |
| .setInterpolator(mPaginationInterpolator) |
| .start(); |
| } |
| |
| private static class PaginateButtonClickListener implements View.OnClickListener { |
| private final int mPaginateDirection; |
| private PaginationListener mPaginationListener; |
| |
| PaginateButtonClickListener(int paginateDirection) { |
| mPaginateDirection = paginateDirection; |
| } |
| |
| public void setPaginationListener(PaginationListener listener) { |
| mPaginationListener = listener; |
| } |
| |
| @Override |
| public void onClick(View v) { |
| if (mPaginationListener != null) { |
| mPaginationListener.onPaginate(mPaginateDirection); |
| } |
| } |
| } |
| |
| private static class AlphaJumpButtonClickListener implements View.OnClickListener { |
| private PaginationListener mPaginationListener; |
| |
| AlphaJumpButtonClickListener() { |
| } |
| |
| public void setPaginationListener(PaginationListener listener) { |
| mPaginationListener = listener; |
| } |
| |
| @Override |
| public void onClick(View v) { |
| if (mPaginationListener != null) { |
| mPaginationListener.onAlphaJump(); |
| } |
| } |
| |
| } |
| } |