Java tutorial
/* 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; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }