summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/BaseRecyclerViewScrubber.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/BaseRecyclerViewScrubber.java')
-rw-r--r--src/com/android/launcher3/BaseRecyclerViewScrubber.java424
1 files changed, 424 insertions, 0 deletions
diff --git a/src/com/android/launcher3/BaseRecyclerViewScrubber.java b/src/com/android/launcher3/BaseRecyclerViewScrubber.java
new file mode 100644
index 000000000..1692548a4
--- /dev/null
+++ b/src/com/android/launcher3/BaseRecyclerViewScrubber.java
@@ -0,0 +1,424 @@
+/*
+ * 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
+ * <pre>
+ * This is the scrubber at the bottom of a BaseRecyclerView
+ * </pre>
+ *
+ * @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
+ * <pre>
+ * 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.
+ * </pre>
+ */
+ 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<BaseRecyclerViewScrubberSection> 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) {
+ 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);
+ }
+ }
+
+ 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) {
+
+ 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);
+ }
+ }
+}