/*
* Android Split Pane Layout.
* https://github.com/MobiDevelop/android-split-pane-layout
*
* Copyright (C) 2012 Justin Shapcott
*
* 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.mobidevelop.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PaintDrawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.ViewGroup;
/**
* A layout that splits the available space between two child views.
*
* An optionally movable bar exists between the children which allows the user
* to redistribute the space allocated to each view.
*/
public class SplitPaneLayout extends ViewGroup {
public static final int ORIENTATION_HORIZONTAL = 0;
public static final int ORIENTATION_VERTICAL = 1;
private int mOrientation = 0;
private int mSplitterSize = 12;
private boolean mSplitterMovable = true;
private int mSplitterPosition = Integer.MIN_VALUE;
private float mSplitterPositionPercent = 0.5f;
private Drawable mSplitterDrawable;
private Drawable mSplitterDraggingDrawable;
private Rect mSplitterRect = new Rect();
private int lastX;
private int lastY;
private Rect temp = new Rect();
private boolean isDragging = false;
public SplitPaneLayout(Context context) {
super(context);
mSplitterPositionPercent = 0.5f;
mSplitterDrawable = new PaintDrawable(0x88FFFFFF);
mSplitterDraggingDrawable = new PaintDrawable(0x88FFFFFF);
}
public SplitPaneLayout(Context context, AttributeSet attrs) {
super(context, attrs);
extractAttributes(context, attrs);
}
public SplitPaneLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
extractAttributes(context, attrs);
}
private void extractAttributes(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SplitPaneLayout);
mOrientation = a.getInt(R.styleable.SplitPaneLayout_orientation, 0);
mSplitterSize = a.getDimensionPixelSize(R.styleable.SplitPaneLayout_splitterSize, context.getResources().getDimensionPixelSize(R.dimen.splitpanelayout_default_splitter_size));
mSplitterMovable = a.getBoolean(R.styleable.SplitPaneLayout_splitterMovable, true);
TypedValue value = a.peekValue(R.styleable.SplitPaneLayout_splitterPosition);
if (value != null) {
if (value.type == TypedValue.TYPE_DIMENSION) {
mSplitterPosition = a.getDimensionPixelSize(R.styleable.SplitPaneLayout_splitterPosition, Integer.MIN_VALUE);
} else if (value.type == TypedValue.TYPE_FRACTION) {
mSplitterPositionPercent = a.getFraction(R.styleable.SplitPaneLayout_splitterPosition, 100, 100, 50) * 0.01f;
}
} else {
mSplitterPosition = Integer.MIN_VALUE;
mSplitterPositionPercent = 0.5f;
}
value = a.peekValue(R.styleable.SplitPaneLayout_splitterBackground);
if (value != null) {
if (value.type == TypedValue.TYPE_REFERENCE ||
value.type == TypedValue.TYPE_STRING) {
mSplitterDrawable = a.getDrawable(R.styleable.SplitPaneLayout_splitterBackground);
}
else
if (value.type == TypedValue.TYPE_INT_COLOR_ARGB8 ||
value.type == TypedValue.TYPE_INT_COLOR_ARGB4 ||
value.type == TypedValue.TYPE_INT_COLOR_RGB8 ||
value.type == TypedValue.TYPE_INT_COLOR_RGB4) {
mSplitterDrawable = new PaintDrawable(a.getColor(R.styleable.SplitPaneLayout_splitterBackground, 0xFF000000));
}
}
value = a.peekValue(R.styleable.SplitPaneLayout_splitterDraggingBackground);
if (value != null) {
if (value.type == TypedValue.TYPE_REFERENCE ||
value.type == TypedValue.TYPE_STRING) {
mSplitterDraggingDrawable = a.getDrawable(R.styleable.SplitPaneLayout_splitterDraggingBackground);
}
else
if (value.type == TypedValue.TYPE_INT_COLOR_ARGB8 ||
value.type == TypedValue.TYPE_INT_COLOR_ARGB4 ||
value.type == TypedValue.TYPE_INT_COLOR_RGB8 ||
value.type == TypedValue.TYPE_INT_COLOR_RGB4) {
mSplitterDraggingDrawable = new PaintDrawable(a.getColor(R.styleable.SplitPaneLayout_splitterDraggingBackground, 0x88FFFFFF));
}
} else {
mSplitterDraggingDrawable = new PaintDrawable(0x88FFFFFF);
}
a.recycle();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
check();
if (widthSize > 0 && heightSize > 0) {
switch (mOrientation) {
case 0: {
if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent < 0) {
mSplitterPosition = widthSize / 2;
} else if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent >= 0) {
mSplitterPosition = (int) (widthSize * mSplitterPositionPercent);
} else if (mSplitterPosition != Integer.MIN_VALUE && mSplitterPositionPercent < 0) {
mSplitterPositionPercent = (float) mSplitterPosition / (float) widthSize;
}
getChildAt(0).measure(MeasureSpec.makeMeasureSpec(mSplitterPosition - (mSplitterSize / 2), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
getChildAt(1).measure(MeasureSpec.makeMeasureSpec(widthSize - (mSplitterSize / 2) - mSplitterPosition, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
break;
}
case 1: {
if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent < 0) {
mSplitterPosition = heightSize / 2;
} else if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent >= 0) {
mSplitterPosition = (int) (heightSize * mSplitterPositionPercent);
} else if (mSplitterPosition != Integer.MIN_VALUE && mSplitterPositionPercent < 0) {
mSplitterPositionPercent = (float) mSplitterPosition / (float) heightSize;
}
getChildAt(0).measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mSplitterPosition - (mSplitterSize / 2), MeasureSpec.EXACTLY));
getChildAt(1).measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize - (mSplitterSize / 2) - mSplitterPosition, MeasureSpec.EXACTLY));
break;
}
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int w = r - l;
int h = b - t;
switch (mOrientation) {
case 0: {
getChildAt(0).layout(0, 0, mSplitterPosition - (mSplitterSize / 2), h);
mSplitterRect.set(mSplitterPosition - (mSplitterSize / 2), 0, mSplitterPosition + (mSplitterSize / 2), h);
getChildAt(1).layout(mSplitterPosition + (mSplitterSize / 2), 0, r, h);
break;
}
case 1: {
getChildAt(0).layout(0, 0, w, mSplitterPosition - (mSplitterSize / 2));
mSplitterRect.set(0, mSplitterPosition - (mSplitterSize / 2), w, mSplitterPosition + (mSplitterSize / 2));
getChildAt(1).layout(0, mSplitterPosition + (mSplitterSize / 2), w, h);
break;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mSplitterMovable) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (mSplitterRect.contains(x,y)) {
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
isDragging = true;
temp.set(mSplitterRect);
invalidate(temp);
lastX = x;
lastY = y;
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (isDragging) {
switch (mOrientation) {
case ORIENTATION_HORIZONTAL: {
temp.offset((x - lastX), 0);
break;
}
case ORIENTATION_VERTICAL: {
temp.offset(0, (int) (y - lastY));
break;
}
}
lastX = x;
lastY = y;
invalidate();
}
break;
}
case MotionEvent.ACTION_UP: {
if (isDragging) {
isDragging = false;
switch (mOrientation) {
case ORIENTATION_HORIZONTAL: {
mSplitterPosition = x;
break;
}
case ORIENTATION_VERTICAL: {
mSplitterPosition = y;
break;
}
}
mSplitterPositionPercent = -1;
remeasure();
requestLayout();
}
break;
}
}
return true;
}
return false;
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.mSplitterPositionPercent = mSplitterPositionPercent;
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
setSplitterPositionPercent(ss.mSplitterPositionPercent);
}
/**
* Convenience for calling own measure method.
*/
private void remeasure() {
measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY)
);
}
/**
* Checks that we have exactly two children.
*/
private void check() {
if (getChildCount() != 2) {
throw new RuntimeException("SplitPaneLayout must have exactly two child views.");
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mSplitterDrawable != null) {
mSplitterDrawable.setBounds(mSplitterRect);
mSplitterDrawable.draw(canvas);
}
if (isDragging) {
mSplitterDraggingDrawable.setBounds(temp);
mSplitterDraggingDrawable.draw(canvas);
}
}
/**
* Gets the current drawable used for the splitter.
*
* @return the drawable used for the splitter
*/
public Drawable getSplitterDrawable() {
return mSplitterDrawable;
}
/**
* Sets the drawable used for the splitter.
*
* @param splitterDrawable the desired orientation of the layout
*/
public void setSplitterDrawable(Drawable splitterDrawable) {
mSplitterDrawable = splitterDrawable;
if (getChildCount() == 2) {
remeasure();
}
}
/**
* Gets the current drawable used for the splitter dragging overlay.
*
* @return the drawable used for the splitter
*/
public Drawable getSplitterDraggingDrawable() {
return mSplitterDraggingDrawable;
}
/**
* Sets the drawable used for the splitter dragging overlay.
*
* @param splitterDrawable the desired orientation of the layout
*/
public void setSplitterDraggingDrawable(Drawable splitterDraggingDrawable) {
mSplitterDraggingDrawable = splitterDraggingDrawable;
if (isDragging) {
invalidate();
}
}
/**
* Gets the current orientation of the layout.
*
* @return the orientation of the layout
*/
public int getOrientation() {
return mOrientation;
}
/**
* Sets the orientation of the layout.
*
* @param orientation the desired orientation of the layout
*/
public void setOrientation(int orientation) {
if (mOrientation != orientation) {
mOrientation = orientation;
if (getChildCount() == 2) {
remeasure();
}
}
}
/**
* Gets the current size of the splitter in pixels.
*
* @return the size of the splitter
*/
public int getSplitterSize() {
return mSplitterSize;
}
/**
* Sets the current size of the splitter in pixels.
*
* @param splitterSize the desired size of the splitter
*/
public void setSplitterSize(int splitterSize) {
mSplitterSize = splitterSize;
if (getChildCount() == 2) {
remeasure();
}
}
/**
* Gets whether the splitter is movable by the user.
*
* @return whether the splitter is movable
*/
public boolean isSplitterMovable() {
return mSplitterMovable;
}
/**
* Sets whether the splitter is movable by the user.
*
* @param splitterMovable whether the splitter is movable
*/
public void setSplitterMovable(boolean splitterMovable) {
mSplitterMovable = splitterMovable;
}
/**
* Gets the current position of the splitter in pixels.
*
* @return the position of the splitter
*/
public int getSplitterPosition() {
return mSplitterPosition;
}
/**
* Sets the current position of the splitter in pixels.
*
* @param position the desired position of the splitter
*/
public void setSplitterPosition(int position) {
if (position < 0) {
position = 0;
}
mSplitterPosition = position;
mSplitterPositionPercent = -1;
remeasure();
}
/**
* Gets the current position of the splitter as a percent.
*
* @return the position of the splitter
*/
public float getSplitterPositionPercent() {
return mSplitterPositionPercent;
}
/**
* Sets the current position of the splitter as a percentage of the layout.
*
* @param position the desired position of the splitter
*/
public void setSplitterPositionPercent(float position) {
if (position < 0) {
position = 0;
}
if (position > 1) {
position = 1;
}
mSplitterPosition = Integer.MIN_VALUE;
mSplitterPositionPercent = position;
remeasure();
}
/**
* Holds important values when we need to save instance state.
*/
public static class SavedState extends BaseSavedState {
float mSplitterPositionPercent;
SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeFloat(mSplitterPositionPercent);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private SavedState(Parcel in) {
super(in);
mSplitterPositionPercent = in.readFloat();
}
}
}