session-android/app/src/main/java/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java

214 lines
7.6 KiB
Java

/**
* Modified version of
* https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller
*
* Their license:
*
* 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.thoughtcrime.securesms.components;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build.VERSION;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.TextView;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RecyclerViewFastScroller extends LinearLayout {
private static final int BUBBLE_ANIMATION_DURATION = 100;
private static final int TRACK_SNAP_RANGE = 5;
private @NonNull TextView bubble;
private @NonNull View handle;
private @Nullable RecyclerView recyclerView;
private int height;
private ObjectAnimator currentAnimator;
private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
if (handle.isSelected()) return;
final int offset = recyclerView.computeVerticalScrollOffset();
final int range = recyclerView.computeVerticalScrollRange();
final int extent = recyclerView.computeVerticalScrollExtent();
final int offsetRange = Math.max(range - extent, 1);
setBubbleAndHandlePosition((float) Util.clamp(offset, 0, offsetRange) / offsetRange);
}
};
public interface FastScrollAdapter {
CharSequence getBubbleText(int pos);
}
public RecyclerViewFastScroller(final Context context) {
this(context, null);
}
public RecyclerViewFastScroller(final Context context, final AttributeSet attrs) {
super(context, attrs);
setOrientation(HORIZONTAL);
setClipChildren(false);
setScrollContainer(true);
inflate(context, R.layout.recycler_view_fast_scroller, this);
bubble = ViewUtil.findById(this, R.id.fastscroller_bubble);
handle = ViewUtil.findById(this, R.id.fastscroller_handle);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
height = h;
}
@Override
@TargetApi(11)
public boolean onTouchEvent(@NonNull MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() ||
event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() ||
event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom())
{
return false;
}
if (currentAnimator != null) {
currentAnimator.cancel();
}
if (bubble.getVisibility() != VISIBLE) {
showBubble();
}
handle.setSelected(true);
case MotionEvent.ACTION_MOVE:
final float y = event.getY();
setBubbleAndHandlePosition(y / height);
setRecyclerViewPosition(y);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
handle.setSelected(false);
hideBubble();
return true;
}
return super.onTouchEvent(event);
}
public void setRecyclerView(final @NonNull RecyclerView recyclerView) {
if (this.recyclerView != null) {
this.recyclerView.removeOnScrollListener(onScrollListener);
}
this.recyclerView = recyclerView;
recyclerView.addOnScrollListener(onScrollListener);
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
recyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
if (handle.isSelected()) return true;
final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
setBubbleAndHandlePosition(height * proportion);
return true;
}
});
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (recyclerView != null)
recyclerView.removeOnScrollListener(onScrollListener);
}
private void setRecyclerViewPosition(float y) {
if (recyclerView != null) {
final int itemCount = recyclerView.getAdapter().getItemCount();
float proportion;
if (ViewUtil.getY(handle) == 0) {
proportion = 0f;
} else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE) {
proportion = 1f;
} else {
proportion = y / (float)height;
}
final int targetPos = Util.clamp((int)(proportion * (float)itemCount), 0, itemCount - 1);
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0);
final CharSequence bubbleText = ((FastScrollAdapter) recyclerView.getAdapter()).getBubbleText(targetPos);
bubble.setText(bubbleText);
}
}
private void setBubbleAndHandlePosition(float y) {
final int handleHeight = handle.getHeight();
final int bubbleHeight = bubble.getHeight();
final int handleY = Util.clamp((int)((height - handleHeight) * y), 0, height - handleHeight);
ViewUtil.setY(handle, handleY);
ViewUtil.setY(bubble, Util.clamp(handleY - bubbleHeight - bubble.getPaddingBottom() + handleHeight,
0,
height - bubbleHeight));
}
@TargetApi(11)
private void showBubble() {
bubble.setVisibility(VISIBLE);
if (VERSION.SDK_INT >= 11) {
if (currentAnimator != null) currentAnimator.cancel();
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION);
currentAnimator.start();
}
}
@TargetApi(11)
private void hideBubble() {
if (VERSION.SDK_INT >= 11) {
if (currentAnimator != null) currentAnimator.cancel();
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION);
currentAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
bubble.setVisibility(INVISIBLE);
currentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
bubble.setVisibility(INVISIBLE);
currentAnimator = null;
}
});
currentAnimator.start();
} else {
bubble.setVisibility(INVISIBLE);
}
}
}