/*
* Copyright (C) 2013 The CyanogenMod 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.launcher3;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import java.lang.IllegalArgumentException;
import java.util.ArrayList;
import java.util.Collections;
/**
* BaseRecyclerViewScrubber
*
* This is the scrubber at the bottom of a BaseRecyclerView
*
*
* @see {@link LinearLayout}
*/
public class BaseRecyclerViewScrubber extends LinearLayout {
private BaseRecyclerView mBaseRecyclerView;
private TextView mScrubberIndicator;
private SeekBar mSeekBar;
private AutoExpandTextView mScrubberText;
private SectionContainer mSectionContainer;
private ScrubberAnimationState mScrubberAnimationState;
private Drawable mTransparentDrawable;
private boolean mIsRtl;
private static final int MSG_SET_TARGET = 1000;
private static final int MSG_ANIMATE_PICK = MSG_SET_TARGET + 1;
/**
* UiHandler
*
* Using a handler for sending signals to perform certain actions. The reason for
* using this is to be able to remove and replace a signal if signals are being
* sent too fast (e.g. user scrubbing like crazy). This allows the touch loop to
* complete then later run the animations in their own loops.
*
*/
private class UiHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SET_TARGET:
int adapterIndex = msg.arg1;
performSetTarget(adapterIndex);
break;
case MSG_ANIMATE_PICK:
int index = msg.arg1;
int width = msg.arg2;
int lastIndex = (Integer)msg.obj;
performAnimatePickMessage(index, width, lastIndex);
break;
default:
super.handleMessage(msg);
}
}
/**
* Overidden to remove identical calls if they are called subsequently fast enough.
*
* This is the final point that is public in the call chain. Other calls to sendMessageXXX
* will eventually call this function which calls "enqueueMessage" which is private.
*
* @param msg {@link Message}
* @param uptimeMillis {@link Long}
*
* @throws IllegalArgumentException {@link IllegalArgumentException}
*/
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) throws
IllegalArgumentException {
if (msg == null) {
throw new IllegalArgumentException("'msg' cannot be null!");
}
if (hasMessages(msg.what)) {
removeMessages(msg.what);
}
return super.sendMessageAtTime(msg, uptimeMillis);
}
}
private Handler mUiHandler = new UiHandler();
private void sendSetTargetMessage(int adapterIndex) {
Message msg = mUiHandler.obtainMessage(MSG_SET_TARGET);
msg.what = MSG_SET_TARGET;
msg.arg1 = adapterIndex;
mUiHandler.sendMessage(msg);
}
private void performSetTarget(int adapterIndex) {
mBaseRecyclerView.scrollToSection(mSectionContainer.getSectionName(adapterIndex, mIsRtl));
}
private void sendAnimatePickMessage(int index, int width, int lastIndex) {
Message msg = mUiHandler.obtainMessage(MSG_ANIMATE_PICK);
msg.what = MSG_ANIMATE_PICK;
msg.arg1 = index;
msg.arg2 = width;
msg.obj = lastIndex;
mUiHandler.sendMessage(msg);
}
private void performAnimatePickMessage(int index, int width, int lastIndex) {
if (mScrubberIndicator != null) {
// get the index based on the direction the user is scrolling
int directionalIndex = mSectionContainer.getDirectionalIndex(lastIndex, index);
String sectionText = mSectionContainer.getSectionName(directionalIndex, mIsRtl);
float translateX = (index * width) / (float) mSectionContainer.size();
// if we are showing letters, grab the position based on the text view
if (mSectionContainer.showLetters()) {
translateX = mScrubberText.getPositionOfSection(index);
}
// center the x position
translateX -= mScrubberIndicator.getMeasuredWidth() / 2;
if (mIsRtl) {
translateX = -translateX;
}
mScrubberIndicator.setTranslationX(translateX);
mScrubberIndicator.setText(sectionText);
}
}
/**
* Constructor
*
* @param context {@link Context}
* @param attrs {@link AttributeSet}
*/
public BaseRecyclerViewScrubber(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
/**
* Constructor
*
* @param context {@link Context}
*/
public BaseRecyclerViewScrubber(Context context) {
super(context);
init(context);
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
mIsRtl = Utilities.isRtl(getResources());
updateSections();
}
/**
* Simple container class that tries to abstract out the knowledge of complex sections vs
* simple string sections
*/
private static class SectionContainer {
private BaseRecyclerViewScrubberSection.
RtlIndexArrayList mSections;
private String[] mSectionNames;
private final boolean mIsRtl;
public SectionContainer(String[] sections, boolean isRtl) {
mIsRtl = isRtl;
mSections = BaseRecyclerViewScrubberSection.createSections(sections, isRtl);
mSectionNames = sections;
if (isRtl && mSections != null) {
final int N = mSectionNames.length;
for(int i = 0; i < N / 2; i++) {
String temp = mSectionNames[i];
mSectionNames[i] = mSectionNames[N - i - 1];
mSectionNames[N - i - 1] = temp;
}
Collections.reverse(mSections);
}
}
public int size() {
return showLetters() ? mSections.size() : mSectionNames.length;
}
public String getSectionName(int idx, boolean isRtl) {
if (size() == 0) {
return null;
}
return showLetters() ? mSections.get(idx, isRtl).getText() : mSectionNames[idx];
}
/**
* Because the list section headers is not necessarily the same size as the scrubber
* letters, we need to map from the larger list to the smaller list.
* In the case that curIdx is not highlighted, it will use the directional index to
* determine the adapter index
* @return the mSectionNames index (aka the underlying adapter index).
*/
public int getAdapterIndex(int prevIdx, int curIdx) {
if (!showLetters() || size() == 0) {
return curIdx;
}
// because we have some unhighlighted letters, we need to first get the directional
// index before getting the adapter index
return mSections.get(getDirectionalIndex(prevIdx, curIdx), mIsRtl).getAdapterIndex();
}
/**
* Given the direction the user is scrolling in, return the closest index which is a
* highlighted index
*/
public int getDirectionalIndex(int prevIdx, int curIdx) {
if (!showLetters() || size() == 0 || mSections.get(curIdx, mIsRtl).getHighlight()) {
return curIdx;
}
if (prevIdx < curIdx) {
if (mIsRtl) {
return mSections.get(curIdx).getPreviousIndex();
} else {
return mSections.get(curIdx).getNextIndex();
}
} else {
if (mIsRtl) {
return mSections.get(curIdx).getNextIndex();
} else {
return mSections.get(curIdx).getPreviousIndex();
}
}
}
/**
* @return true if the scrubber is showing characters as opposed to a line
*/
public boolean showLetters() {
return mSections != null;
}
/**
* Initializes the scrubber text with the proper characters
*/
public void initializeScrubberText(AutoExpandTextView scrubberText) {
scrubberText.setSections(BaseRecyclerViewScrubberSection.getHighlightText(mSections));
}
}
public void updateSections() {
if (mBaseRecyclerView != null) {
mSectionContainer = new SectionContainer(mBaseRecyclerView.getSectionNames(), mIsRtl);
mSectionContainer.initializeScrubberText(mScrubberText);
mSeekBar.setMax(mSectionContainer.size() - 1);
// show a white line if there are no letters, otherwise show transparent
Drawable d = mSectionContainer.showLetters() ? mTransparentDrawable
: getContext().getResources().getDrawable(R.drawable.seek_back);
((ViewGroup) mSeekBar.getParent()).setBackground(d);
}
}
public void setRecycler(BaseRecyclerView baseRecyclerView) {
mBaseRecyclerView = baseRecyclerView;
}
public void setScrubberIndicator(TextView scrubberIndicator) {
mScrubberIndicator = scrubberIndicator;
}
private boolean isReady() {
return mBaseRecyclerView != null &&
mSectionContainer != null;
}
private void init(Context context) {
mIsRtl = Utilities.isRtl(context.getResources());
LayoutInflater.from(context).inflate(R.layout.scrub_layout, this);
mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
mScrubberAnimationState = new ScrubberAnimationState();
mSeekBar = (SeekBar) findViewById(R.id.scrubber);
mScrubberText = (AutoExpandTextView) findViewById(R.id.scrubberText);
mSeekBar.setOnSeekBarChangeListener(mScrubberAnimationState);
}
/**
* Handles the animations of the scrubber indicator
*/
private class ScrubberAnimationState implements SeekBar.OnSeekBarChangeListener {
private static final long SCRUBBER_DISPLAY_DURATION_IN = 60;
private static final long SCRUBBER_DISPLAY_DURATION_OUT = 150;
private static final long SCRUBBER_DISPLAY_DELAY_IN = 0;
private static final long SCRUBBER_DISPLAY_DELAY_OUT = 200;
private static final float SCRUBBER_SCALE_START = 0f;
private static final float SCRUBBER_SCALE_END = 1f;
private static final float SCRUBBER_ALPHA_START = 0f;
private static final float SCRUBBER_ALPHA_END = 1f;
private boolean mTouchingTrack = false;
private boolean mAnimatingIn = false;
private int mLastIndex = -1;
private void touchTrack(boolean touching) {
mTouchingTrack = touching;
if (mScrubberIndicator != null) {
if (mTouchingTrack) {
animateIn();
} else if (!mAnimatingIn) { // finish animating in before animating out
animateOut();
}
mBaseRecyclerView.setFastScrollDragging(mTouchingTrack);
if (mTouchingTrack) {
mBaseRecyclerView.setPreviousSectionFastScrollFocused();
}
}
}
private void animateIn() {
if (mScrubberIndicator == null) {
return;
}
// start from a scratch position when animating in
mScrubberIndicator.animate().cancel();
mScrubberIndicator.setPivotX(mScrubberIndicator.getMeasuredWidth() / 2);
mScrubberIndicator.setPivotY(mScrubberIndicator.getMeasuredHeight() * 0.9f);
mScrubberIndicator.setAlpha(SCRUBBER_ALPHA_START);
mScrubberIndicator.setScaleX(SCRUBBER_SCALE_START);
mScrubberIndicator.setScaleY(SCRUBBER_SCALE_START);
mScrubberIndicator.setVisibility(View.VISIBLE);
mAnimatingIn = true;
mScrubberIndicator.animate()
.alpha(SCRUBBER_ALPHA_END)
.scaleX(SCRUBBER_SCALE_END)
.scaleY(SCRUBBER_SCALE_END)
.setStartDelay(SCRUBBER_DISPLAY_DELAY_IN)
.setDuration(SCRUBBER_DISPLAY_DURATION_IN)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimatingIn = false;
// if the user has stopped touching the seekbar, animate back out
if (!mTouchingTrack) {
animateOut();
}
}
}).start();
}
private void animateOut() {
if (mScrubberIndicator == null) {
return;
}
mScrubberIndicator.animate()
.alpha(SCRUBBER_ALPHA_START)
.scaleX(SCRUBBER_SCALE_START)
.scaleY(SCRUBBER_SCALE_START)
.setStartDelay(SCRUBBER_DISPLAY_DELAY_OUT)
.setDuration(SCRUBBER_DISPLAY_DURATION_OUT)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mScrubberIndicator.setVisibility(View.INVISIBLE);
}
});
}
@Override
public void onProgressChanged(SeekBar seekBar, int index, boolean fromUser) {
if (!isReady()) {
return;
}
progressChanged(seekBar, index, fromUser);
}
private void progressChanged(SeekBar seekBar, int index, boolean fromUser) {
if (!fromUser) {
return;
}
sendAnimatePickMessage(index, seekBar.getWidth(), mLastIndex);
// get the index of the underlying list
int adapterIndex = mSectionContainer.getDirectionalIndex(mLastIndex, index);
// Post set target index on queue to get processed by Looper later
sendSetTargetMessage(adapterIndex);
mLastIndex = index;
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
touchTrack(true);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
touchTrack(false);
}
}
}