/* Original author's notice is below. This class has been heavily modified.
*
* Copyright (C) 2012 0xlab - http://0xlab.org/
*
* 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.
*
* Authored by Julian Chu <walkingice AT 0xlab.org>
*/
package com.vaguehope.onosendai.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import com.vaguehope.onosendai.R;
public class SidebarLayout extends ViewGroup {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private static final int DEFAULT_SIDEBAR_WIDTH = 150;
private static final float SIDEBAR_MAX_WIDTH = 0.9f; // The max width of side bar is 90% of Parent.
private static final int SLIDE_DURATION = 200; // 0.2 seconds.
private static final double PROPORTION_THAT_COUNTS_AS_CLOSED = 0.25; // Consider closed if more that % dragged.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// properties.
private final int sidebarViewRes;
private final int hostViewRes;
private View sidebarView;
private View hostView;
private SidebarListener listener;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// state.
private boolean sidebarOpen;
private int sidebarWidth = DEFAULT_SIDEBAR_WIDTH; // assign default value. It will be overwrite in onMeasure by Layout XML resource.
private OpenListener openListener;
private CloseListener closeListener;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public SidebarLayout (final Context context) {
this(context, null);
}
public SidebarLayout (final Context context, final AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SidebarLayout);
this.hostViewRes = a.getResourceId(R.styleable.SidebarLayout_hostView, -1);
this.sidebarViewRes = a.getResourceId(R.styleable.SidebarLayout_sidebarView, -1);
a.recycle();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public View getHostView () {
if (this.hostView == null && this.hostViewRes > 0) {
this.hostView = findViewById(this.hostViewRes);
}
if (this.hostView == null) throw new IllegalStateException("Host view is not set.");
return this.hostView;
}
public void setHostView (final View hostView) {
if (hostView == null) throw new IllegalArgumentException("Host view can not be null.");
this.hostView = hostView;
}
public View getSidebarView () {
if (this.sidebarView == null && this.sidebarViewRes > 0) {
this.sidebarView = findViewById(this.sidebarViewRes);
}
if (this.sidebarView == null) throw new IllegalStateException("Side bar view is not set.");
return this.sidebarView;
}
public void setSidebarView (final View sidebarView) {
if (sidebarView == null) throw new IllegalArgumentException("Side bar view can not be null.");
this.sidebarView = sidebarView;
}
public SidebarListener getListener () {
return this.listener;
}
public void setListener (final SidebarListener l) {
this.listener = l;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public boolean isOpen () {
return this.sidebarOpen;
}
protected boolean setOpen (final boolean open) {
if (this.sidebarOpen != open) {
this.sidebarOpen = open;
return true;
}
return false;
}
public void toggleSidebar () {
animateSidebar(!this.sidebarOpen);
}
protected void animateSidebar (final boolean gotoOpen) {
final View host = getHostView();
if (host.getAnimation() != null) return;
final float deltaX;
final Animation animation;
if (gotoOpen) {
deltaX = host.getTranslationX() > 0 ? -host.getTranslationX() : -this.sidebarWidth;
animation = new TranslateAnimation(0, deltaX, 0, 0);
animation.setAnimationListener(this.openListener);
}
else {
deltaX = this.sidebarWidth - host.getTranslationX();
animation = new TranslateAnimation(0, deltaX, 0, 0);
animation.setAnimationListener(this.closeListener);
}
animation.setDuration((long) (SLIDE_DURATION * (Math.abs(deltaX) / this.sidebarWidth)));
animation.setFillAfter(true);
animation.setFillEnabled(true);
host.startAnimation(animation);
}
public boolean openSidebar () {
if (!this.sidebarOpen) {
toggleSidebar();
return true;
}
return false;
}
public boolean closeSidebar () {
if (this.sidebarOpen) {
toggleSidebar();
return true;
}
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@Override
public void onFinishInflate () {
super.onFinishInflate();
this.openListener = new OpenListener(this);
this.closeListener = new CloseListener(this);
this.touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public void onLayout (final boolean changed, final int l, final int t, final int r, final int b) {
// The title bar assign top padding, drop it.
this.getSidebarView().layout(r - this.sidebarWidth, 0, r, 0 + this.getSidebarView().getMeasuredHeight());
if (this.sidebarOpen) {
this.getHostView().layout(l - this.sidebarWidth, 0, r - this.sidebarWidth, b);
}
else {
this.getHostView().layout(l, 0, r, b);
}
}
@Override
public void onMeasure (final int w, final int h) {
super.onMeasure(w, h);
super.measureChildren(w, h);
this.sidebarWidth = this.getSidebarView().getMeasuredWidth();
}
@Override
protected void measureChild (final View child, final int parentWSpec, final int parentHSpec) {
if (child == this.getSidebarView()) {
final int mode = MeasureSpec.getMode(parentWSpec);
final int width = (int) (getMeasuredWidth() * SIDEBAR_MAX_WIDTH);
super.measureChild(child, MeasureSpec.makeMeasureSpec(width, mode), parentHSpec);
}
else {
super.measureChild(child, parentWSpec, parentHSpec);
}
}
private boolean intercepting = false;
private int touchSlop;
private float touchStartX;
private float touchStartY;
private boolean dragging = false;
private boolean clicking = false;
@Override
public boolean onInterceptTouchEvent (final MotionEvent ev) {
if (!isOpen()) return false;
if (!eventWithinView(ev, getHostView())) return false;
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
this.intercepting = false;
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
if (this.intercepting) return true;
if (eventWithinView(ev, getHostView())) {
this.intercepting = true;
return true;
}
break;
default:
}
return false;
}
@Override
public boolean onTouchEvent (final MotionEvent ev) {
switch (MotionEventCompat.getActionMasked(ev)) {
case MotionEvent.ACTION_DOWN:
this.clicking = true;
this.dragging = false;
this.touchStartX = ev.getX();
this.touchStartY = ev.getY();
return true;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
if (!this.dragging && (
Math.abs(ev.getX() - this.touchStartX) > this.touchSlop
|| Math.abs(ev.getY() - this.touchStartY) > this.touchSlop)) {
this.clicking = false;
this.dragging = true;
}
if (this.dragging) {
final float x = ev.getX() - this.touchStartX - this.sidebarWidth;
if (x >= -this.sidebarWidth && x <= 0) this.getHostView().setX(x);
}
return true;
case MotionEvent.ACTION_UP:
if (this.clicking) {
closeSidebar();
return true;
}
case MotionEvent.ACTION_CANCEL:
if (ev.getX() >= (this.sidebarWidth * PROPORTION_THAT_COUNTS_AS_CLOSED)) {
closeSidebar();
}
else {
animateSidebar(true);
}
getParent().requestDisallowInterceptTouchEvent(false);
return true;
default:
}
return false;
}
private static boolean eventWithinView (final MotionEvent ev, final View view) {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
return view.getLeft() < x && view.getRight() > x && view.getTop() < y && view.getBottom() > y;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private static class OpenListener implements Animation.AnimationListener {
private final SidebarLayout sidebarLayout;
public OpenListener (final SidebarLayout sidebarLayout) {
this.sidebarLayout = sidebarLayout;
}
@Override
public void onAnimationStart (final Animation a) {
this.sidebarLayout.getSidebarView().setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd (final Animation a) {
this.sidebarLayout.getHostView().clearAnimation();
this.sidebarLayout.getHostView().setTranslationX(0); // Clear offset from manual drag.
final boolean stateChanged = this.sidebarLayout.setOpen(true);
this.sidebarLayout.requestLayout();
if (stateChanged) {
final SidebarListener l = this.sidebarLayout.getListener();
if (l != null) l.onSidebarOpened(this.sidebarLayout);
}
}
@Override
public void onAnimationRepeat (final Animation a) {/* Unused. */}
}
private static class CloseListener implements Animation.AnimationListener {
private final SidebarLayout sidebarLayout;
public CloseListener (final SidebarLayout sidebarLayout) {
this.sidebarLayout = sidebarLayout;
}
@Override
public void onAnimationEnd (final Animation a) {
this.sidebarLayout.getHostView().clearAnimation();
this.sidebarLayout.getHostView().setTranslationX(0); // Clear offset from manual drag.
this.sidebarLayout.getSidebarView().setVisibility(View.INVISIBLE);
final boolean stateChanged = this.sidebarLayout.setOpen(false);
this.sidebarLayout.requestLayout();
if (stateChanged) {
final SidebarListener l = this.sidebarLayout.getListener();
if (l != null) l.onSidebarClosed(this.sidebarLayout);
}
}
@Override
public void onAnimationRepeat (final Animation a) {/* Unused. */}
@Override
public void onAnimationStart (final Animation a) {/* Unused. */}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public interface SidebarListener {
void onSidebarOpened (SidebarLayout sidebar);
void onSidebarClosed (SidebarLayout sidebar);
}
public static class ToggleSidebarListener implements OnClickListener {
private final SidebarLayout sidebar;
public ToggleSidebarListener (final SidebarLayout sidebar) {
this.sidebar = sidebar;
}
@Override
public void onClick (final View v) {
this.sidebar.toggleSidebar();
}
}
public static class BackButtonListener implements OnKeyListener {
private final SidebarLayout sidebar;
public BackButtonListener (final SidebarLayout sidebar) {
this.sidebar = sidebar;
}
@Override
public boolean onKey (final View v, final int keyCode, final KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
return this.sidebar.closeSidebar();
}
return false;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
}