summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoremancebo <emancebo@cyngn.com>2014-08-26 13:27:17 -0700
committeremancebo <emancebo@cyngn.com>2014-09-29 14:48:15 -0700
commit0888f8e20d475e03ea79f5d416f9089b479d447c (patch)
tree905c9534852d7a12418e4ee56029013ed45c2ddd
parent8be09ac4aebd03fce7071324e87b92a28978c0a6 (diff)
downloadandroid_external_cyanogen_UICommon-0888f8e20d475e03ea79f5d416f9089b479d447c.tar.gz
android_external_cyanogen_UICommon-0888f8e20d475e03ea79f5d416f9089b479d447c.tar.bz2
android_external_cyanogen_UICommon-0888f8e20d475e03ea79f5d416f9089b479d447c.zip
Add horizontal list scrubber to UICommon
* Add option to use either ListView or RecyclerView as the source * Add optional fade in/out behavior on list idle Change-Id: Idf2751c6201a23f4a25b3a1c55af13f60dd69793
-rw-r--r--res/drawable-nodpi/letter_indicator.9.pngbin0 -> 3614 bytes
-rw-r--r--res/drawable/scrubber_back.xml5
-rw-r--r--res/drawable/seek_back.xml7
-rw-r--r--res/layout/scrub_layout.xml87
-rw-r--r--src/com/cyngn/uicommon/view/ListScrubber.java233
-rw-r--r--src/com/cyngn/uicommon/view/ListScrubberFadeHelper.java152
-rw-r--r--src/com/cyngn/uicommon/view/ListScrubberSeekBar.java47
7 files changed, 531 insertions, 0 deletions
diff --git a/res/drawable-nodpi/letter_indicator.9.png b/res/drawable-nodpi/letter_indicator.9.png
new file mode 100644
index 0000000..af3578e
--- /dev/null
+++ b/res/drawable-nodpi/letter_indicator.9.png
Binary files differ
diff --git a/res/drawable/scrubber_back.xml b/res/drawable/scrubber_back.xml
new file mode 100644
index 0000000..8d2be51
--- /dev/null
+++ b/res/drawable/scrubber_back.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#99333333"/>
+ <corners android:radius="5dip"/>
+</shape>
diff --git a/res/drawable/seek_back.xml b/res/drawable/seek_back.xml
new file mode 100644
index 0000000..358c022
--- /dev/null
+++ b/res/drawable/seek_back.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="line">
+ <stroke android:width="4dp" android:color="#FFF"/>
+ <size android:height="2dp" />
+</shape>
diff --git a/res/layout/scrub_layout.xml b/res/layout/scrub_layout.xml
new file mode 100644
index 0000000..2f725da
--- /dev/null
+++ b/res/layout/scrub_layout.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/scrubberWidget"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_marginRight="20dp"
+ android:layout_marginLeft="20dp"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:clickable="true"
+ android:layout_marginTop="-5dp"
+ android:layout_below="@+id/scrubberIndicator"
+ android:orientation="horizontal"
+ android:background="@drawable/scrubber_back"
+ android:layout_width="match_parent"
+ android:layout_height="50dp">
+
+ <Space
+ android:layout_weight="0.1"
+ android:layout_width="0dp"
+ android:layout_height="match_parent" />
+
+ <TextView
+ android:id="@+id/firstSection"
+ android:gravity="center"
+ android:textColor="@android:color/white"
+ android:paddingRight="10dp"
+ android:textStyle="bold"
+ style="?android:attr/textAppearanceLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" />
+
+ <LinearLayout
+ android:layout_weight="0.9"
+ android:layout_width="0dp"
+ android:layout_gravity="center"
+ android:orientation="vertical"
+ android:layout_height="match_parent">
+
+ <com.cyngn.uicommon.view.ListScrubberSeekBar
+ android:id="@+id/scrubber"
+ android:paddingLeft="0dp"
+ android:paddingRight="0dp"
+ android:layout_marginRight="-10dp"
+ android:layout_marginLeft="-10dp"
+ android:thumb="@android:color/transparent"
+ android:thumbOffset="-10dp"
+ android:progressDrawable="@drawable/seek_back"
+ android:layout_width="match_parent"
+ android:layout_gravity="center"
+ android:layout_height="match_parent" />
+
+ </LinearLayout>
+
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/lastSection"
+ android:paddingLeft="10dp"
+ android:textColor="@android:color/white"
+ android:textStyle="bold"
+ style="?android:attr/textAppearanceLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" />
+
+ <Space
+ android:layout_weight="0.1"
+ android:layout_width="0dp"
+ android:layout_height="match_parent" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/scrubberIndicator"
+ android:background="@drawable/letter_indicator"
+ android:layout_width="80dp"
+ android:textSize="30sp"
+ android:gravity="center"
+ android:textColor="@android:color/white"
+ android:clickable="false"
+ android:layout_marginBottom="-20dp"
+ android:visibility="invisible"
+ android:layout_height="100dp" />
+
+</RelativeLayout>
diff --git a/src/com/cyngn/uicommon/view/ListScrubber.java b/src/com/cyngn/uicommon/view/ListScrubber.java
new file mode 100644
index 0000000..f4e9596
--- /dev/null
+++ b/src/com/cyngn/uicommon/view/ListScrubber.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2014 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.cyngn.uicommon.view;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AbsListView;
+import android.widget.HeaderViewListAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.SectionIndexer;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import com.cyngn.uicommon.R;
+
+/**
+ * Widget to fast scroll through a list view with a horizontal scrubber.
+ * Usage:
+ * scrubber = new ListScrubber(context);
+ * scrubber.setFadeInOnMotion(true); // optional
+ * scrubber.setSource(listView); // call when adapter is ready
+ */
+public class ListScrubber extends LinearLayout implements OnClickListener {
+
+ private SectionIndexer mSectionIndexer;
+ private RelativeLayout mScrubberWidget;
+ private ListView mListView;
+ private RecyclerView mRecyclerView;
+ private TextView mFirstIndicator, mLastIndicator;
+ private TextView mScrubberIndicator;
+ private SeekBar mSeekBar;
+ private String[] mSections;
+ private int mHeaderCount = 0;
+ private boolean mFadeInOnMotion;
+ private ListScrubberFadeHelper mFadeHelper;
+
+ public ListScrubber(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.scrub_layout, this);
+ mScrubberWidget = (RelativeLayout)findViewById(R.id.scrubberWidget);
+ mFirstIndicator = ((TextView) findViewById(R.id.firstSection));
+ mFirstIndicator.setOnClickListener(this);
+ mLastIndicator = ((TextView) findViewById(R.id.lastSection));
+ mLastIndicator.setOnClickListener(this);
+ mScrubberIndicator = (TextView) findViewById(R.id.scrubberIndicator);
+ mSeekBar = (SeekBar) findViewById(R.id.scrubber);
+ init();
+ }
+
+ public void updateSections() {
+ mSections = (String[]) mSectionIndexer.getSections();
+ if (mSections.length > 0) {
+ mSeekBar.setMax(mSections.length - 1);
+ mFirstIndicator.setText(mSections[0]);
+ mLastIndicator.setText(mSections[mSections.length - 1]);
+ }
+ }
+
+ public void setSource(ListView listView) {
+ resetSource();
+ mListView = listView;
+
+ ListAdapter adapter = listView.getAdapter();
+ if (adapter instanceof HeaderViewListAdapter) {
+ mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
+ adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
+ }
+
+ if (adapter instanceof SectionIndexer) {
+ mSectionIndexer = (SectionIndexer)adapter;
+ } else {
+ throw new IllegalArgumentException("ListView adapter must implement SectionIndexer");
+ }
+ }
+
+ public void setSource(RecyclerView recyclerView) {
+ resetSource();
+ mRecyclerView = recyclerView;
+ mSectionIndexer = (SectionIndexer)mRecyclerView.getAdapter();
+
+ }
+
+ private void resetSource() {
+ mRecyclerView = null;
+ mListView = null;
+ }
+
+ private boolean isReady() {
+ return (mListView != null || mRecyclerView != null) &&
+ mSectionIndexer != null &&
+ mSections != null;
+ }
+
+ private void init() {
+ mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, final int progress, boolean fromUser) {
+ if (!isReady()) {
+ return;
+ }
+ resetScrubber();
+ mScrubberIndicator.setTranslationX((progress * seekBar.getWidth()) / mSections.length);
+ String section = String.valueOf(mSections[progress]);
+ scrollToPositionWithOffset(mSectionIndexer.getPositionForSection(progress)
+ + mHeaderCount, 0);
+ mScrubberIndicator.setText(section);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (!isReady()) {
+ return;
+ }
+ resetScrubber();
+ mScrubberIndicator.setAlpha(1f);
+ mScrubberIndicator.setVisibility(View.VISIBLE);
+ if (mFadeInOnMotion) {
+ mFadeHelper.onEvent(ListScrubberFadeHelper.Event.SCRUBBER_TOUCH);
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (!isReady()) {
+ return;
+ }
+ resetScrubber();
+ mScrubberIndicator.animate().alpha(0f).translationYBy(20f)
+ .setDuration(200).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mScrubberIndicator.setVisibility(View.INVISIBLE);
+ }
+ });
+ if (mFadeInOnMotion) {
+ mFadeHelper.onEvent(ListScrubberFadeHelper.Event.SCRUBBER_RELEASE);
+ }
+ }
+
+ private void resetScrubber() {
+ mScrubberIndicator.animate().cancel();
+ mScrubberIndicator.setTranslationY(0f);
+ }
+ });
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mFirstIndicator) {
+ int positionForFirstSection = mSectionIndexer.getPositionForSection(0);
+ scrollToPositionWithOffset(positionForFirstSection, 0);
+ } else if (v == mLastIndicator) {
+ int positionForLastSection = mSectionIndexer.getPositionForSection(mSections.length - 1);
+ scrollToPositionWithOffset(positionForLastSection, 0);
+ }
+ }
+
+ /**
+ * If set to true, the scrubber appears once the list is scrolled and disappears when
+ * the list is idle for some predetermined amount of time.
+ *
+ * Defaults to false
+ *
+ * @param fadeInOnMotion true to enable fading behavior
+ */
+ public void setFadeInOnMotion(boolean fadeInOnMotion) {
+ if (fadeInOnMotion) {
+ mFadeHelper = new ListScrubberFadeHelper(mScrubberWidget);
+ mFadeHelper.updateState(ListScrubberFadeHelper.State.HIDDEN);
+ }
+ else {
+ mFadeHelper = null;
+ }
+ mFadeInOnMotion = fadeInOnMotion;
+ }
+
+ public boolean isFadeInOnMotion() {
+ return mFadeInOnMotion;
+ }
+
+ private void scrollToPositionWithOffset(int position, int y) {
+ if (mListView != null) {
+ mListView.setSelectionFromTop(position, y);
+ }
+ else if (mRecyclerView != null) {
+ LinearLayoutManager layoutManager =
+ (LinearLayoutManager)mRecyclerView.getLayoutManager();
+ layoutManager.scrollToPositionWithOffset(position, y);
+ }
+ }
+
+ public void handleScrollStateChanged(int scrollState) {
+ if (!mFadeInOnMotion)
+ return;
+
+ switch (scrollState) {
+ case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
+ mFadeHelper.onEvent(ListScrubberFadeHelper.Event.LIST_SCROLL);
+ break;
+ case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
+ mFadeHelper.onEvent(ListScrubberFadeHelper.Event.LIST_FLING);
+ break;
+ case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
+ mFadeHelper.onEvent(ListScrubberFadeHelper.Event.LIST_IDLE);
+ break;
+ }
+ }
+
+ public void setHeaderCount(int count) {
+ mHeaderCount = count;
+ }
+} \ No newline at end of file
diff --git a/src/com/cyngn/uicommon/view/ListScrubberFadeHelper.java b/src/com/cyngn/uicommon/view/ListScrubberFadeHelper.java
new file mode 100644
index 0000000..7b20c5c
--- /dev/null
+++ b/src/com/cyngn/uicommon/view/ListScrubberFadeHelper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2014 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.cyngn.uicommon.view;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.os.Handler;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * State machine to manage fading the list scrubber in and out when the user interacts
+ * with the list view or the scrubber itself.
+ */
+public class ListScrubberFadeHelper {
+
+ private static final int FADE_OUT_DELAY = 2000;
+
+ public static enum State {
+ VISIBLE,
+ HIDDEN,
+ FADING_IN,
+ FADING_OUT,
+ FADE_OUT_SCHEDULED,
+ };
+
+ public static enum Event {
+ LIST_FLING,
+ LIST_SCROLL,
+ LIST_IDLE,
+ LIST_IDLE_TIMEOUT,
+ SCRUBBER_TOUCH,
+ SCRUBBER_RELEASE,
+ }
+
+ private ViewGroup mScrubber;
+ private Handler mHandler;
+ private Runnable mFadeOutRunnable;
+ private State mState;
+
+ class FadeOutRunnable implements Runnable {
+ @Override
+ public void run() {
+ onEvent(Event.LIST_IDLE_TIMEOUT);
+ }
+ }
+
+ public ListScrubberFadeHelper(ViewGroup scrubber) {
+ mScrubber = scrubber;
+ mHandler = new Handler();
+ }
+
+ public void onEvent(Event event) {
+ switch (event) {
+ case LIST_FLING:
+ case LIST_SCROLL:
+ if (mState == State.FADING_OUT || mState == State.FADE_OUT_SCHEDULED) {
+ cancelFade();
+ updateState(State.VISIBLE);
+ } else if (mState == State.HIDDEN) {
+ updateState(State.FADING_IN);
+ }
+ break;
+
+ case LIST_IDLE:
+ updateState(State.FADE_OUT_SCHEDULED);
+ break;
+
+ case LIST_IDLE_TIMEOUT:
+ updateState(State.FADING_OUT);
+ break;
+
+ case SCRUBBER_TOUCH:
+ if (mState == State.FADING_OUT || mState == State.FADE_OUT_SCHEDULED) {
+ cancelFade();
+ }
+ break;
+
+ case SCRUBBER_RELEASE:
+ updateState(State.FADE_OUT_SCHEDULED);
+ break;
+ }
+ }
+
+ public void updateState(State state) {
+ mState = state;
+ switch (state) {
+ case VISIBLE:
+ mScrubber.setAlpha(1f);
+ mScrubber.setVisibility(View.VISIBLE);
+ break;
+ case HIDDEN:
+ mScrubber.setAlpha(0f);
+ mScrubber.setVisibility(View.INVISIBLE);
+ break;
+ case FADING_IN:
+ mScrubber.animate().alpha(1f).setDuration(200)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // strange that this is required...
+ updateState(State.VISIBLE);
+ }
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ updateState(State.VISIBLE);
+ }
+ });
+ break;
+ case FADING_OUT:
+ mScrubber.animate().alpha(0f).setDuration(200)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // strange that this is required...
+ updateState(State.HIDDEN);
+ }
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ updateState(State.HIDDEN);
+ }
+ });
+ break;
+ case FADE_OUT_SCHEDULED:
+ mFadeOutRunnable = new FadeOutRunnable();
+ mHandler.postDelayed(mFadeOutRunnable, FADE_OUT_DELAY);
+ break;
+ }
+ }
+
+ private void cancelFade() {
+ if (mFadeOutRunnable != null) {
+ mHandler.removeCallbacks(mFadeOutRunnable);
+ mFadeOutRunnable = null;
+ }
+ mScrubber.animate().cancel();
+ }
+}
diff --git a/src/com/cyngn/uicommon/view/ListScrubberSeekBar.java b/src/com/cyngn/uicommon/view/ListScrubberSeekBar.java
new file mode 100644
index 0000000..83ce176
--- /dev/null
+++ b/src/com/cyngn/uicommon/view/ListScrubberSeekBar.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.cyngn.uicommon.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+/**
+ * Override SeekBar for use in list scrubber
+ */
+public class ListScrubberSeekBar extends SeekBar {
+
+ public ListScrubberSeekBar(Context context) {
+ super(context);
+ }
+
+ public ListScrubberSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ListScrubberSeekBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean isInScrollingContainer() {
+ // For the list scrubber seek bar we can simplify things by assuming this
+ // is false. This would only backfire if we attempt to put a list scrubber
+ // inside a list item.
+ return false;
+ }
+}