From ba2ca6ecb6196984c64cb738fa8c31cce3c33019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com> Date: Mon, 4 Mar 2024 23:01:10 -0500 Subject: [PATCH] TwoPaneLayout: convert to kotlin Change-Id: I1cb1742773a133598a7fe291919a9b571b54d030 --- .../cx/ring/views/twopane/TwoPaneLayout.java | 885 ------------------ .../cx/ring/views/twopane/TwoPaneLayout.kt | 767 +++++++++++++++ 2 files changed, 767 insertions(+), 885 deletions(-) delete mode 100644 jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.java create mode 100644 jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.kt diff --git a/jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.java b/jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.java deleted file mode 100644 index ffe4de12f..000000000 --- a/jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.java +++ /dev/null @@ -1,885 +0,0 @@ -/* - * Copyright 2018 The Android Open Source Project - * - * 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 cx.ring.views.twopane; - -import android.app.Activity; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.os.Parcel; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.view.AccessibilityDelegateCompat; -import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; -import androidx.core.view.animation.PathInterpolatorCompat; -import androidx.customview.view.AbsSavedState; -import androidx.customview.widget.Openable; -import androidx.transition.ChangeBounds; -import androidx.transition.Transition; -import androidx.transition.TransitionManager; -import androidx.window.layout.FoldingFeature; -import androidx.window.layout.WindowInfoTracker; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; - -/** - * TwoPaneLayout provides a horizontal, multi-pane layout for use at the top level - * of a UI. A left (or start) pane is treated as a content list or browser, subordinate to a - * primary detail view for displaying content. - * - * <p>Child views overlap if their combined width exceeds the available width - * in the TwoPaneLayout. Each of child views is expand out to fill the available width in - * the TwoPaneLayout.</p> - * - * <p>Thanks to this behavior, TwoPaneLayout may be suitable for creating layouts - * that can smoothly adapt across many different screen sizes, expanding out fully on larger - * screens and collapsing on smaller screens.</p> - * - * <p>TwoPaneLayout is distinct from a navigation drawer as described in the design - * guide and should not be used in the same scenarios. TwoPaneLayout should be thought - * of only as a way to allow a two-pane layout normally used on larger screens to adapt to smaller - * screens in a natural way. The interaction patterns expressed by TwoPaneLayout imply - * a physicality and direct information hierarchy between panes that does not necessarily exist - * in a scenario where a navigation drawer should be used instead.</p> - * - * <p>Appropriate uses of TwoPaneLayout include pairings of panes such as a contact list and - * subordinate interactions with those contacts, or an email thread list with the content pane - * displaying the contents of the selected thread. Inappropriate uses of SlidingPaneLayout include - * switching between disparate functions of your app, such as jumping from a social stream view - * to a view of your personal profile - cases such as this should use the navigation drawer - * pattern instead. ({@link androidx.drawerlayout.widget.DrawerLayout DrawerLayout} implements - * this pattern.)</p> - * - * <p>Like {@link android.widget.LinearLayout LinearLayout}, TwoPaneLayout supports - * the use of the layout parameter <code>layout_weight</code> on child views to determine - * how to divide leftover space after measurement is complete. It is only relevant for width. - * When views do not overlap weight behaves as it does in a LinearLayout.</p> - */ -public class TwoPaneLayout extends ViewGroup implements Openable { - private static final String TAG = "TwoPaneLayout"; - - /** Class name may be obfuscated by Proguard. Hardcode the string for accessibility usage. */ - private static final String ACCESSIBILITY_CLASS_NAME = - "cx.ring.views.twopane.TwoPaneLayout"; - - /** - * True if a panel can slide with the current measurements - */ - private boolean mCanSlide; - - /** - * The child view that can slide, if any. - */ - private View mSlideableView; - - /** - * How far the panel is offset from its usual position. - * range [0, 1] where 0 = open, 1 = closed. - */ - private boolean isOpened = false; - - private final List<PanelListener> mPanelListeners = new CopyOnWriteArrayList<>(); - - /** - * Stores whether or not the pane was open the last time it was slideable. - * If open/close operations are invoked this state is modified. Used by - * instance state save/restore. - */ - private boolean mPreservedOpenState; - private boolean mFirstLayout = true; - - private final Rect mTmpRect = new Rect(); - - FoldingFeature mFoldingFeature; - - /** - * Listener for monitoring events about sliding panes. - */ - public interface PanelListener { - /** - * Called when a detail view becomes slid completely open. - * - * @param panel The detail view that was slid to an open position - */ - void onPanelOpened(@NonNull View panel); - - /** - * Called when a detail view becomes slid completely closed. - * - * @param panel The detail view that was slid to a closed position - */ - void onPanelClosed(@NonNull View panel); - } - - private final FoldingFeatureObserver.OnFoldingFeatureChangeListener mOnFoldingFeatureChangeListener = - new FoldingFeatureObserver.OnFoldingFeatureChangeListener() { - @Override - public void onFoldingFeatureChange(@NonNull FoldingFeature foldingFeature) { - mFoldingFeature = foldingFeature; - // Start transition animation when folding feature changed - Transition changeBounds = new ChangeBounds(); - changeBounds.setDuration(300L); - changeBounds.setInterpolator(PathInterpolatorCompat.create(0.2f, 0, 0, 1)); - TransitionManager.beginDelayedTransition(TwoPaneLayout.this, changeBounds); - requestLayout(); - } - }; - - private FoldingFeatureObserver mFoldingFeatureObserver; - - public TwoPaneLayout(@NonNull Context context) { - this(context, null); - } - - public TwoPaneLayout(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public TwoPaneLayout(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); - ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); - - WindowInfoTracker repo = WindowInfoTracker.getOrCreate(context); - Executor mainExecutor = ContextCompat.getMainExecutor(context); - mFoldingFeatureObserver = new FoldingFeatureObserver(repo, mainExecutor); - mFoldingFeatureObserver.setOnFoldingFeatureChangeListener(mOnFoldingFeatureChangeListener); - } - - /** - * Adds the specified listener to the list of listeners that will be notified of - * panel slide events. - * - * @param listener Listener to notify when panel slide events occur. - * @see #removePanelListener(PanelListener) - */ - public void addPanelListener(@NonNull PanelListener listener) { - mPanelListeners.add(listener); - } - - /** - * Removes the specified listener from the list of listeners that will be notified of - * panel slide events. - * - * @param listener Listener to remove from being notified of panel slide events - * @see #addPanelListener(PanelListener) - */ - public void removePanelListener(@NonNull PanelListener listener) { - mPanelListeners.remove(listener); - } - - void dispatchOnPanelOpened(@NonNull View panel) { - for (PanelListener listener : mPanelListeners) { - listener.onPanelOpened(panel); - } - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - } - - void dispatchOnPanelClosed(@NonNull View panel) { - for (PanelListener listener : mPanelListeners) { - listener.onPanelClosed(panel); - } - sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mFirstLayout = true; - if (mFoldingFeatureObserver != null) { - Activity activity = getActivityOrNull(getContext()); - if (activity != null) { - mFoldingFeatureObserver.registerLayoutStateChangeCallback(activity); - } - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mFirstLayout = true; - if (mFoldingFeatureObserver != null) { - mFoldingFeatureObserver.unregisterLayoutStateChangeCallback(); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - int layoutHeight = 0; - int maxLayoutHeight = 0; - switch (heightMode) { - case MeasureSpec.EXACTLY: - layoutHeight = maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); - break; - case MeasureSpec.AT_MOST: - maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); - break; - } - - float weightSum = 0; - boolean canSlide = false; - final int widthAvailable = Math.max(widthSize - getPaddingLeft() - getPaddingRight(), 0); - int widthRemaining = widthAvailable; - final int childCount = getChildCount(); - - if (childCount > 2) { - Log.e(TAG, "onMeasure: More than two child views are not supported."); - } - - // We'll find the current one below. - mSlideableView = null; - - // First pass. Measure based on child LayoutParams width/height. - // Weight will incur a second pass. - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - if (child.getVisibility() == GONE) { - continue; - } - - if (lp.weight > 0) { - weightSum += lp.weight; - - // If we have no width, weight is the only contributor to the final size. - // Measure this view on the weight pass only. - if (lp.width == 0) continue; - } - - int childWidthSpec; - final int horizontalMargin = lp.leftMargin + lp.rightMargin; - - int childWidthSize = Math.max(widthAvailable - horizontalMargin, 0); - // When the parent width spec is UNSPECIFIED, measure each of child to get its - // desired width. - if (lp.width == LayoutParams.WRAP_CONTENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(childWidthSize, - widthMode == MeasureSpec.UNSPECIFIED ? widthMode : MeasureSpec.AT_MOST); - } else if (lp.width == LayoutParams.MATCH_PARENT) { - childWidthSpec = MeasureSpec.makeMeasureSpec(childWidthSize, widthMode); - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); - } - - int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, - getPaddingTop() + getPaddingBottom(), lp.height); - child.measure(childWidthSpec, childHeightSpec); - final int childWidth = child.getMeasuredWidth(); - final int childHeight = child.getMeasuredHeight(); - - if (childHeight > layoutHeight) { - if (heightMode == MeasureSpec.AT_MOST) { - layoutHeight = Math.min(childHeight, maxLayoutHeight); - } else if (heightMode == MeasureSpec.UNSPECIFIED) { - layoutHeight = childHeight; - } - } - - widthRemaining -= childWidth; - // Skip first child (list pane), the list pane is always a non-sliding pane. - if (i == 0) { - continue; - } - canSlide |= lp.slideable = widthRemaining < 0; - if (lp.slideable) { - mSlideableView = child; - } - } - // Second pass. Resolve weight. - // Child views overlap when the combined width of child views cannot fit into the - // available width. Each of child views is sized to fill all available space. If there is - // no overlap, distribute the extra width proportionally to weight. - if (canSlide || weightSum > 0) { - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == GONE) { - continue; - } - - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0; - final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth(); - int newWidth = measuredWidth; - int childWidthSpec = 0; - if (canSlide) { - // Child view consumes available space if the combined width cannot fit into - // the layout available width. - final int horizontalMargin = lp.leftMargin + lp.rightMargin; - newWidth = widthAvailable - horizontalMargin; - childWidthSpec = MeasureSpec.makeMeasureSpec( - newWidth, MeasureSpec.EXACTLY); - - } else if (lp.weight > 0) { - // Distribute the extra width proportionally similar to LinearLayout - final int widthToDistribute = Math.max(0, widthRemaining); - final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum); - newWidth = measuredWidth + addedWidth; - childWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY); - } - final int childHeightSpec = measureChildHeight(child, heightMeasureSpec, - getPaddingTop() + getPaddingBottom()); - if (measuredWidth != newWidth) { - child.measure(childWidthSpec, childHeightSpec); - final int childHeight = child.getMeasuredHeight(); - if (childHeight > layoutHeight) { - if (heightMode == MeasureSpec.AT_MOST) { - layoutHeight = Math.min(childHeight, maxLayoutHeight); - } else if (heightMode == MeasureSpec.UNSPECIFIED) { - layoutHeight = childHeight; - } - } - } - } - } - - // At this point, all child views have been measured. Calculate the device fold position - // in the view. Update the split position to where the fold when it exists. - ArrayList<Rect> splitViews = splitViewPositions(); - - if (splitViews != null && !canSlide) { - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - - if (child.getVisibility() == GONE) { - continue; - } - - final Rect splitView = splitViews.get(i); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - // If child view cannot fit in the separating view, expand the child view to fill - // available space. - final int horizontalMargin = lp.leftMargin + lp.rightMargin; - final int childHeightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), - MeasureSpec.EXACTLY); - int childWidthSpec = MeasureSpec.makeMeasureSpec(splitView.width(), - MeasureSpec.AT_MOST); - child.measure(childWidthSpec, childHeightSpec); - if ((child.getMeasuredWidthAndState() & MEASURED_STATE_TOO_SMALL) != 0 || ( - getMinimumWidth(child) != 0 - && splitView.width() < getMinimumWidth(child))) { - childWidthSpec = MeasureSpec.makeMeasureSpec(widthAvailable - horizontalMargin, - MeasureSpec.EXACTLY); - child.measure(childWidthSpec, childHeightSpec); - // Skip first child (list pane), the list pane is always a non-sliding pane. - if (i == 0) { - continue; - } - canSlide = lp.slideable = true; - mSlideableView = child; - } else { - childWidthSpec = MeasureSpec.makeMeasureSpec(splitView.width(), - MeasureSpec.EXACTLY); - child.measure(childWidthSpec, childHeightSpec); - } - } - } - - final int measuredWidth = widthSize; - final int measuredHeight = layoutHeight + getPaddingTop() + getPaddingBottom(); - - setMeasuredDimension(measuredWidth, measuredHeight); - mCanSlide = canSlide; - } - - private static int getMinimumWidth(View child) { - return ViewCompat.getMinimumWidth(child); - } - - private static int measureChildHeight(@NonNull View child, - int spec, int padding) { - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - final int childHeightSpec; - final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0; - if (skippedFirstPass) { - // This was skipped the first time; figure out a real height spec. - childHeightSpec = getChildMeasureSpec(spec, padding, lp.height); - - } else { - childHeightSpec = MeasureSpec.makeMeasureSpec( - child.getMeasuredHeight(), MeasureSpec.EXACTLY); - } - return childHeightSpec; - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final boolean isLayoutRtl = isLayoutRtlSupport(); - final int width = r - l; - final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft(); - final int paddingEnd = isLayoutRtl ? getPaddingLeft() : getPaddingRight(); - final int paddingTop = getPaddingTop(); - - final int childCount = getChildCount(); - int xStart = paddingStart; - int nextXStart = xStart; - - if (mFirstLayout) { - isOpened = mCanSlide && mPreservedOpenState; - } - - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - - if (child.getVisibility() == GONE) { - continue; - } - - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - final int childWidth = child.getMeasuredWidth(); - int offset = 0; - - if (lp.slideable) { - final int margin = lp.leftMargin + lp.rightMargin; - final int range = Math.min(nextXStart, width - paddingEnd) - xStart - margin; - final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin; - final int pos = (isOpened) ? 0 : range; - xStart += pos + lpMargin; - } else { - xStart = nextXStart; - } - - final int childRight; - final int childLeft; - if (isLayoutRtl) { - childRight = width - xStart + offset; - childLeft = childRight - childWidth; - } else { - childLeft = xStart - offset; - childRight = childLeft + childWidth; - } - - final int childTop = paddingTop; - final int childBottom = childTop + child.getMeasuredHeight(); - child.layout(childLeft, paddingTop, childRight, childBottom); - - // If a folding feature separates the content, we use its width as the extra - // offset for the next child, in order to avoid rendering the content under it. - int nextXOffset = 0; - if (mFoldingFeature != null - && mFoldingFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL - && mFoldingFeature.isSeparating()) { - nextXOffset = mFoldingFeature.getBounds().width(); - } - nextXStart += child.getWidth() + Math.abs(nextXOffset); - } - - mFirstLayout = false; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - // Recalculate sliding panes and their details - if (w != oldw) { - mFirstLayout = true; - } - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - if (!isInTouchMode() && !mCanSlide) { - mPreservedOpenState = child == mSlideableView; - } - } - - /** - * Close the detail view if it is currently slideable. If first layout - * has already completed this will animate. - * - * @return true if the pane was slideable and is now closed/in the process of closing - */ - public boolean closePane() { - if (!mCanSlide) { - mPreservedOpenState = false; - } - if (mFirstLayout || slideTo(false)) { - mPreservedOpenState = false; - return true; - } - return false; - } - - - /** - * Open the detail view if it is currently slideable. If first layout - * has already completed this will animate. - * - * @return true if the pane was slideable and is now open/in the process of opening - */ - public boolean openPane() { - if (!mCanSlide) { - mPreservedOpenState = true; - } - if (mFirstLayout || slideTo(true)) { - mPreservedOpenState = true; - return true; - } - return false; - } - - /** - * Open the detail view if it is currently slideable. If first layout - * has already completed this will animate. - */ - @Override - public void open() { - openPane(); - } - - /** - * Close the detail view if it is currently slideable. If first layout - * has already completed this will animate. - */ - @Override - public void close() { - closePane(); - } - - /** - * Check if the detail view is completely open. It can be open either because the slider - * itself is open revealing the detail view, or if all content fits without sliding. - * - * @return true if the detail view is completely open - */ - @Override - public boolean isOpen() { - return !mCanSlide || isOpened; - } - - /** - * Check if both the list and detail view panes in this layout can fully fit side-by-side. If - * not, the content pane has the capability to slide back and forth. Note that the lock mode - * is not taken into account in this method. This method is typically used to determine - * whether the layout is showing two-pane or single-pane. - * - * @return true if both panes cannot fit side-by-side, and detail pane in this layout has - * the capability to slide back and forth. - */ - public boolean isSlideable() { - return mCanSlide; - } - - /** - * @param opened position to switch to - */ - boolean slideTo(boolean opened) { - if (!mCanSlide) { - // Nothing to do. - return false; - } - - View slideableView = mSlideableView; - isOpened = opened; - mFirstLayout = true; - requestLayout(); - invalidate(); - if (opened) - dispatchOnPanelOpened(slideableView); - else - dispatchOnPanelClosed(slideableView); - mPreservedOpenState = opened; - return true; - } - - @Override - protected ViewGroup.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(); - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof MarginLayoutParams - ? new LayoutParams((MarginLayoutParams) p) - : new LayoutParams(p); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof LayoutParams && super.checkLayoutParams(p); - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); - } - - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - SavedState ss = new SavedState(superState); - ss.isOpen = isSlideable() ? isOpen() : mPreservedOpenState; - return ss; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - if (!(state instanceof SavedState)) { - super.onRestoreInstanceState(state); - return; - } - - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - - if (ss.isOpen) { - openPane(); - } else { - closePane(); - } - mPreservedOpenState = ss.isOpen; - } - - public static class LayoutParams extends ViewGroup.MarginLayoutParams { - private static final int[] ATTRS = new int[]{ - android.R.attr.layout_weight - }; - - /** - * The weighted proportion of how much of the leftover space - * this child should consume after measurement. - */ - public float weight = 0; - - /** - * True if this pane is the slideable pane in the layout. - */ - boolean slideable; - - public LayoutParams() { - super(MATCH_PARENT, MATCH_PARENT); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - public LayoutParams(@NonNull android.view.ViewGroup.LayoutParams source) { - super(source); - } - - public LayoutParams(@NonNull MarginLayoutParams source) { - super(source); - } - - public LayoutParams(@NonNull LayoutParams source) { - super(source); - this.weight = source.weight; - } - - public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) { - super(c, attrs); - - final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); - this.weight = a.getFloat(0, 0); - a.recycle(); - } - - } - - static class SavedState extends AbsSavedState { - boolean isOpen; - - SavedState(Parcelable superState) { - super(superState); - } - - SavedState(Parcel in, ClassLoader loader) { - super(in, loader); - isOpen = in.readInt() != 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeInt(isOpen ? 1 : 0); - } - - public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() { - @Override - public SavedState createFromParcel(Parcel in, ClassLoader loader) { - return new SavedState(in, null); - } - - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in, null); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - class AccessibilityDelegate extends AccessibilityDelegateCompat { - private final Rect mTmpRect = new Rect(); - - @Override - public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { - final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); - super.onInitializeAccessibilityNodeInfo(host, superNode); - copyNodeInfoNoChildren(info, superNode); - superNode.recycle(); - - info.setClassName(ACCESSIBILITY_CLASS_NAME); - info.setSource(host); - - final ViewParent parent = ViewCompat.getParentForAccessibility(host); - if (parent instanceof View) { - info.setParent((View) parent); - } - - // This is a best-approximation of addChildrenForAccessibility() - // that accounts for filtering. - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == View.VISIBLE) { - // Force importance to "yes" since we can't read the value. - ViewCompat.setImportantForAccessibility( - child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); - info.addChild(child); - } - } - } - - @Override - public void onInitializeAccessibilityEvent(@NonNull View host, @NonNull AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(host, event); - event.setClassName(ACCESSIBILITY_CLASS_NAME); - } - - /** - * This should really be in AccessibilityNodeInfoCompat, but there unfortunately - * seem to be a few elements that are not easily cloneable using the underlying API. - * Leave it private here as it's not general-purpose useful. - */ - private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, - AccessibilityNodeInfoCompat src) { - final Rect rect = mTmpRect; - - src.getBoundsInScreen(rect); - dest.setBoundsInScreen(rect); - - dest.setVisibleToUser(src.isVisibleToUser()); - dest.setPackageName(src.getPackageName()); - dest.setClassName(src.getClassName()); - dest.setContentDescription(src.getContentDescription()); - - dest.setEnabled(src.isEnabled()); - dest.setClickable(src.isClickable()); - dest.setFocusable(src.isFocusable()); - dest.setFocused(src.isFocused()); - dest.setAccessibilityFocused(src.isAccessibilityFocused()); - dest.setSelected(src.isSelected()); - dest.setLongClickable(src.isLongClickable()); - - dest.addAction(src.getActions()); - - dest.setMovementGranularities(src.getMovementGranularities()); - } - } - - boolean isLayoutRtlSupport() { - return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; - } - - /** - * @return A pair of rects define the position of the split, or {@null} if there is no split - */ - private ArrayList<Rect> splitViewPositions() { - if (mFoldingFeature == null || !mFoldingFeature.isSeparating()) { - return null; - } - - // Don't support horizontal fold in list-detail view layout - if (mFoldingFeature.getBounds().left == 0) { - return null; - } - // vertical split - if (mFoldingFeature.getBounds().top == 0) { - Rect splitPosition = getFoldBoundsInView(mFoldingFeature, this); - if (splitPosition == null) { - return null; - } - Rect leftRect = new Rect(getPaddingLeft(), getPaddingTop(), - Math.max(getPaddingLeft(), splitPosition.left), - getHeight() - getPaddingBottom()); - int rightBound = getWidth() - getPaddingRight(); - Rect rightRect = new Rect(Math.min(rightBound, splitPosition.right), - getPaddingTop(), rightBound, getHeight() - getPaddingBottom()); - return new ArrayList<>(Arrays.asList(leftRect, rightRect)); - } - return null; - } - - private static Rect getFoldBoundsInView(@NonNull FoldingFeature foldingFeature, View view) { - int[] viewLocationInWindow = new int[2]; - view.getLocationInWindow(viewLocationInWindow); - - Rect viewRect = new Rect(viewLocationInWindow[0], viewLocationInWindow[1], - viewLocationInWindow[0] + view.getWidth(), - viewLocationInWindow[1] + view.getWidth()); - Rect foldRectInView = new Rect(foldingFeature.getBounds()); - // Translate coordinate space of split from window coordinate space to current view - // position in window - boolean intersects = foldRectInView.intersect(viewRect); - // Check if the split is overlapped with the view - if ((foldRectInView.width() == 0 && foldRectInView.height() == 0) || !intersects) { - return null; - } - foldRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1]); - return foldRectInView; - } - - @Nullable - private static Activity getActivityOrNull(Context context) { - Context iterator = context; - while (iterator instanceof ContextWrapper) { - if (iterator instanceof Activity) { - return (Activity) iterator; - } - iterator = ((ContextWrapper) iterator).getBaseContext(); - } - return null; - } -} diff --git a/jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.kt b/jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.kt new file mode 100644 index 000000000..68a197b92 --- /dev/null +++ b/jami-android/app/src/main/java/cx/ring/views/twopane/TwoPaneLayout.kt @@ -0,0 +1,767 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 cx.ring.views.twopane + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.graphics.Rect +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.ClassLoaderCreator +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import androidx.core.content.ContextCompat +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import androidx.core.view.animation.PathInterpolatorCompat +import androidx.customview.view.AbsSavedState +import androidx.customview.widget.Openable +import androidx.transition.ChangeBounds +import androidx.transition.Transition +import androidx.transition.TransitionManager +import androidx.window.layout.FoldingFeature +import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL +import androidx.window.layout.WindowInfoTracker +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * TwoPaneLayout provides a horizontal, multi-pane layout for use at the top level + * of a UI. A left (or start) pane is treated as a content list or browser, subordinate to a + * primary detail view for displaying content. + * + * Each of child views is expand out to fill the available width in + * the TwoPaneLayout. + * + * Thanks to this behavior, TwoPaneLayout may be suitable for creating layouts + * that can smoothly adapt across many different screen sizes, expanding out fully on larger + * screens and collapsing on smaller screens. + * + * Like [LinearLayout][android.widget.LinearLayout], TwoPaneLayout supports + * the use of the layout parameter `layout_weight` on child views to determine + * how to divide leftover space after measurement is complete. It is only relevant for width. + * When views do not overlap weight behaves as it does in a LinearLayout. + */ +class TwoPaneLayout @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, + defStyle: Int = 0 +) : ViewGroup(context, attrs, defStyle), Openable { + /** + * Check if both the list and detail view panes in this layout can fully fit side-by-side. If + * not, the content pane has the capability to slide back and forth. Note that the lock mode + * is not taken into account in this method. This method is typically used to determine + * whether the layout is showing two-pane or single-pane. + * + * @return true if both panes cannot fit side-by-side, and detail pane in this layout has + * the capability to slide back and forth. + */ + /** + * True if a panel can slide with the current measurements + */ + var isSlideable = false + private set + + /** + * The child view that can slide, if any. + */ + private var mSlideableView: View? = null + + /** + * How far the panel is offset from its usual position. + * range [0, 1] where 0 = open, 1 = closed. + */ + private var isOpened = false + private val mPanelListeners: MutableList<PanelListener> = CopyOnWriteArrayList() + + private val tmpRect = Rect() + private val tmpRect2 = Rect() + private val foldBoundsCalculator = FoldBoundsCalculator() + + /** + * Stores whether or not the pane was open the last time it was slideable. + * If open/close operations are invoked this state is modified. Used by + * instance state save/restore. + */ + private var mPreservedOpenState = false + private var mFirstLayout = true + private var foldingFeature: FoldingFeature? = null + set(value) { + if (value != field) { + field = value + val changeBounds: Transition = ChangeBounds() + changeBounds.setDuration(300L) + changeBounds.setInterpolator(PathInterpolatorCompat.create(0.2f, 0f, 0f, 1f)) + TransitionManager.beginDelayedTransition(this@TwoPaneLayout, changeBounds) + requestLayout() + } + } + + /** + * Listener for monitoring events about sliding panes. + */ + interface PanelListener { + /** + * Called when a detail view becomes slid completely open. + * + * @param panel The detail view that was slid to an open position + */ + fun onPanelOpened(panel: View) + + /** + * Called when a detail view becomes slid completely closed. + * + * @param panel The detail view that was slid to a closed position + */ + fun onPanelClosed(panel: View) + } + + private val mFoldingFeatureObserver: FoldingFeatureObserver? + init { + ViewCompat.setAccessibilityDelegate(this, AccessibilityDelegate()) + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES) + val repo = WindowInfoTracker.getOrCreate(context) + val mainExecutor = ContextCompat.getMainExecutor(context) + mFoldingFeatureObserver = FoldingFeatureObserver(repo, mainExecutor) + mFoldingFeatureObserver.setOnFoldingFeatureChangeListener(object : FoldingFeatureObserver.OnFoldingFeatureChangeListener { + override fun onFoldingFeatureChange(foldingFeature: FoldingFeature) { + this@TwoPaneLayout.foldingFeature = foldingFeature + } + }) + } + + /** + * Adds the specified listener to the list of listeners that will be notified of + * panel slide events. + * + * @param listener Listener to notify when panel slide events occur. + * @see .removePanelListener + */ + fun addPanelListener(listener: PanelListener) { + mPanelListeners.add(listener) + } + + /** + * Removes the specified listener from the list of listeners that will be notified of + * panel slide events. + * + * @param listener Listener to remove from being notified of panel slide events + * @see .addPanelListener + */ + fun removePanelListener(listener: PanelListener) { + mPanelListeners.remove(listener) + } + + fun dispatchOnPanelOpened(panel: View) { + for (listener in mPanelListeners) { + listener.onPanelOpened(panel) + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) + } + + fun dispatchOnPanelClosed(panel: View) { + for (listener in mPanelListeners) { + listener.onPanelClosed(panel) + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mFirstLayout = true + if (mFoldingFeatureObserver != null) { + val activity = getActivityOrNull(context) + if (activity != null) { + mFoldingFeatureObserver.registerLayoutStateChangeCallback(activity) + } + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mFirstLayout = true + mFoldingFeatureObserver?.unregisterLayoutStateChangeCallback() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + var layoutHeight = 0 + var maxLayoutHeight = 0 + when (heightMode) { + MeasureSpec.EXACTLY -> { + maxLayoutHeight = heightSize - paddingTop - paddingBottom + layoutHeight = maxLayoutHeight + } + MeasureSpec.AT_MOST -> maxLayoutHeight = heightSize - paddingTop - paddingBottom + } + var weightSum = 0f + var canSlide = false + val widthAvailable = max(widthSize - getPaddingLeft() - getPaddingRight(), 0) + var widthRemaining = widthAvailable + val childCount = childCount + if (childCount > 2) { + Log.e(TAG, "onMeasure: More than two child views are not supported.") + } + + // We'll find the current one below. + mSlideableView = null + + // First pass. Measure based on child LayoutParams width/height. + // Weight will incur a second pass. + for (i in 0 until childCount) { + val child = getChildAt(i) + val lp = child.layoutParams as LayoutParams + if (child.visibility == GONE) { + continue + } + if (lp.weight > 0) { + weightSum += lp.weight + + // If we have no width, weight is the only contributor to the final size. + // Measure this view on the weight pass only. + if (lp.width == 0) continue + } + var childWidthSpec: Int + val horizontalMargin = lp.leftMargin + lp.rightMargin + val childWidthSize = max(widthAvailable - horizontalMargin, 0) + // When the parent width spec is UNSPECIFIED, measure each of child to get its + // desired width. + childWidthSpec = when (lp.width) { + ViewGroup.LayoutParams.WRAP_CONTENT -> MeasureSpec.makeMeasureSpec(childWidthSize, + if (widthMode == MeasureSpec.UNSPECIFIED) widthMode else MeasureSpec.AT_MOST) + ViewGroup.LayoutParams.MATCH_PARENT -> MeasureSpec.makeMeasureSpec(childWidthSize, widthMode) + else -> MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY) + } + val childHeightSpec = getChildMeasureSpec(heightMeasureSpec, + paddingTop + paddingBottom, lp.height + ) + child.measure(childWidthSpec, childHeightSpec) + val childWidth = child.measuredWidth + val childHeight = child.measuredHeight + if (childHeight > layoutHeight) { + if (heightMode == MeasureSpec.AT_MOST) { + layoutHeight = min(childHeight, maxLayoutHeight) + } else if (heightMode == MeasureSpec.UNSPECIFIED) { + layoutHeight = childHeight + } + } + widthRemaining -= childWidth + // Skip first child (list pane), the list pane is always a non-sliding pane. + if (i == 0) { + continue + } + lp.slideable = widthRemaining < 0 + canSlide = canSlide or lp.slideable + if (lp.slideable) { + mSlideableView = child + } + } + // Second pass. Resolve weight. + // Child views overlap when the combined width of child views cannot fit into the + // available width. Each of child views is sized to fill all available space. If there is + // no overlap, distribute the extra width proportionally to weight. + if (canSlide || weightSum > 0) { + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility == GONE) { + continue + } + val lp = child.layoutParams as LayoutParams + val skippedFirstPass = lp.width == 0 && lp.weight > 0 + val measuredWidth = if (skippedFirstPass) 0 else child.measuredWidth + var newWidth = measuredWidth + var childWidthSpec = 0 + if (canSlide) { + // Child view consumes available space if the combined width cannot fit into + // the layout available width. + val horizontalMargin = lp.leftMargin + lp.rightMargin + newWidth = widthAvailable - horizontalMargin + childWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY) + } else if (lp.weight > 0) { + // Distribute the extra width proportionally similar to LinearLayout + val widthToDistribute = max(0, widthRemaining) + val addedWidth = (lp.weight * widthToDistribute / weightSum).toInt() + newWidth = measuredWidth + addedWidth + childWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY) + } + val childHeightSpec = measureChildHeight( + child, heightMeasureSpec, + paddingTop + paddingBottom + ) + if (measuredWidth != newWidth) { + child.measure(childWidthSpec, childHeightSpec) + val childHeight = child.measuredHeight + if (childHeight > layoutHeight) { + if (heightMode == MeasureSpec.AT_MOST) { + layoutHeight = min(childHeight, maxLayoutHeight) + } else if (heightMode == MeasureSpec.UNSPECIFIED) { + layoutHeight = childHeight + } + } + } + } + } + + // At this point, all child views have been measured. Calculate the device fold position + // in the view. Update the split position to where the fold when it exists. + val leftSplitBounds = tmpRect + val rightSplitBounds = tmpRect2 + val hasFold = foldBoundsCalculator.splitViewPositions(foldingFeature, this, leftSplitBounds, rightSplitBounds) + if (hasFold && !canSlide) { + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility == GONE) { + continue + } + val splitView = if (i == 0) leftSplitBounds else rightSplitBounds + val lp = child.layoutParams as LayoutParams + + // If child view cannot fit in the separating view, expand the child view to fill + // available space. + val horizontalMargin = lp.leftMargin + lp.rightMargin + val childHeightSpec = MeasureSpec.makeMeasureSpec(child.measuredHeight, MeasureSpec.EXACTLY) + var childWidthSpec = MeasureSpec.makeMeasureSpec(splitView.width(), MeasureSpec.AT_MOST) + child.measure(childWidthSpec, childHeightSpec) + if ((getMinimumWidth(child) != 0 && splitView.width() < getMinimumWidth(child))) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthAvailable - horizontalMargin, MeasureSpec.EXACTLY) + child.measure(childWidthSpec, childHeightSpec) + // Skip first child (list pane), the list pane is always a non-sliding pane. + if (i == 0) { + continue + } + lp.slideable = true + canSlide = lp.slideable + mSlideableView = child + } else { + childWidthSpec = MeasureSpec.makeMeasureSpec(splitView.width(), MeasureSpec.EXACTLY) + child.measure(childWidthSpec, childHeightSpec) + } + } + } + val measuredHeight = layoutHeight + paddingTop + paddingBottom + setMeasuredDimension(widthSize, measuredHeight) + isSlideable = canSlide + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + val isLayoutRtl = isLayoutRtlSupport + val width = r - l + val paddingStart = if (isLayoutRtl) getPaddingRight() else getPaddingLeft() + val paddingEnd = if (isLayoutRtl) getPaddingLeft() else getPaddingRight() + val paddingTop = paddingTop + val childCount = childCount + var xStart = paddingStart + var nextXStart = xStart + if (mFirstLayout) { + isOpened = isSlideable && mPreservedOpenState + } + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility == GONE) { + continue + } + val lp = child.layoutParams as LayoutParams + val childWidth = child.measuredWidth + val offset = 0 + if (lp.slideable) { + val margin = lp.leftMargin + lp.rightMargin + val range = (min(nextXStart, (width - paddingEnd)) - xStart - margin) + val lpMargin = if (isLayoutRtl) lp.rightMargin else lp.leftMargin + val pos = if (isOpened || foldingFeature == null) 0 else range + xStart += pos + lpMargin + } else { + xStart = nextXStart + } + val childRight: Int + val childLeft: Int + if (isLayoutRtl) { + childRight = width - xStart + offset + childLeft = childRight - childWidth + } else { + childLeft = xStart - offset + childRight = childLeft + childWidth + } + val childBottom = paddingTop + child.measuredHeight + child.layout(childLeft, paddingTop, childRight, childBottom) + + // If a folding feature separates the content, we use its width as the extra + // offset for the next child, in order to avoid rendering the content under it. + var nextXOffset = 0 + if (foldingFeature != null && foldingFeature!!.orientation == VERTICAL && foldingFeature!!.isSeparating) { + nextXOffset = foldingFeature!!.bounds.width() + } + nextXStart += child.width + abs(nextXOffset) + } + mFirstLayout = false + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + // Recalculate sliding panes and their details + if (w != oldw) { + mFirstLayout = true + } + } + + override fun requestChildFocus(child: View, focused: View) { + super.requestChildFocus(child, focused) + if (!isInTouchMode() && !isSlideable) { + mPreservedOpenState = child === mSlideableView + } + } + + /** + * Close the detail view if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now closed/in the process of closing + */ + fun closePane(): Boolean { + if (!isSlideable) { + mPreservedOpenState = false + } + if (mFirstLayout || slideTo(false)) { + mPreservedOpenState = false + return true + } + return false + } + + /** + * Open the detail view if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now open/in the process of opening + */ + fun openPane(): Boolean { + if (!isSlideable) { + mPreservedOpenState = true + } + if (mFirstLayout || slideTo(true)) { + mPreservedOpenState = true + return true + } + return false + } + + /** + * Open the detail view if it is currently slideable. If first layout + * has already completed this will animate. + */ + override fun open() { + openPane() + } + + /** + * Close the detail view if it is currently slideable. If first layout + * has already completed this will animate. + */ + override fun close() { + closePane() + } + + /** + * Check if the detail view is completely open. It can be open either because the slider + * itself is open revealing the detail view, or if all content fits without sliding. + * + * @return true if the detail view is completely open + */ + override fun isOpen(): Boolean = !isSlideable || isOpened + + /** + * @param opened position to switch to + */ + fun slideTo(opened: Boolean): Boolean { + if (!isSlideable) { + // Nothing to do. + return false + } + val slideableView = mSlideableView + isOpened = opened + mFirstLayout = true + requestLayout() + invalidate() + if (opened) dispatchOnPanelOpened(slideableView!!) else dispatchOnPanelClosed(slideableView!!) + mPreservedOpenState = opened + return true + } + + override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams = LayoutParams() + + override fun generateLayoutParams(p: ViewGroup.LayoutParams): ViewGroup.LayoutParams = + if (p is MarginLayoutParams) LayoutParams(p) else LayoutParams(p) + + override fun checkLayoutParams(p: ViewGroup.LayoutParams): Boolean = + p is LayoutParams && super.checkLayoutParams(p) + + override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams = LayoutParams(context, attrs) + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + val ss = SavedState(superState) + ss.isOpen = if (isSlideable) isOpen else mPreservedOpenState + return ss + } + + override fun onRestoreInstanceState(state: Parcelable) { + if (state !is SavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + if (state.isOpen) { + openPane() + } else { + closePane() + } + mPreservedOpenState = state.isOpen + } + + class LayoutParams : MarginLayoutParams { + /** + * The weighted proportion of how much of the leftover space + * this child should consume after measurement. + */ + var weight = 0f + + /** + * True if this pane is the slideable pane in the layout. + */ + var slideable = false + + constructor() : super(MATCH_PARENT, MATCH_PARENT) + constructor(width: Int, height: Int) : super(width, height) + constructor(source: ViewGroup.LayoutParams) : super(source) + constructor(source: MarginLayoutParams) : super(source) + constructor(source: LayoutParams) : super(source) { + weight = source.weight + } + + constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { + val a = c.obtainStyledAttributes(attrs, ATTRS) + weight = a.getFloat(0, 0f) + a.recycle() + } + + companion object { + private val ATTRS = intArrayOf( + android.R.attr.layout_weight + ) + } + } + + internal class SavedState : AbsSavedState { + var isOpen = false + + constructor(superState: Parcelable?) : super(superState!!) + constructor(i: Parcel, loader: ClassLoader?) : super(i, loader) { + isOpen = i.readInt() != 0 + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeInt(if (isOpen) 1 else 0) + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<SavedState> = object : ClassLoaderCreator<SavedState> { + override fun createFromParcel(i: Parcel, loader: ClassLoader): SavedState = SavedState(i, null) + override fun createFromParcel(i: Parcel): SavedState = SavedState(i, null) + override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size) + } + } + } + + internal inner class AccessibilityDelegate : AccessibilityDelegateCompat() { + private val mTmpRect = Rect() + override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) { + val superNode = AccessibilityNodeInfoCompat.obtain(info) + super.onInitializeAccessibilityNodeInfo(host, superNode) + copyNodeInfoNoChildren(info, superNode) + @Suppress("Deprecation") + superNode.recycle() + info.className = ACCESSIBILITY_CLASS_NAME + info.setSource(host) + val parent = ViewCompat.getParentForAccessibility(host) + if (parent is View) { + info.setParent(parent as View?) + } + + // This is a best-approximation of addChildrenForAccessibility() + // that accounts for filtering. + val childCount = childCount + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility == VISIBLE) { + // Force importance to "yes" since we can't read the value. + ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES) + info.addChild(child) + } + } + } + + override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) { + super.onInitializeAccessibilityEvent(host, event) + event.setClassName(ACCESSIBILITY_CLASS_NAME) + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + private fun copyNodeInfoNoChildren(dest: AccessibilityNodeInfoCompat, src: AccessibilityNodeInfoCompat) { + val rect = mTmpRect + src.getBoundsInScreen(rect) + dest.setBoundsInScreen(rect) + dest.isVisibleToUser = src.isVisibleToUser + dest.packageName = src.packageName + dest.className = src.className + dest.contentDescription = src.contentDescription + dest.isEnabled = src.isEnabled + dest.isClickable = src.isClickable + dest.isFocusable = src.isFocusable + dest.isFocused = src.isFocused + dest.isAccessibilityFocused = src.isAccessibilityFocused + dest.isSelected = src.isSelected + dest.isLongClickable = src.isLongClickable + @Suppress("Deprecation") + dest.addAction(src.actions) + dest.movementGranularities = src.movementGranularities + } + } + + private val isLayoutRtlSupport: Boolean + get() = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL + + + /** + * Utility for calculating layout positioning of child views relative to a [FoldingFeature]. + * This class is not thread-safe. + */ + private class FoldBoundsCalculator { + private val tmpIntArray = IntArray(2) + private val splitViewPositionsTmpRect = Rect() + private val getFoldBoundsInViewTmpRect = Rect() + /** + * Returns `true` if there is a split and [outLeftRect] and [outRightRect] contain the split + * positions; false if there is not a compatible split available, [outLeftRect] and + * [outRightRect] will remain unmodified. + */ + fun splitViewPositions( + foldingFeature: FoldingFeature?, + parentView: View, + outLeftRect: Rect, + outRightRect: Rect, + ): Boolean { + if (foldingFeature == null) return false + if (!foldingFeature.isSeparating) return false + // Don't support horizontal fold in list-detail view layout + if (foldingFeature.bounds.left == 0) return false + // vertical split + val splitPosition = splitViewPositionsTmpRect + if (foldingFeature.bounds.top == 0 && + getFoldBoundsInView(foldingFeature, parentView, splitPosition) + ) { + outLeftRect.set( + parentView.paddingLeft, + parentView.paddingTop, + max(parentView.paddingLeft, splitPosition.left), + parentView.height - parentView.paddingBottom + ) + val rightBound = parentView.width - parentView.paddingRight + outRightRect.set( + min(rightBound, splitPosition.right), + parentView.paddingTop, + rightBound, + parentView.height - parentView.paddingBottom + ) + return true + } + return false + } + /** + * Returns `true` if [foldingFeature] overlaps with [view] and writes the bounds to [outRect]. + */ + private fun getFoldBoundsInView( + foldingFeature: FoldingFeature, + view: View, + outRect: Rect + ): Boolean { + val viewLocationInWindow = tmpIntArray + view.getLocationInWindow(viewLocationInWindow) + val x = viewLocationInWindow[0] + val y = viewLocationInWindow[1] + val viewRect = getFoldBoundsInViewTmpRect.apply { + set(x, y, x + view.width, y + view.width) + } + val foldRectInView = outRect.apply { set(foldingFeature.bounds) } + // Translate coordinate space of split from window coordinate space to current view + // position in window + val intersects = foldRectInView.intersect(viewRect) + // Check if the split is overlapped with the view + if (foldRectInView.width() == 0 && foldRectInView.height() == 0 || !intersects) { + return false + } + foldRectInView.offset(-x, -y) + return true + } + } + + companion object { + private const val TAG = "TwoPaneLayout" + + /** Class name may be obfuscated by Proguard. Hardcode the string for accessibility usage. */ + private const val ACCESSIBILITY_CLASS_NAME = "cx.ring.views.twopane.TwoPaneLayout" + private fun getMinimumWidth(child: View): Int { + return ViewCompat.getMinimumWidth(child) + } + + private fun measureChildHeight(child: View, spec: Int, padding: Int): Int { + val lp = child.layoutParams as LayoutParams + val childHeightSpec: Int + val skippedFirstPass = lp.width == 0 && lp.weight > 0 + childHeightSpec = if (skippedFirstPass) { + // This was skipped the first time; figure out a real height spec. + getChildMeasureSpec(spec, padding, lp.height) + } else { + MeasureSpec.makeMeasureSpec(child.measuredHeight, MeasureSpec.EXACTLY) + } + return childHeightSpec + } + + private fun getActivityOrNull(context: Context): Activity? { + var iterator: Context? = context + while (iterator is ContextWrapper) { + if (iterator is Activity) { + return iterator + } + iterator = iterator.baseContext + } + return null + } + } +} -- GitLab