Skip to content

Instantly share code, notes, and snippets.

@ksoichiro
Forked from sw1ftc0d3r/SlidingUpBaseActivity.java
Last active August 29, 2015 14:12
Show Gist options
  • Save ksoichiro/5e20d7f3d27a025d53ae to your computer and use it in GitHub Desktop.
Save ksoichiro/5e20d7f3d27a025d53ae to your computer and use it in GitHub Desktop.
<!--
Copyright 2014 Soichiro Kashima
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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:orientation="vertical">
<!--
Dummy background contents.
You can replace this to map or something.
-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#E91E63" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#3F51B5" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#2196F3" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#03A9F4" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#8BC34A" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#FFEB3B" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#FFC107" />
<View
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#FF9800" />
</LinearLayout>
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="@dimen/flexible_space_image_height"
android:scaleType="centerCrop"
android:src="@drawable/example" />
<com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout
android:id="@+id/scroll_wrapper"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.ksoichiro.android.observablescrollview.ObservableScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/header_bar_height"
android:background="@android:color/white"
android:fillViewport="true">
<TextView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:text="@string/lipsum" />
</com.github.ksoichiro.android.observablescrollview.ObservableScrollView>
<FrameLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false">
<View
android:id="@+id/header_background"
android:layout_width="match_parent"
android:layout_height="@dimen/header_bar_height"
android:background="?attr/colorPrimary"
android:minHeight="@dimen/header_bar_height" />
<LinearLayout
android:id="@+id/header_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/header_bar_height"
android:background="@android:color/white"
android:minHeight="@dimen/header_bar_height"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingLeft="@dimen/margin_standard"
android:textColor="@android:color/black"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/header_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/header_flexible_space"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="?attr/colorPrimary" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/sliding_overlay_blur_size"
android:background="@drawable/sliding_header_overlay"
android:minHeight="@dimen/sliding_overlay_blur_size" />
</LinearLayout>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="center"
app:fab_colorNormal="@color/accentLight"
app:fab_colorPressed="@color/accent" />
</FrameLayout>
</com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_gravity="top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:popupTheme="@style/Theme.AppCompat.Light.DarkActionBar"
app:theme="@style/Toolbar" />
<TextView
android:id="@+id/toolbar_title"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingLeft="@dimen/toolbar_margin_start"
android:paddingRight="@dimen/margin_extra_short"
android:textColor="@android:color/white"
android:textSize="20sp" />
</FrameLayout>
<!--
Copyright 2014 Soichiro Kashima
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.
-->
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="margin_standard">16dp</dimen>
<dimen name="margin_short">8dp</dimen>
<dimen name="margin_extra_short">4dp</dimen>
<dimen name="toolbar_elevation">4dp</dimen>
<dimen name="toolbar_margin_start">72dp</dimen>
<dimen name="flexible_space_height">72dp</dimen>
<dimen name="flexible_space_image_height">240dp</dimen>
<dimen name="flexible_space_show_fab_offset">120dp</dimen>
<dimen name="parallax_image_height">180dp</dimen>
<dimen name="tab_height">48dp</dimen>
<dimen name="header_bar_height">72dp</dimen>
<dimen name="intersection_height">16dp</dimen>
<dimen name="sliding_slop">32dp</dimen>
<dimen name="sliding_overlay_blur_size">4dp</dimen>
</resources>
<!--
Copyright 2014 Soichiro Kashima
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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:endColor="#00009688"
android:startColor="#FF009688" />
</shape>
/*
* Copyright 2014 Soichiro Kashima
*
* 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.github.ksoichiro.android.observablescrollview.samples;
import android.annotation.TargetApi;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.widget.Toolbar;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks;
import com.github.ksoichiro.android.observablescrollview.ScrollState;
import com.github.ksoichiro.android.observablescrollview.Scrollable;
import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.view.ViewHelper;
import com.nineoldandroids.view.ViewPropertyAnimator;
public abstract class SlidingUpBaseActivity<S extends Scrollable> extends ActionBarActivity implements ObservableScrollViewCallbacks {
private View mHeader;
private View mHeaderBar;
private View mHeaderOverlay;
private View mHeaderFlexibleSpace;
private TextView mTitle;
private TextView mToolbarTitle;
private View mImageView;
private View mFab;
private Toolbar mToolbar;
private S mScrollable;
private TouchInterceptionFrameLayout mInterceptionLayout;
private int mActionBarSize;
private int mIntersectionHeight;
private int mHeaderBarHeight;
private int mSlidingSlop;
private int mSlidingHeaderBlueSize;
private float mScrollYOnDownMotion;
private boolean mMoved;
private float mInitialY;
private float mMovedDistanceY;
private int mFabMargin;
private boolean mFabIsShown;
private int mFlexibleSpaceImageHeight;
private int mToolbarColor;
private boolean mHeaderColorChanging;
private boolean mHeaderIsAtBottom;
private boolean mHeaderIsNotAtBottom;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutResId());
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
ViewHelper.setScaleY(mToolbar, 0);
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle("");
mToolbarColor = getResources().getColor(R.color.primary);
mToolbar.setBackgroundColor(Color.TRANSPARENT);
mToolbar.setTitle("");
mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height);
mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height);
mHeaderBarHeight = getResources().getDimensionPixelSize(R.dimen.header_bar_height);
mSlidingSlop = getResources().getDimensionPixelSize(R.dimen.sliding_slop);
mActionBarSize = getActionBarSize();
mSlidingHeaderBlueSize = getResources().getDimensionPixelSize(R.dimen.sliding_overlay_blur_size);
mHeader = findViewById(R.id.header);
mHeaderBar = findViewById(R.id.header_bar);
mHeaderOverlay = findViewById(R.id.header_overlay);
mHeaderFlexibleSpace = findViewById(R.id.header_flexible_space);
mImageView = findViewById(R.id.image);
mImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
slideOnClick();
}
});
mScrollable = createScrollable();
mFab = findViewById(R.id.fab);
mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard);
mFabIsShown = true;
mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper);
mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener);
mTitle = (TextView) findViewById(R.id.title);
mTitle.setText(getTitle());
mToolbarTitle = (TextView) findViewById(R.id.toolbar_title);
mToolbarTitle.setText(mTitle.getText());
ViewHelper.setAlpha(mToolbarTitle, 0);
ViewHelper.setTranslationY(mTitle, (mHeaderBarHeight - mActionBarSize) / 2);
ViewTreeObserver vto = mInterceptionLayout.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
mInterceptionLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
mInterceptionLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
ViewHelper.setTranslationY(mInterceptionLayout, getScreenHeight() - mHeaderBarHeight);
ViewHelper.setTranslationY(mImageView, getScreenHeight() - mHeaderBarHeight);
if (mFab != null) {
ViewHelper.setTranslationX(mFab, mTitle.getWidth() - mFabMargin - mFab.getWidth());
ViewHelper.setTranslationY(mFab, ViewHelper.getX(mTitle) - (mFab.getHeight() / 2));
}
changeHeaderBarColor();
changeHeaderOverlay();
}
});
}
protected abstract int getLayoutResId();
protected abstract S createScrollable();
@Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
}
@Override
public void onDownMotionEvent() {
}
@Override
public void onUpOrCancelMotionEvent(ScrollState scrollState) {
}
private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() {
@Override
public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) {
final int minInterceptionLayoutY = -mIntersectionHeight;
return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout)
|| (moving && mScrollable.getCurrentScrollY() - diffY < 0);
}
@Override
public void onDownMotionEvent(MotionEvent ev) {
mScrollYOnDownMotion = mScrollable.getCurrentScrollY();
mInitialY = ViewHelper.getTranslationY(mInterceptionLayout);
}
@Override
public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) {
mMoved = true;
float translationY = ViewHelper.getTranslationY(mInterceptionLayout) - mScrollYOnDownMotion + diffY;
if (translationY < -mIntersectionHeight) {
translationY = -mIntersectionHeight;
} else if (getScreenHeight() - mHeaderBarHeight < translationY) {
translationY = getScreenHeight() - mHeaderBarHeight;
}
slideTo(translationY);
mMovedDistanceY = ViewHelper.getTranslationY(mInterceptionLayout) - mInitialY;
}
@Override
public void onUpOrCancelMotionEvent(MotionEvent ev) {
if (!mMoved) {
// Invoke slide animation only on header view
Rect outRect = new Rect();
mHeader.getHitRect(outRect);
if (outRect.contains((int) ev.getX(), (int) ev.getY())) {
slideOnClick();
}
} else {
stickToAnchors();
}
mMoved = false;
}
};
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void changeHeaderBarColor() {
if (mHeaderColorChanging) {
return;
}
boolean shouldBeWhite = getAnchorYBottom() == ViewHelper.getTranslationY(mInterceptionLayout);
final int headerColorAtBottom = Color.WHITE;
int color = ((ColorDrawable) mHeaderBar.getBackground()).getColor();
if (!mHeaderIsAtBottom && color != headerColorAtBottom && shouldBeWhite) {
mHeaderIsAtBottom = true;
mHeaderIsNotAtBottom = false;
ValueAnimator animator = ValueAnimator.ofFloat(0, 1).setDuration(100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (float) animation.getAnimatedValue();
mHeaderColorChanging = (alpha != 1);
setBackgroundAlpha(mHeaderBar, alpha, headerColorAtBottom);
int level = Math.min(255, Math.max(0, (int) ((1 - alpha) * 255)));
mTitle.setTextColor(0xff000000 + (0x010101 * level));
}
});
animator.start();
} else if (!mHeaderIsNotAtBottom && !shouldBeWhite) {
mHeaderIsAtBottom = false;
mHeaderIsNotAtBottom = true;
ValueAnimator animator = ValueAnimator.ofFloat(1, 0).setDuration(100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (float) animation.getAnimatedValue();
mHeaderColorChanging = (alpha != 0);
setBackgroundAlpha(mHeaderBar, alpha, headerColorAtBottom);
int level = Math.min(255, Math.max(0, (int) ((1 - alpha) * 255)));
mTitle.setTextColor(0xff000000 + (0x010101 * level));
}
});
animator.start();
}
}
private void changeHeaderOverlay() {
final float translationY = ViewHelper.getTranslationY(mInterceptionLayout);
if (translationY <= mToolbar.getHeight() - mSlidingHeaderBlueSize) {
mHeaderOverlay.setVisibility(View.VISIBLE);
mHeaderFlexibleSpace.getLayoutParams().height = (int) (mToolbar.getHeight() - mSlidingHeaderBlueSize - translationY);
mHeaderFlexibleSpace.requestLayout();
mHeaderOverlay.requestLayout();
} else {
mHeaderOverlay.setVisibility(View.INVISIBLE);
}
}
private void slideOnClick() {
float translationY = ViewHelper.getTranslationY(mInterceptionLayout);
if (translationY == getAnchorYBottom()) {
slideWithAnimation(getAnchorYImage());
} else if (translationY == getAnchorYImage()) {
slideWithAnimation(getAnchorYBottom());
}
}
private void stickToAnchors() {
// Slide to some points automatically
if (0 < mMovedDistanceY) {
// Sliding down
if (mSlidingSlop < mMovedDistanceY) {
// Sliding down to an anchor
if (getAnchorYImage() < ViewHelper.getTranslationY(mInterceptionLayout)) {
slideWithAnimation(getAnchorYBottom());
} else {
slideWithAnimation(getAnchorYImage());
}
} else {
// Sliding up(back) to an anchor
if (getAnchorYImage() < ViewHelper.getTranslationY(mInterceptionLayout)) {
slideWithAnimation(getAnchorYImage());
} else {
slideWithAnimation(0);
}
}
} else if (mMovedDistanceY < 0) {
// Sliding up
if (mMovedDistanceY < -mSlidingSlop) {
// Sliding up to an anchor
if (getAnchorYImage() < ViewHelper.getTranslationY(mInterceptionLayout)) {
slideWithAnimation(getAnchorYImage());
} else {
slideWithAnimation(0);
}
} else {
// Sliding down(back) to an anchor
if (getAnchorYImage() < ViewHelper.getTranslationY(mInterceptionLayout)) {
slideWithAnimation(getAnchorYBottom());
} else {
slideWithAnimation(getAnchorYImage());
}
}
}
}
private void slideTo(float translationY) {
ViewHelper.setTranslationY(mInterceptionLayout, translationY);
if (translationY < 0) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams();
lp.height = (int) -translationY + getScreenHeight();
mInterceptionLayout.requestLayout();
}
// Translate title
float hiddenHeight = translationY < 0 ? -translationY : 0;
ViewHelper.setTranslationY(mTitle, Math.min(mIntersectionHeight, (mHeaderBarHeight + hiddenHeight - mActionBarSize) / 2));
// Translate image
float imageAnimatableHeight = getScreenHeight() - mHeaderBarHeight;
float imageTranslationScale = imageAnimatableHeight / (imageAnimatableHeight - mImageView.getHeight());
float imageTranslationY = Math.max(0, imageAnimatableHeight - (imageAnimatableHeight - translationY) * imageTranslationScale);
ViewHelper.setTranslationY(mImageView, imageTranslationY);
// Show/hide FAB
if (ViewHelper.getTranslationY(mInterceptionLayout) < mFlexibleSpaceImageHeight) {
hideFab();
} else {
ViewPropertyAnimator.animate(mToolbar).scaleY(0).setDuration(200).start();
showFab();
}
if (ViewHelper.getTranslationY(mInterceptionLayout) <= mFlexibleSpaceImageHeight) {
ViewPropertyAnimator.animate(mToolbar).scaleY(1).setDuration(200).start();
setBackgroundAlpha(mToolbar, 0, mToolbarColor);
}
if (ViewHelper.getTranslationY(mInterceptionLayout) <= mIntersectionHeight) {
if (ViewHelper.getAlpha(mToolbarTitle) == 0) {
ViewPropertyAnimator.animate(mToolbarTitle).cancel();
ViewPropertyAnimator.animate(mToolbarTitle).alpha(1).setDuration(200).start();
}
} else if (ViewHelper.getAlpha(mToolbarTitle) == 1) {
ViewPropertyAnimator.animate(mToolbarTitle).cancel();
ViewPropertyAnimator.animate(mToolbarTitle).alpha(0).setDuration(200).start();
} else {
ViewHelper.setAlpha(mToolbarTitle, 0);
}
changeHeaderBarColor();
changeHeaderOverlay();
}
private void slideWithAnimation(float toY) {
float layoutTranslationY = ViewHelper.getTranslationY(mInterceptionLayout);
if (layoutTranslationY != toY) {
ValueAnimator animator = ValueAnimator.ofFloat(ViewHelper.getTranslationY(mInterceptionLayout), toY).setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
slideTo((float) animation.getAnimatedValue());
}
});
animator.start();
}
}
private float getAnchorYBottom() {
return getScreenHeight() - mHeaderBarHeight;
}
private float getAnchorYImage() {
return mImageView.getHeight();
}
private int getActionBarSize() {
TypedValue typedValue = new TypedValue();
int[] textSizeAttr = new int[]{R.attr.actionBarSize};
int indexOfAttrTextSize = 0;
TypedArray a = obtainStyledAttributes(typedValue.data, textSizeAttr);
int actionBarSize = a.getDimensionPixelSize(indexOfAttrTextSize, -1);
a.recycle();
return actionBarSize;
}
private int getScreenHeight() {
return findViewById(android.R.id.content).getHeight();
}
private void showFab() {
if (!mFabIsShown && mFab != null) {
ViewPropertyAnimator.animate(mFab).cancel();
ViewPropertyAnimator.animate(mFab).scaleX(1).scaleY(1).setDuration(200).start();
mFabIsShown = true;
}
}
private void hideFab() {
if (mFabIsShown && mFab != null) {
ViewPropertyAnimator.animate(mFab).cancel();
ViewPropertyAnimator.animate(mFab).scaleX(0).scaleY(0).setDuration(200).start();
mFabIsShown = false;
}
}
private void setBackgroundAlpha(View view, float alpha, int baseColor) {
int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24;
int rgb = 0x00ffffff & baseColor;
view.setBackgroundColor(a + rgb);
}
}
@sw1ftc0d3r
Copy link

On line 151 the visibility is set to gone.. Remove the line or set to visible.

@ksoichiro
Copy link
Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment