package com.sage42.android.view.animations; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; import com.sage42.android.view.R; import com.sage42.android.view.ui.ExpandAndShrinkLayoutStates; /** * Copyright (C) 2013- Sage 42 App Sdn Bhd 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. * * @author Corey Scott ([email protected]) */ public class ClickToOpenCloseLayout extends LinearLayout { private static final int INITIAL_ANIMATION_DELAY = 100; private static final int DEFAULT_HEIGHT_CLOSED_DP = 50; private static final int DEFAULT_MAX_HEIGHT_OPENED_DP = 500; private static final int DEFAULT_INITIAL_ANIMATION_DURATION = 100; private static final int DEFAULT_ANIMATION_DURATION = 300; // unique index for this layout. Important for state preservations private long mUuid; // dimensions (in px) when closed protected int mHeightClosed; // max dimensions (in px) when opened protected int mMaxHeightOpen; // dimensions (in px) when open (default: 50dp) protected int mHeightOpen; // close animation on load (default: 100ms) protected long mInitialAnimationDuration; // open/close toggle animation duration (default: 300ms) protected long mAnimationDuration; private AnimateOpenAndClosed mCloseAnimation; private AnimateOpenAndClosed mOpenAnimation; private IClickToOpenCloseLayoutListener mListener; public ClickToOpenCloseLayout(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); this.init(context, attrs); } public ClickToOpenCloseLayout(final Context context, final AttributeSet attrs) { super(context, attrs); this.init(context, attrs); } private void init(final Context context, final AttributeSet attrs) { final TypedArray args = context.obtainStyledAttributes(attrs, R.styleable.clickToOpenClosedLayout); // extract closed height from attribs (or use default) and convert to px final int closedHeight = args.getInt(R.styleable.clickToOpenClosedLayout_closedHeight, ClickToOpenCloseLayout.DEFAULT_HEIGHT_CLOSED_DP); this.mHeightClosed = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, closedHeight, this .getResources().getDisplayMetrics()); // extract max open height from attribs (or use default) and convert to px final int maxHeightOpen = args.getInt(R.styleable.clickToOpenClosedLayout_maxCardHeight, ClickToOpenCloseLayout.DEFAULT_MAX_HEIGHT_OPENED_DP); this.mMaxHeightOpen = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, maxHeightOpen, this .getResources().getDisplayMetrics()); // extra initial close animation duration from attribs this.mInitialAnimationDuration = args.getInt(R.styleable.clickToOpenClosedLayout_initialAnimationDuration, ClickToOpenCloseLayout.DEFAULT_INITIAL_ANIMATION_DURATION); // extra open/close animation duration from attribs this.mAnimationDuration = args.getInt(R.styleable.clickToOpenClosedLayout_animationDuration, ClickToOpenCloseLayout.DEFAULT_ANIMATION_DURATION); args.recycle(); } public void onLayoutReuse(final long newIndex) { // Reset uuid this.mUuid = newIndex; // Allow Recalculation of Layout Height based on state this.mHeightOpen = 0; if (this.getListener() != null) { // Reset state this.getListener().onLayoutStateChange(newIndex, ExpandAndShrinkLayoutStates.INITIAL); } // Reset Animation if (this.mOpenAnimation != null) { this.mOpenAnimation.cancel(); this.mOpenAnimation = null; } if (this.mCloseAnimation != null) { this.mCloseAnimation.cancel(); this.mCloseAnimation = null; } this.requestLayout(); } protected void requestShrink() { // Cache current UUID, State final long currentUuid = this.getUuid(); final ExpandAndShrinkLayoutStates currentState = this.getListener().getLayoutState(currentUuid); final int openHeight = this.mHeightOpen; final int closeHeight = this.mHeightClosed; if (this.getListener() == null) { return; } // animate closed (so that people know they can click to toggle it) this.postDelayed(new Runnable() { @Override public void run() { if (currentState == ExpandAndShrinkLayoutStates.OPEN || currentState == ExpandAndShrinkLayoutStates.INITIAL) { if (openHeight > closeHeight) { final AnimateOpenAndClosed animation = new AnimateOpenAndClosed(ClickToOpenCloseLayout.this, openHeight, closeHeight, false); animation.setDuration(ClickToOpenCloseLayout.this.mInitialAnimationDuration); ClickToOpenCloseLayout.this.startAnimation(animation); // Set State to closed ClickToOpenCloseLayout.this.getListener().onLayoutStateChange(currentUuid, ExpandAndShrinkLayoutStates.CLOSE); } } } }, ClickToOpenCloseLayout.INITIAL_ANIMATION_DELAY); } /** * Ask all children to measure themselves and compute the measurement of this * layout based on the children. */ @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { ExpandAndShrinkLayoutStates currentState = null; if (this.getListener() != null) { currentState = this.getListener().getLayoutState(this.getUuid()); } if (this.mHeightOpen == 0) { // Let Child decide how much space they actually need to show a fully opened card final int realHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(this.mMaxHeightOpen, View.MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, realHeightMeasureSpec); this.mHeightOpen = this.getMeasuredHeight(); // Re-init animation this.initAnimations(); // Restore current card height (Important after recycling) if (currentState == ExpandAndShrinkLayoutStates.INITIAL) { this.getLayoutParams().height = this.getMeasuredHeight(); // Initial pass. Request shrink this.requestShrink(); } else if (currentState == ExpandAndShrinkLayoutStates.OPEN) { this.getLayoutParams().height = this.getMeasuredHeight(); } else if (currentState == ExpandAndShrinkLayoutStates.CLOSE) { this.getLayoutParams().height = this.mHeightClosed; } } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) { super.onLayout(changed, left, top, right, bottom); } public void toggleDisplay() { if (!this.isAnimationInitialized() || this.mListener == null) { // Not Setup Yet return; } final long currentUuid = this.getUuid(); final ExpandAndShrinkLayoutStates currentState = this.mListener.getLayoutState(currentUuid); if (currentState == null) { return; } switch (currentState) { case OPEN: this.mListener.onLayoutStateChange(currentUuid, ExpandAndShrinkLayoutStates.CLOSE); this.startAnimation(this.mCloseAnimation); break; case CLOSE: this.mListener.onLayoutStateChange(currentUuid, ExpandAndShrinkLayoutStates.OPEN); this.startAnimation(this.mOpenAnimation); break; case INITIAL: this.requestShrink(); default: // Do nothing break; } } /** * Set the animation duration. This should normally be done in the XML as setting it here will cause the animations * to be re-defined. * * @param animationDuration * the animationDuration to set */ public void setAnimationDuration(final long animationDuration) { this.mAnimationDuration = animationDuration; // re-init the animations this.initAnimations(); } private void initAnimations() { // must be done here so that we have the open dimension // setup the open and close animations this.mCloseAnimation = new AnimateOpenAndClosed(this, this.mHeightOpen, this.mHeightClosed, false); this.mCloseAnimation.setDuration(this.mAnimationDuration); this.mOpenAnimation = new AnimateOpenAndClosed(this, this.mHeightOpen, this.mHeightClosed, true); this.mOpenAnimation.setDuration(this.mAnimationDuration); } private boolean isAnimationInitialized() { if (this.mCloseAnimation == null || this.mOpenAnimation == null) { return false; } return true; } public long getUuid() { return this.mUuid; } public void setUuid(final long uuid) { this.mUuid = uuid; } public IClickToOpenCloseLayoutListener getListener() { return this.mListener; } public void setListener(final IClickToOpenCloseLayoutListener listener) { this.mListener = listener; } public static interface IClickToOpenCloseLayoutListener { void onLayoutStateChange(final long index, final ExpandAndShrinkLayoutStates newState); ExpandAndShrinkLayoutStates getLayoutState(final long index); } }