diff --git a/build.gradle b/build.gradle index 43c0708..bd0b3eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - + ext.kotlin_version = '1.3.70' + repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' - + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.android.tools.build:gradle:3.6.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties index 9fa0cd3..36924eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,14 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + GROUP=com.mobidevelop.spl VERSION_NAME=1.0.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0664622..a830f45 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jul 31 20:52:57 MST 2018 +#Tue Mar 03 18:55:52 CET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/split-pane-layout-demo/build.gradle b/split-pane-layout-demo/build.gradle index 3e4aa9d..4fc2c26 100644 --- a/split-pane-layout-demo/build.gradle +++ b/split-pane-layout-demo/build.gradle @@ -1,15 +1,18 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { - compileSdkVersion 26 + compileSdkVersion 29 defaultConfig { applicationId "com.mobidevelop.spl.demo" minSdkVersion 16 - targetSdkVersion 26 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -22,5 +25,5 @@ android { dependencies { implementation project(':split-pane-layout') - implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'androidx.appcompat:appcompat:1.1.0' } diff --git a/split-pane-layout-demo/src/main/java/com/mobidevelop/spl/demo/MainActivity.java b/split-pane-layout-demo/src/main/java/com/mobidevelop/spl/demo/MainActivity.java deleted file mode 100644 index 623084e..0000000 --- a/split-pane-layout-demo/src/main/java/com/mobidevelop/spl/demo/MainActivity.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.mobidevelop.spl.demo; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import com.mobidevelop.spl.widget.SplitPaneLayout; - -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.Locale; - -public class MainActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - final TextView first = findViewById(R.id.first); - final TextView second = findViewById(R.id.second); - - final SplitPaneLayout layout = findViewById(R.id.layout); - layout.setOnSplitterPositionChangedListener(new SplitPaneLayout.OnSplitterPositionChangedListener() { - @Override - public void onSplitterPositionChanged(SplitPaneLayout splitPaneLayout, boolean fromUser) { - NumberFormat percent = DecimalFormat.getPercentInstance(Locale.getDefault()); - - first.setText(percent.format(layout.getSplitterPositionPercent())); - second.setText(percent.format(1f - layout.getSplitterPositionPercent())); - } - }); - layout.post(new Runnable() { - @Override - public void run() { - NumberFormat percent = DecimalFormat.getPercentInstance(Locale.getDefault()); - - first.setText(percent.format(layout.getSplitterPositionPercent())); - second.setText(percent.format(1f - layout.getSplitterPositionPercent())); - - } - }); - - } - -} diff --git a/split-pane-layout-demo/src/main/java/com/mobidevelop/spl/demo/MainActivity.kt b/split-pane-layout-demo/src/main/java/com/mobidevelop/spl/demo/MainActivity.kt new file mode 100644 index 0000000..e244e43 --- /dev/null +++ b/split-pane-layout-demo/src/main/java/com/mobidevelop/spl/demo/MainActivity.kt @@ -0,0 +1,32 @@ +package com.mobidevelop.spl.demo + +import android.app.Activity +import android.os.Bundle +import com.mobidevelop.spl.widget.SplitPaneLayout +import com.mobidevelop.spl.widget.SplitPaneLayout.OnSplitterPositionChangedListener +import kotlinx.android.synthetic.main.activity_main.* +import java.text.DecimalFormat +import java.util.* + +class MainActivity : Activity(), OnSplitterPositionChangedListener { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + layout.onSplitterPositionChangedListener = this + } + + override fun onResume() { + super.onResume() + updateViews() + } + + override fun onSplitterPositionChanged(splitPaneLayout: SplitPaneLayout, fromUser: Boolean) { + updateViews() + } + + private fun updateViews() { + val percent = DecimalFormat.getPercentInstance(Locale.getDefault()) + first.text = percent.format(layout.splitterPositionPercent.toDouble()) + second.text = percent.format(1f - layout.splitterPositionPercent.toDouble()) + } +} \ No newline at end of file diff --git a/split-pane-layout-demo/src/main/res/layout-land/activity_main.xml b/split-pane-layout-demo/src/main/res/layout-land/activity_main.xml index 2156e45..ed7c720 100644 --- a/split-pane-layout-demo/src/main/res/layout-land/activity_main.xml +++ b/split-pane-layout-demo/src/main/res/layout-land/activity_main.xml @@ -1,28 +1,31 @@ - - + spl:splitterPosition="33%" + spl:splitterSize="4dip" + spl:splitterTouchSlop="48dp"> \ No newline at end of file diff --git a/split-pane-layout-demo/src/main/res/layout/activity_main.xml b/split-pane-layout-demo/src/main/res/layout/activity_main.xml index be908c8..61ebc4b 100644 --- a/split-pane-layout-demo/src/main/res/layout/activity_main.xml +++ b/split-pane-layout-demo/src/main/res/layout/activity_main.xml @@ -1,28 +1,31 @@ - - + spl:splitterPosition="50%" + spl:splitterSize="4dip" + spl:splitterTouchSlop="48dp"> \ No newline at end of file diff --git a/split-pane-layout/build.gradle b/split-pane-layout/build.gradle index 574f80b..d09cab5 100644 --- a/split-pane-layout/build.gradle +++ b/split-pane-layout/build.gradle @@ -1,15 +1,18 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { - compileSdkVersion 26 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 - targetSdkVersion 26 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/split-pane-layout/src/main/java/com/mobidevelop/spl/widget/SplitPaneLayout.java b/split-pane-layout/src/main/java/com/mobidevelop/spl/widget/SplitPaneLayout.java deleted file mode 100644 index 85c0cc1..0000000 --- a/split-pane-layout/src/main/java/com/mobidevelop/spl/widget/SplitPaneLayout.java +++ /dev/null @@ -1,736 +0,0 @@ -/* - * 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.spl.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.Log; -import android.util.TypedValue; -import android.view.HapticFeedbackConstants; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.ViewConfiguration; -import android.view.ViewGroup; - -import com.mobidevelop.spl.R; - -/** - * 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 interface OnSplitterPositionChangedListener { - void onSplitterPositionChanged(SplitPaneLayout splitPaneLayout, boolean fromUser); - } - - public static final int ORIENTATION_HORIZONTAL = 0; - public static final int ORIENTATION_VERTICAL = 1; - - private int mOrientation = 0; - private int mSplitterSize = 8; - private boolean mSplitterMovable = true; - private int mSplitterPosition = Integer.MIN_VALUE; - private float mSplitterPositionPercent = 0.5f; - private int mSplitterTouchSlop = 0; - - private int mPaneSizeMin = 0; - - private Drawable mSplitterDrawable; - private Drawable mSplitterDraggingDrawable; - - private Rect mSplitterBounds = new Rect(); - private Rect mSplitterTouchBounds = new Rect(); - private Rect mSplitterDraggingBounds = new Rect(); - - private OnSplitterPositionChangedListener mOnSplitterPositionChangedListener; - - private int lastTouchX; - private int lastTouchY; - - private boolean isDragging = false; - private boolean isMovingSplitter = false; - - private boolean isMeasured = 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); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - setFocusable(true); - setFocusableInTouchMode(false); - } - - public SplitPaneLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - extractAttributes(context, attrs); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - setFocusable(true); - setFocusableInTouchMode(false); - } - - 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.spl_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); - } - mSplitterTouchSlop = a.getDimensionPixelSize(R.styleable.SplitPaneLayout_splitterTouchSlop, ViewConfiguration.get(context).getScaledTouchSlop()); - mPaneSizeMin = a.getDimensionPixelSize(R.styleable.SplitPaneLayout_paneSizeMin, 0); - a.recycle(); - } - } - - private void computeSplitterPosition() { - - int measuredWidth = getMeasuredWidth(); - int measuredHeight = getMeasuredHeight(); - - if (measuredWidth > 0 && measuredHeight > 0) { - switch (mOrientation) { - case ORIENTATION_HORIZONTAL: { - if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent < 0) { - mSplitterPosition = measuredWidth / 2; - } else if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent >= 0) { - mSplitterPosition = (int) (measuredWidth * mSplitterPositionPercent); - if (!between(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition())) { - mSplitterPosition = clamp(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = (float) mSplitterPosition / (float) measuredWidth; - } - } else if (mSplitterPosition != Integer.MIN_VALUE && mSplitterPositionPercent < 0) { - if (!between(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition())) { - mSplitterPosition = clamp(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition()); - } - mSplitterPositionPercent = (float) mSplitterPosition / (float) measuredWidth; - } - mSplitterBounds.set(mSplitterPosition - (mSplitterSize / 2), 0, mSplitterPosition + (mSplitterSize / 2), measuredHeight); - mSplitterTouchBounds.set(mSplitterBounds.left - mSplitterTouchSlop, mSplitterBounds.top, mSplitterBounds.right + mSplitterTouchSlop, mSplitterBounds.bottom); - break; - } - case ORIENTATION_VERTICAL: { - if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent < 0) { - mSplitterPosition = measuredHeight / 2; - } else if (mSplitterPosition == Integer.MIN_VALUE && mSplitterPositionPercent >= 0) { - mSplitterPosition = (int) (measuredHeight * mSplitterPositionPercent); - if (!between(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition())) { - mSplitterPosition = clamp(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = (float) mSplitterPosition / (float) measuredHeight; - } - } else if (mSplitterPosition != Integer.MIN_VALUE && mSplitterPositionPercent < 0) { - if (!between(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition())) { - mSplitterPosition = clamp(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition()); - } - mSplitterPositionPercent = (float) mSplitterPosition / (float) measuredHeight; - } - mSplitterBounds.set(0, mSplitterPosition - (mSplitterSize / 2), measuredWidth, mSplitterPosition + (mSplitterSize / 2)); - mSplitterTouchBounds.set(mSplitterBounds.left, mSplitterBounds.top - mSplitterTouchSlop / 2, mSplitterBounds.right, mSplitterBounds.bottom + mSplitterTouchSlop / 2); - break; - } - } - } - - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int measuredWidth = getMeasuredWidth(); - int measuredHeight = getMeasuredHeight(); - - check(); - - if (measuredWidth > 0 && measuredHeight > 0) { - - computeSplitterPosition(); - - switch (mOrientation) { - case ORIENTATION_HORIZONTAL: { - getChildAt(0).measure(MeasureSpec.makeMeasureSpec(mSplitterPosition - (mSplitterSize / 2), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)); - getChildAt(1).measure(MeasureSpec.makeMeasureSpec(measuredWidth - (mSplitterSize / 2) - mSplitterPosition, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)); - break; - } - case ORIENTATION_VERTICAL: { - getChildAt(0).measure(MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mSplitterPosition - (mSplitterSize / 2), MeasureSpec.EXACTLY)); - getChildAt(1).measure(MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(measuredHeight - (mSplitterSize / 2) - mSplitterPosition, MeasureSpec.EXACTLY)); - break; - } - } - - isMeasured = true; - } - } - - @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 ORIENTATION_HORIZONTAL: { - getChildAt(0).layout(0, 0, mSplitterPosition - (mSplitterSize / 2), h); - getChildAt(1).layout(mSplitterPosition + (mSplitterSize / 2), 0, r, h); - break; - } - case ORIENTATION_VERTICAL: { - getChildAt(0).layout(0, 0, w, mSplitterPosition - (mSplitterSize / 2)); - getChildAt(1).layout(0, mSplitterPosition + (mSplitterSize / 2), w, h); - break; - } - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - boolean remeasure = false; - int offset = mSplitterSize; - if (event.isShiftPressed()) { - offset *= 5; - } - switch (mOrientation) { - case ORIENTATION_HORIZONTAL: - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { - mSplitterPosition = clamp(mSplitterPosition - offset, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = -1; - remeasure = true; - } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - mSplitterPosition = clamp(mSplitterPosition + offset, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = -1; - remeasure = true; - } - break; - case ORIENTATION_VERTICAL: - if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { - mSplitterPosition = clamp(mSplitterPosition - offset, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = -1; - remeasure = true; - } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - mSplitterPosition = clamp(mSplitterPosition + offset, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = -1; - remeasure = true; - } - break; - } - if (remeasure) { - remeasure(); - notifySplitterPositionChanged(true); - return true; - } - return super.onKeyDown(keyCode, event); - } - - @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: - handleTouchDown(x, y); - break; - case MotionEvent.ACTION_MOVE: - handleTouchMove(x, y); - break; - case MotionEvent.ACTION_UP: - handleTouchUp(x, y); - break; - } - return true; - } - return false; - } - - private void handleTouchDown(int x, int y) { - if (mSplitterTouchBounds.contains(x, y)) { - performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - isDragging = true; - mSplitterDraggingBounds.set(mSplitterBounds); - invalidate(mSplitterDraggingBounds); - lastTouchX = x; - lastTouchY = y; - } - } - - private void handleTouchMove(int x, int y) { - if (isDragging) { - if (!isMovingSplitter) { - // Verify we've moved far enough to leave the touch bounds before moving the splitter - if (mSplitterTouchBounds.contains(x, y)) { - return; - } else { - isMovingSplitter = true; - } - } - boolean take = true; - switch (mOrientation) { - case ORIENTATION_HORIZONTAL: { - mSplitterDraggingBounds.offset(x - lastTouchX, 0); - if (mSplitterDraggingBounds.centerX() < getMinSplitterPosition()) { - take = false; - mSplitterDraggingBounds.offset(getMinSplitterPosition() - mSplitterDraggingBounds.centerX(), 0); - } - if (mSplitterDraggingBounds.centerX() > getMaxSplitterPosition()) { - take = false; - mSplitterDraggingBounds.offset(getMaxSplitterPosition() - mSplitterDraggingBounds.centerX(), 0); - } - break; - } - case ORIENTATION_VERTICAL: { - mSplitterDraggingBounds.offset(0, y - lastTouchY); - if (mSplitterDraggingBounds.centerY() < getMinSplitterPosition()) { - take = false; - mSplitterDraggingBounds.offset(0, getMinSplitterPosition() - mSplitterDraggingBounds.centerY()); - } - if (mSplitterDraggingBounds.centerY() > getMaxSplitterPosition()) { - take = false; - mSplitterDraggingBounds.offset(0, getMaxSplitterPosition() - mSplitterDraggingBounds.centerY()); - } - break; - } - } - if (take) { - lastTouchX = x; - lastTouchY = y; - } - invalidate(); - } - } - - private void handleTouchUp(int x, int y) { - if (isDragging) { - isDragging = false; - isMovingSplitter = false; - switch (mOrientation) { - case ORIENTATION_HORIZONTAL: { - mSplitterPosition = clamp(x, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = -1; - break; - } - case ORIENTATION_VERTICAL: { - mSplitterPosition = clamp(y, getMinSplitterPosition(), getMaxSplitterPosition()); - mSplitterPositionPercent = -1; - break; - } - } - remeasure(); - notifySplitterPositionChanged(true); - } - } - - private int getMinSplitterPosition() { - return mPaneSizeMin; - } - - private int getMaxSplitterPosition() { - switch (mOrientation) { - case ORIENTATION_HORIZONTAL: - return getMeasuredWidth() - mPaneSizeMin; - case ORIENTATION_VERTICAL: - return getMeasuredHeight() - mPaneSizeMin; - } - return 0; - } - - @Override - public Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - SavedState ss = new SavedState(superState); - ss.mSplitterPositionPercent = mSplitterPositionPercent; - return ss; - } - - @Override - public 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() { - // TODO: Performance: Guard against calling too often, can it be done without requestLayout? - forceLayout(); - measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY) - ); - requestLayout(); - } - - /** - * Checks that we have exactly two children. - */ - private void check() { - if (getChildCount() != 2) { - throw new RuntimeException("SplitPaneLayout must have exactly two child views."); - } - } - - private void enforcePaneSizeMin() { - - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - if (mSplitterDrawable != null) { - mSplitterDrawable.setState(getDrawableState()); - mSplitterDrawable.setBounds(mSplitterBounds); - mSplitterDrawable.draw(canvas); - } - if (isDragging) { - mSplitterDraggingDrawable.setState(getDrawableState()); - mSplitterDraggingDrawable.setBounds(mSplitterDraggingBounds); - 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 splitterDraggingDrawable the drawable to use while dragging the splitter - */ - 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) { - mSplitterPosition = clamp(position, 0, Integer.MAX_VALUE); - mSplitterPositionPercent = -1; - remeasure(); - notifySplitterPositionChanged(false); - } - - /** - * 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) { - mSplitterPosition = Integer.MIN_VALUE; - mSplitterPositionPercent = clamp(position, 0, 1); - remeasure(); - notifySplitterPositionChanged(false); - } - - /** - * Gets the current "touch slop" which is used to extends the grab size of the splitter - * and requires the splitter to be dragged at least this far to be considered a move. - * - * @return the current "touch slop" of the splitter - */ - public int getSplitterTouchSlop() { - return mSplitterTouchSlop; - } - - /** - * Sets the current "touch slop" which is used to extends the grab size of the splitter - * and requires the splitter to be dragged at least this far to be considered a move. - * - * @param splitterTouchSlop the desired "touch slop" of the splitter - */ - public void setSplitterTouchSlop(int splitterTouchSlop) { - this.mSplitterTouchSlop = splitterTouchSlop; - computeSplitterPosition(); - } - - /** - * Gets the minimum size of panes, in pixels. - * - * @return the minimum size of panes, in pixels. - */ - public int getPaneSizeMin() { - return mPaneSizeMin; - } - - /** - * Sets the minimum size of panes, in pixels. - * - * @param paneSizeMin the minimum size of panes, in pixels - */ - public void setPaneSizeMin(int paneSizeMin) { - mPaneSizeMin = paneSizeMin; - if (isMeasured) { - int newSplitterPosition = clamp(mSplitterPosition, getMinSplitterPosition(), getMaxSplitterPosition()); - if(newSplitterPosition != mSplitterPosition) { - setSplitterPosition(newSplitterPosition); - } - } - } - - /** - * Gets the OnSplitterPositionChangedListener to receive callbacks when the splitter position is changed - * - * @return the OnSplitterPositionChangedListener to receive callbacks when the splitter position is changed - */ - public OnSplitterPositionChangedListener getOnSplitterPositionChangedListener() { - return mOnSplitterPositionChangedListener; - } - - /** - * Sets the OnSplitterPositionChangedListener to receive callbacks when the splitter position is changed - * - * @param l the OnSplitterPositionChangedListener to receive callbacks when the splitter position is changed - */ - public void setOnSplitterPositionChangedListener(OnSplitterPositionChangedListener l) { - this.mOnSplitterPositionChangedListener = l; - } - - private void notifySplitterPositionChanged(boolean fromUser) { - if (mOnSplitterPositionChangedListener != null) { - Log.d("SPL", "Splitter Position Changed"); - mOnSplitterPositionChangedListener.onSplitterPositionChanged(this, fromUser); - } - } - - private static float clamp(float value, float min, float max) { - if (value < min) { - return min; - } else if (value > max) { - return max; - } - return value; - } - - private static int clamp(int value, int min, int max) { - if (value < min) { - return min; - } else if (value > max) { - return max; - } - return value; - } - - private static boolean between(int value, int min, int max) { - return min <= value && value <= max; - } - - /** - * Holds important values when we need to save instance state. - */ - public static class SavedState extends BaseSavedState { - public static final Creator CREATOR = new Creator() { - - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - - }; - - float mSplitterPositionPercent; - - SavedState(Parcelable superState) { - super(superState); - } - - private SavedState(Parcel in) { - super(in); - mSplitterPositionPercent = in.readFloat(); - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeFloat(mSplitterPositionPercent); - } - } - -} \ No newline at end of file diff --git a/split-pane-layout/src/main/java/com/mobidevelop/spl/widget/SplitPaneLayout.kt b/split-pane-layout/src/main/java/com/mobidevelop/spl/widget/SplitPaneLayout.kt new file mode 100644 index 0000000..8580c67 --- /dev/null +++ b/split-pane-layout/src/main/java/com/mobidevelop/spl/widget/SplitPaneLayout.kt @@ -0,0 +1,597 @@ +/* + * 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.spl.widget + +import android.content.Context +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.Log +import android.util.TypedValue +import android.view.* +import com.mobidevelop.spl.R + +/** + * 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. + */ +class SplitPaneLayout : ViewGroup { + interface OnSplitterPositionChangedListener { + fun onSplitterPositionChanged(splitPaneLayout: SplitPaneLayout, fromUser: Boolean) + } + + private var mOrientation = DEFAULT_ORIENTATION + private var mSplitterSize = 8 + private var mSplitterPosition = Int.MIN_VALUE + private var mSplitterPositionPercent = DEFAULT_POSITION_PERCENT + private var mSplitterTouchSlop = 0 + private var minSplitterPosition = 0 + get() { + return if (field < paneSizeMin) paneSizeMin else field + } + private var mSplitterDrawable: Drawable = DEFAULT_DRAWABLE + private var mSplitterDraggingDrawable: Drawable = DEFAULT_DRAWABLE + private val mSplitterBounds = Rect() + private val mSplitterTouchBounds = Rect() + private val mSplitterDraggingBounds = Rect() + private var lastTouchX = 0 + private var lastTouchY = 0 + private var isDragging = false + private var isMovingSplitter = false + private var isMeasured = false + + constructor(context: Context) : super(context) { + init(null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init(attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : + super(context, attrs, defStyle) { + init(attrs) + } + + private fun init(attrs: AttributeSet?) { + extractAttributes(context, attrs) + descendantFocusability = FOCUS_AFTER_DESCENDANTS + isFocusable = true + isFocusableInTouchMode = false + } + + private fun extractAttributes(context: Context, attrs: AttributeSet?) { + if (attrs == null) return + + val a = context.obtainStyledAttributes(attrs, R.styleable.SplitPaneLayout) + mOrientation = a.getInt(R.styleable.SplitPaneLayout_orientation, ORIENTATION_HORIZONTAL) + mSplitterSize = a.getDimensionPixelSize( + R.styleable.SplitPaneLayout_splitterSize, + context.resources.getDimensionPixelSize(R.dimen.spl_default_splitter_size) + ) + isSplitterMovable = a.getBoolean(R.styleable.SplitPaneLayout_splitterMovable, true) + a.peekValue(R.styleable.SplitPaneLayout_splitterPosition)?.let { + when (it.type) { + TypedValue.TYPE_DIMENSION -> { + mSplitterPosition = a.getDimensionPixelSize(R.styleable.SplitPaneLayout_splitterPosition, Int.MIN_VALUE) + } + TypedValue.TYPE_FRACTION -> { + val fraction = a.getFraction(R.styleable.SplitPaneLayout_splitterPosition, 100, 100, 50f) + mSplitterPositionPercent = fraction * 0.01f + } + } + } + mSplitterDrawable = a.peekValue(R.styleable.SplitPaneLayout_splitterBackground)?.let { + when (it.type) { + TypedValue.TYPE_REFERENCE, + TypedValue.TYPE_STRING -> + a.getDrawable(R.styleable.SplitPaneLayout_splitterBackground) + TypedValue.TYPE_INT_COLOR_ARGB8, + TypedValue.TYPE_INT_COLOR_ARGB4, + TypedValue.TYPE_INT_COLOR_RGB8, + TypedValue.TYPE_INT_COLOR_RGB4 -> { + val backgroundColor = a.getColor(R.styleable.SplitPaneLayout_splitterBackground, DEFAULT_SPLITTER_COLOR) + PaintDrawable(backgroundColor) + } + else -> DEFAULT_DRAWABLE + } + } ?: DEFAULT_DRAWABLE + mSplitterDraggingDrawable = a.peekValue(R.styleable.SplitPaneLayout_splitterDraggingBackground)?.let { + when (it.type) { + TypedValue.TYPE_REFERENCE, + TypedValue.TYPE_STRING -> + a.getDrawable(R.styleable.SplitPaneLayout_splitterDraggingBackground) + TypedValue.TYPE_INT_COLOR_ARGB8, + TypedValue.TYPE_INT_COLOR_ARGB4, + TypedValue.TYPE_INT_COLOR_RGB8, + TypedValue.TYPE_INT_COLOR_RGB4 -> PaintDrawable( + a.getColor( + R.styleable.SplitPaneLayout_splitterDraggingBackground, + DEFAULT_DRAGGING_COLOR + ) + ) + else -> DEFAULT_DRAWABLE + } + } ?: DEFAULT_DRAWABLE + mSplitterTouchSlop = a.getDimensionPixelSize( + R.styleable.SplitPaneLayout_splitterTouchSlop, + ViewConfiguration.get(context).scaledTouchSlop + ) + minSplitterPosition = a.getDimensionPixelSize(R.styleable.SplitPaneLayout_paneSizeMin, 0) + a.recycle() + } + + private fun computeSplitterPosition() { + if (measuredWidth <= 0 || measuredHeight <= 0) return + + val measuredSize = + if (mOrientation == ORIENTATION_HORIZONTAL) measuredWidth else measuredHeight + + when { + mSplitterPosition == Int.MIN_VALUE && mSplitterPositionPercent < 0 -> + mSplitterPosition = measuredSize / 2 + mSplitterPosition == Int.MIN_VALUE && mSplitterPositionPercent >= 0 -> { + mSplitterPosition = (measuredSize * mSplitterPositionPercent).toInt() + if (!mSplitterPosition.between(minSplitterPosition, maxSplitterPosition) + ) { + mSplitterPosition = clamp(mSplitterPosition, minSplitterPosition, maxSplitterPosition) + mSplitterPositionPercent = relativePositionPercent(measuredSize) + } + } + mSplitterPosition != Int.MIN_VALUE && mSplitterPositionPercent < 0 -> { + if (!mSplitterPosition.between(minSplitterPosition, maxSplitterPosition) + ) { + mSplitterPosition = clamp(mSplitterPosition, minSplitterPosition, maxSplitterPosition) + } + mSplitterPositionPercent = relativePositionPercent(measuredSize) + } + } + + when (mOrientation) { + ORIENTATION_HORIZONTAL -> { + mSplitterBounds.set(mSplitterPosition - mSplitterSize / 2, 0, mSplitterPosition + mSplitterSize / 2, measuredHeight) + mSplitterTouchBounds.set(mSplitterBounds.left - mSplitterTouchSlop, mSplitterBounds.top, mSplitterBounds.right + mSplitterTouchSlop, mSplitterBounds.bottom) + } + ORIENTATION_VERTICAL -> { + mSplitterBounds.set(0, mSplitterPosition - mSplitterSize / 2, measuredWidth, mSplitterPosition + mSplitterSize / 2) + mSplitterTouchBounds.set(mSplitterBounds.left, mSplitterBounds.top - mSplitterTouchSlop / 2, mSplitterBounds.right, mSplitterBounds.bottom + mSplitterTouchSlop / 2) + } + } + } + + private fun relativePositionPercent(measuredSize: Int): Float = when { + mSplitterPosition <= mSplitterSize / 2 -> 0.0f + mSplitterPosition >= measuredSize - mSplitterSize / 2 -> 1.0f + else -> mSplitterPosition / measuredSize.toFloat() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + check() + if (measuredWidth <= 0 || measuredHeight <= 0) return + + mSplitterPosition = Int.MIN_VALUE + measure() + } + + private fun measure() { + if (measuredWidth <= 0 || measuredHeight <= 0) return + computeSplitterPosition() + when (mOrientation) { + ORIENTATION_HORIZONTAL -> { + getChildAt(0).measure( + MeasureSpec.makeMeasureSpec(mSplitterPosition - mSplitterSize / 2, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + getChildAt(1).measure( + MeasureSpec.makeMeasureSpec(measuredWidth - mSplitterSize / 2 - mSplitterPosition, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + } + ORIENTATION_VERTICAL -> { + getChildAt(0).measure( + MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mSplitterPosition - mSplitterSize / 2, MeasureSpec.EXACTLY) + ) + getChildAt(1).measure( + MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(measuredHeight - mSplitterSize / 2 - mSplitterPosition, MeasureSpec.EXACTLY) + ) + } + } + isMeasured = true + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + val width = right - left + val height = bottom - top + when (mOrientation) { + ORIENTATION_HORIZONTAL -> { + getChildAt(0).layout(0, 0, mSplitterPosition - mSplitterSize / 2, height) + getChildAt(1).layout(mSplitterPosition + mSplitterSize / 2, 0, right, height) + } + ORIENTATION_VERTICAL -> { + getChildAt(0).layout(0, 0, width, mSplitterPosition - mSplitterSize / 2) + getChildAt(1).layout(0, mSplitterPosition + mSplitterSize / 2, width, height) + } + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + var remeasure = false + var offset = mSplitterSize + if (event.isShiftPressed) { + offset *= 5 + } + when { + (mOrientation == ORIENTATION_HORIZONTAL && keyCode == KeyEvent.KEYCODE_DPAD_LEFT) || + (mOrientation == ORIENTATION_VERTICAL && keyCode == KeyEvent.KEYCODE_DPAD_UP) -> { + mSplitterPosition = clamp(mSplitterPosition - offset, minSplitterPosition, maxSplitterPosition) + mSplitterPositionPercent = -1f + remeasure = true + } + + (mOrientation == ORIENTATION_HORIZONTAL && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) || + (mOrientation == ORIENTATION_VERTICAL && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) -> { + mSplitterPosition = clamp(mSplitterPosition + offset, minSplitterPosition, maxSplitterPosition) + mSplitterPositionPercent = -1f + remeasure = true + } + } + if (remeasure) { + remeasure() + notifySplitterPositionChanged(true) + return true + } + return super.onKeyDown(keyCode, event) + } + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + if (isSplitterMovable) { + val x = event.x.toInt() + val y = event.y.toInt() + return if (event.action == MotionEvent.ACTION_DOWN) + (mSplitterTouchBounds.contains(x, y)) + else false + } + return false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (isSplitterMovable) { + val x = event.x.toInt() + val y = event.y.toInt() + when (event.action) { + MotionEvent.ACTION_DOWN -> handleTouchDown(x, y) + MotionEvent.ACTION_MOVE -> handleTouchMove(x, y) + MotionEvent.ACTION_UP -> handleTouchUp(x, y) + } + return true + } + return false + } + + private fun handleTouchDown(x: Int, y: Int) { + if (mSplitterTouchBounds.contains(x, y)) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + isDragging = true + mSplitterDraggingBounds.set(mSplitterBounds) + invalidate(mSplitterDraggingBounds) + lastTouchX = x + lastTouchY = y + } + } + + private fun handleTouchMove(x: Int, y: Int) { + if (isDragging) { + if (!isMovingSplitter) { + // Verify we've moved far enough to leave the touch bounds before moving the splitter + isMovingSplitter = if (mSplitterTouchBounds.contains(x, y)) { + return + } else { + true + } + } + var take = true + when (mOrientation) { + ORIENTATION_HORIZONTAL -> { + //regular X axis dragging + mSplitterDraggingBounds.offset(x - lastTouchX, 0) + //clip dragging splitter to left bound + if (mSplitterDraggingBounds.centerX() < minSplitterPosition) { + take = false + mSplitterDraggingBounds.offset(minSplitterPosition - mSplitterDraggingBounds.centerX(), 0) + } + //clip dragging splitter to right bound + if (mSplitterDraggingBounds.centerX() > maxSplitterPosition) { + take = false + mSplitterDraggingBounds.offset(maxSplitterPosition - mSplitterDraggingBounds.centerX(), 0) + } + } + ORIENTATION_VERTICAL -> { + //regular Y axis dragging + mSplitterDraggingBounds.offset(0, y - lastTouchY) + //clip dragging splitter to top bound + if (mSplitterDraggingBounds.centerY() < minSplitterPosition) { + take = false + mSplitterDraggingBounds.offset(0, minSplitterPosition - mSplitterDraggingBounds.centerY()) + } + //clip dragging splitter to bottom bound + if (mSplitterDraggingBounds.centerY() > maxSplitterPosition) { + take = false + mSplitterDraggingBounds.offset(0, maxSplitterPosition - mSplitterDraggingBounds.centerY()) + } + } + } + if (take) { + lastTouchX = x + lastTouchY = y + } + invalidate() + } + } + + private fun handleTouchUp(x: Int, y: Int) { + if (isDragging) { + isDragging = false + isMovingSplitter = false + val axis = when (mOrientation) { + ORIENTATION_HORIZONTAL -> x + //ORIENTATION_VERTICAL + else -> y + } + mSplitterPosition = clamp(axis, minSplitterPosition, maxSplitterPosition) + mSplitterPositionPercent = -1f + remeasure() + notifySplitterPositionChanged(true) + } + } + + private val maxSplitterPosition: Int + get() { + when (mOrientation) { + ORIENTATION_HORIZONTAL -> return measuredWidth - minSplitterPosition + ORIENTATION_VERTICAL -> return measuredHeight - minSplitterPosition + } + return 0 + } + + public override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + return SavedState(superState).apply { + mSplitterPositionPercent = this@SplitPaneLayout.mSplitterPositionPercent + } + } + + public override fun onRestoreInstanceState(state: Parcelable) { + if (state !is SavedState) { + super.onRestoreInstanceState(state) + return + } else { + super.onRestoreInstanceState(state.superState) + splitterPositionPercent = state.mSplitterPositionPercent + } + } + + /** + * Convenience for calling own measure method. + */ + private fun remeasure() { + // TODO: Performance: Guard against calling too often, can it be done without requestLayout? + forceLayout() + measure() + requestLayout() + } + + /** + * Checks that layout has exactly two children. + */ + private fun check() { + require(childCount == 2) { "SplitPaneLayout must have exactly two child views." } + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + mSplitterDrawable.state = drawableState + mSplitterDrawable.bounds = mSplitterBounds + mSplitterDrawable.draw(canvas) + if (isDragging) { + mSplitterDraggingDrawable.state = drawableState + mSplitterDraggingDrawable.bounds = mSplitterDraggingBounds + mSplitterDraggingDrawable.draw(canvas) + } + } + + /** + * Whether the splitter is movable by the user + */ + var isSplitterMovable = DEFAULT_IS_MOVABLE + + /** + * Listener to receive callbacks when the splitter position is changed + */ + var onSplitterPositionChangedListener: OnSplitterPositionChangedListener? = null + + /** + * Current drawable used for the splitter. + */ + var splitterDrawable: Drawable + get() = mSplitterDrawable + set(value) { + mSplitterDrawable = value + check() + remeasure() + } + + /** + * The current drawable to use while dragging the splitter. + */ + var splitterDraggingDrawable: Drawable + get() = mSplitterDraggingDrawable + set(value) { + mSplitterDraggingDrawable = value + if (isDragging) { + invalidate() + } + } + + /** + * The current orientation of the layout. + */ + var orientation: Int + get() = mOrientation + set(value) { + if (mOrientation != value) { + mOrientation = value + check() + remeasure() + } + } + + /** + * The current size of the splitter in pixels. + */ + var splitterSize: Int + get() = mSplitterSize + set(value) { + mSplitterSize = value + check() + remeasure() + } + + /** + * The current position of the splitter in pixels. + */ + var splitterPosition: Int + get() = mSplitterPosition + set(value) { + mSplitterPosition = clamp(value, 0, Int.MAX_VALUE) + mSplitterPositionPercent = -1f + remeasure() + notifySplitterPositionChanged(false) + } + + /** + * The current position of the splitter as a percentage of the layout. + */ + var splitterPositionPercent: Float + get() = mSplitterPositionPercent + set(value) { + mSplitterPosition = Int.MIN_VALUE + mSplitterPositionPercent = clamp(value, 0f, 1f) + remeasure() + notifySplitterPositionChanged(false) + } + + /** + * The current "touch slop" which is used to extends the grab size of the splitter + * and requires the splitter to be dragged at least this far to be considered a move. + */ + var splitterTouchSlop: Int + get() = mSplitterTouchSlop + set(value) { + mSplitterTouchSlop = value + computeSplitterPosition() + } + + /** + * Minimum size of panes, in pixels. + */ + var paneSizeMin: Int = DEFAULT_PANE_SIZE_MIN + set(value) { + field = value + if (isMeasured) { + val newSplitterPosition = clamp(mSplitterPosition, minSplitterPosition, maxSplitterPosition) + if (newSplitterPosition != mSplitterPosition) { + splitterPosition = newSplitterPosition + } + } + } + + private fun notifySplitterPositionChanged(fromUser: Boolean) { + Log.d("SPL", "Splitter Position Changed") + onSplitterPositionChangedListener?.onSplitterPositionChanged(this, fromUser) + } + + /** + * Holds important values when we need to save instance state. + */ + class SavedState : BaseSavedState { + var mSplitterPositionPercent = DEFAULT_POSITION_PERCENT + + internal constructor(superState: Parcelable?) : super(superState) + private constructor(parcel: Parcel) : super(parcel) { + mSplitterPositionPercent = parcel.readFloat() + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeFloat(mSplitterPositionPercent) + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + companion object { + const val ORIENTATION_HORIZONTAL = 0 + const val ORIENTATION_VERTICAL = 1 + const val DEFAULT_ORIENTATION = ORIENTATION_HORIZONTAL + const val DEFAULT_POSITION_PERCENT = 0.5f + const val DEFAULT_IS_MOVABLE = true + const val DEFAULT_DRAGGING_COLOR = -0x77000001 + const val DEFAULT_SPLITTER_COLOR = -0x1000000 + const val DEFAULT_PANE_SIZE_MIN = 0 + const val DEFAULT_SPLITTER_TOUCH_SLOP = 0 + val DEFAULT_DRAWABLE = PaintDrawable(DEFAULT_DRAGGING_COLOR) + private fun clamp(value: Float, min: Float, max: Float): Float { + return when { + value < min -> min + value > max -> max + else -> value + } + } + + private fun clamp(value: Int, min: Int, max: Int): Int { + return when { + value < min -> min + value > max -> max + else -> value + } + } + + private fun Int.between(min: Int, max: Int): Boolean { + return this in min..max + } + } +} \ No newline at end of file