Repo created
This commit is contained in:
parent
a629de6271
commit
3cef7c5092
2161 changed files with 246605 additions and 2 deletions
11
ui-utils/ItemTouchHelper/build.gradle.kts
Normal file
11
ui-utils/ItemTouchHelper/build.gradle.kts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.recyclerview)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.k9mail.ui.utils.itemtouchhelper"
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 app.k9mail.ui.utils.itemtouchhelper;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.R;
|
||||
import androidx.recyclerview.widget.ItemTouchUIUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
||||
/**
|
||||
* Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
|
||||
* public API, which is not desired in this case.
|
||||
*/
|
||||
class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
|
||||
static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl();
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
|
||||
int actionState, boolean isCurrentlyActive) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (isCurrentlyActive) {
|
||||
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
|
||||
if (originalElevation == null) {
|
||||
originalElevation = ViewCompat.getElevation(view);
|
||||
float newElevation = 1f + findMaxElevation(recyclerView, view);
|
||||
ViewCompat.setElevation(view, newElevation);
|
||||
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.setTranslationX(dX);
|
||||
view.setTranslationY(dY);
|
||||
}
|
||||
|
||||
private static float findMaxElevation(RecyclerView recyclerView, View itemView) {
|
||||
final int childCount = recyclerView.getChildCount();
|
||||
float max = 0;
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = recyclerView.getChildAt(i);
|
||||
if (child == itemView) {
|
||||
continue;
|
||||
}
|
||||
final float elevation = ViewCompat.getElevation(child);
|
||||
if (elevation > max) {
|
||||
max = elevation;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
|
||||
int actionState, boolean isCurrentlyActive) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(View view) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
|
||||
if (tag instanceof Float) {
|
||||
ViewCompat.setElevation(view, (Float) tag);
|
||||
}
|
||||
view.setTag(R.id.item_touch_helper_previous_elevation, null);
|
||||
}
|
||||
|
||||
view.setTranslationX(0f);
|
||||
view.setTranslationY(0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelected(View view) {
|
||||
}
|
||||
}
|
||||
11
ui-utils/LinearLayoutManager/build.gradle.kts
Normal file
11
ui-utils/LinearLayoutManager/build.gradle.kts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.recyclerview)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.k9mail.ui.utils.linearlayoutmanager"
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package app.k9mail.ui.utils.linearlayoutmanager;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
// Source: https://github.com/SchildiChat/SchildiChat-android/blob/a321d6a79a1dc93bcfea442390e49c557285eabe/vector/src/main/java/de/spiritcroc/recyclerview/widget/LayoutManager.java
|
||||
/**
|
||||
* Exposing/replicating some internal functions from RecylerView.LayoutManager
|
||||
*/
|
||||
public abstract class LayoutManager extends RecyclerView.LayoutManager {
|
||||
|
||||
|
||||
/*
|
||||
* Exposed things from RecyclerView.java
|
||||
*/
|
||||
|
||||
/**
|
||||
* The callback used for retrieving information about a RecyclerView and its children in the
|
||||
* horizontal direction.
|
||||
*/
|
||||
private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback =
|
||||
new ViewBoundsCheck.Callback() {
|
||||
@Override
|
||||
public View getChildAt(int index) {
|
||||
return LayoutManager.this.getChildAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getParentStart() {
|
||||
return LayoutManager.this.getPaddingLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getParentEnd() {
|
||||
return LayoutManager.this.getWidth() - LayoutManager.this.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return LayoutManager.this.getDecoratedLeft(view) - params.leftMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return LayoutManager.this.getDecoratedRight(view) + params.rightMargin;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The callback used for retrieving information about a RecyclerView and its children in the
|
||||
* vertical direction.
|
||||
*/
|
||||
private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback =
|
||||
new ViewBoundsCheck.Callback() {
|
||||
@Override
|
||||
public View getChildAt(int index) {
|
||||
return LayoutManager.this.getChildAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getParentStart() {
|
||||
return LayoutManager.this.getPaddingTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getParentEnd() {
|
||||
return LayoutManager.this.getHeight()
|
||||
- LayoutManager.this.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return LayoutManager.this.getDecoratedTop(view) - params.topMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return LayoutManager.this.getDecoratedBottom(view) + params.bottomMargin;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility objects used to check the boundaries of children against their parent
|
||||
* RecyclerView.
|
||||
*
|
||||
* @see #isViewPartiallyVisible(View, boolean, boolean),
|
||||
* {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)},
|
||||
* and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}.
|
||||
*/
|
||||
ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback);
|
||||
ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback);
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 app.k9mail.ui.utils.linearlayoutmanager;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.OrientationHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
||||
/**
|
||||
* A helper class to do scroll offset calculations.
|
||||
*/
|
||||
class ScrollbarHelper {
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled, boolean reverseLayout) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|
||||
|| endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
final int minPosition = Math.min(lm.getPosition(startChild),
|
||||
lm.getPosition(endChild));
|
||||
final int maxPosition = Math.max(lm.getPosition(startChild),
|
||||
lm.getPosition(endChild));
|
||||
final int itemsBefore = reverseLayout
|
||||
? Math.max(0, state.getItemCount() - maxPosition - 1)
|
||||
: Math.max(0, minPosition);
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return itemsBefore;
|
||||
}
|
||||
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild));
|
||||
final int itemRange = Math.abs(lm.getPosition(startChild)
|
||||
- lm.getPosition(endChild)) + 1;
|
||||
final float avgSizePerRow = (float) laidOutArea / itemRange;
|
||||
|
||||
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
|
||||
- orientation.getDecoratedStart(startChild)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|
||||
|| endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
}
|
||||
final int extend = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
return Math.min(orientation.getTotalSpace(), extend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|
||||
|| endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return state.getItemCount();
|
||||
}
|
||||
// smooth scrollbar enabled. try to estimate better.
|
||||
final int laidOutArea = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
final int laidOutRange = Math.abs(lm.getPosition(startChild)
|
||||
- lm.getPosition(endChild))
|
||||
+ 1;
|
||||
// estimate a size for full list.
|
||||
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
|
||||
}
|
||||
|
||||
private ScrollbarHelper() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* 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 app.k9mail.ui.utils.linearlayoutmanager;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* A utility class used to check the boundaries of a given view within its parent view based on
|
||||
* a set of boundary flags.
|
||||
*/
|
||||
class ViewBoundsCheck {
|
||||
|
||||
static final int GT = 1 << 0;
|
||||
static final int EQ = 1 << 1;
|
||||
static final int LT = 1 << 2;
|
||||
|
||||
|
||||
static final int CVS_PVS_POS = 0;
|
||||
/**
|
||||
* The child view's start should be strictly greater than parent view's start.
|
||||
*/
|
||||
static final int FLAG_CVS_GT_PVS = GT << CVS_PVS_POS;
|
||||
|
||||
/**
|
||||
* The child view's start can be equal to its parent view's start. This flag follows with GT
|
||||
* or LT indicating greater (less) than or equal relation.
|
||||
*/
|
||||
static final int FLAG_CVS_EQ_PVS = EQ << CVS_PVS_POS;
|
||||
|
||||
/**
|
||||
* The child view's start should be strictly less than parent view's start.
|
||||
*/
|
||||
static final int FLAG_CVS_LT_PVS = LT << CVS_PVS_POS;
|
||||
|
||||
|
||||
static final int CVS_PVE_POS = 4;
|
||||
/**
|
||||
* The child view's start should be strictly greater than parent view's end.
|
||||
*/
|
||||
static final int FLAG_CVS_GT_PVE = GT << CVS_PVE_POS;
|
||||
|
||||
/**
|
||||
* The child view's start can be equal to its parent view's end. This flag follows with GT
|
||||
* or LT indicating greater (less) than or equal relation.
|
||||
*/
|
||||
static final int FLAG_CVS_EQ_PVE = EQ << CVS_PVE_POS;
|
||||
|
||||
/**
|
||||
* The child view's start should be strictly less than parent view's end.
|
||||
*/
|
||||
static final int FLAG_CVS_LT_PVE = LT << CVS_PVE_POS;
|
||||
|
||||
|
||||
static final int CVE_PVS_POS = 8;
|
||||
/**
|
||||
* The child view's end should be strictly greater than parent view's start.
|
||||
*/
|
||||
static final int FLAG_CVE_GT_PVS = GT << CVE_PVS_POS;
|
||||
|
||||
/**
|
||||
* The child view's end can be equal to its parent view's start. This flag follows with GT
|
||||
* or LT indicating greater (less) than or equal relation.
|
||||
*/
|
||||
static final int FLAG_CVE_EQ_PVS = EQ << CVE_PVS_POS;
|
||||
|
||||
/**
|
||||
* The child view's end should be strictly less than parent view's start.
|
||||
*/
|
||||
static final int FLAG_CVE_LT_PVS = LT << CVE_PVS_POS;
|
||||
|
||||
|
||||
static final int CVE_PVE_POS = 12;
|
||||
/**
|
||||
* The child view's end should be strictly greater than parent view's end.
|
||||
*/
|
||||
static final int FLAG_CVE_GT_PVE = GT << CVE_PVE_POS;
|
||||
|
||||
/**
|
||||
* The child view's end can be equal to its parent view's end. This flag follows with GT
|
||||
* or LT indicating greater (less) than or equal relation.
|
||||
*/
|
||||
static final int FLAG_CVE_EQ_PVE = EQ << CVE_PVE_POS;
|
||||
|
||||
/**
|
||||
* The child view's end should be strictly less than parent view's end.
|
||||
*/
|
||||
static final int FLAG_CVE_LT_PVE = LT << CVE_PVE_POS;
|
||||
|
||||
static final int MASK = GT | EQ | LT;
|
||||
|
||||
final Callback mCallback;
|
||||
BoundFlags mBoundFlags;
|
||||
/**
|
||||
* The set of flags that can be passed for checking the view boundary conditions.
|
||||
* CVS in the flag name indicates the child view, and PV indicates the parent view.\
|
||||
* The following S, E indicate a view's start and end points, respectively.
|
||||
* GT and LT indicate a strictly greater and less than relationship.
|
||||
* Greater than or equal (or less than or equal) can be specified by setting both GT and EQ (or
|
||||
* LT and EQ) flags.
|
||||
* For instance, setting both {@link #FLAG_CVS_GT_PVS} and {@link #FLAG_CVS_EQ_PVS} indicate the
|
||||
* child view's start should be greater than or equal to its parent start.
|
||||
*/
|
||||
@IntDef(flag = true, value = {
|
||||
FLAG_CVS_GT_PVS, FLAG_CVS_EQ_PVS, FLAG_CVS_LT_PVS,
|
||||
FLAG_CVS_GT_PVE, FLAG_CVS_EQ_PVE, FLAG_CVS_LT_PVE,
|
||||
FLAG_CVE_GT_PVS, FLAG_CVE_EQ_PVS, FLAG_CVE_LT_PVS,
|
||||
FLAG_CVE_GT_PVE, FLAG_CVE_EQ_PVE, FLAG_CVE_LT_PVE
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface ViewBounds {}
|
||||
|
||||
ViewBoundsCheck(Callback callback) {
|
||||
mCallback = callback;
|
||||
mBoundFlags = new BoundFlags();
|
||||
}
|
||||
|
||||
static class BoundFlags {
|
||||
int mBoundFlags = 0;
|
||||
int mRvStart, mRvEnd, mChildStart, mChildEnd;
|
||||
|
||||
void setBounds(int rvStart, int rvEnd, int childStart, int childEnd) {
|
||||
mRvStart = rvStart;
|
||||
mRvEnd = rvEnd;
|
||||
mChildStart = childStart;
|
||||
mChildEnd = childEnd;
|
||||
}
|
||||
|
||||
void addFlags(@ViewBounds int flags) {
|
||||
mBoundFlags |= flags;
|
||||
}
|
||||
|
||||
void resetFlags() {
|
||||
mBoundFlags = 0;
|
||||
}
|
||||
|
||||
int compare(int x, int y) {
|
||||
if (x > y) {
|
||||
return GT;
|
||||
}
|
||||
if (x == y) {
|
||||
return EQ;
|
||||
}
|
||||
return LT;
|
||||
}
|
||||
|
||||
boolean boundsMatch() {
|
||||
if ((mBoundFlags & (MASK << CVS_PVS_POS)) != 0) {
|
||||
if ((mBoundFlags & (compare(mChildStart, mRvStart) << CVS_PVS_POS)) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((mBoundFlags & (MASK << CVS_PVE_POS)) != 0) {
|
||||
if ((mBoundFlags & (compare(mChildStart, mRvEnd) << CVS_PVE_POS)) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((mBoundFlags & (MASK << CVE_PVS_POS)) != 0) {
|
||||
if ((mBoundFlags & (compare(mChildEnd, mRvStart) << CVE_PVS_POS)) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) {
|
||||
if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the first view starting from fromIndex to toIndex in views whose bounds lie within
|
||||
* its parent bounds based on the provided preferredBoundFlags. If no match is found based on
|
||||
* the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose
|
||||
* bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such
|
||||
* view is found based on either of these two flags, null is returned.
|
||||
* @param fromIndex The view position index to start the search from.
|
||||
* @param toIndex The view position index to end the search at.
|
||||
* @param preferredBoundFlags The flags indicating the preferred match. Once a match is found
|
||||
* based on this flag, that view is returned instantly.
|
||||
* @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match
|
||||
* is found. If so, and if acceptableBoundFlags is non-zero, the
|
||||
* last matching acceptable view is returned. Otherwise, null is
|
||||
* returned.
|
||||
* @return The first view that satisfies acceptableBoundFlags or the last view satisfying
|
||||
* acceptableBoundFlags boundary conditions.
|
||||
*/
|
||||
View findOneViewWithinBoundFlags(int fromIndex, int toIndex,
|
||||
@ViewBounds int preferredBoundFlags,
|
||||
@ViewBounds int acceptableBoundFlags) {
|
||||
final int start = mCallback.getParentStart();
|
||||
final int end = mCallback.getParentEnd();
|
||||
final int next = toIndex > fromIndex ? 1 : -1;
|
||||
View acceptableMatch = null;
|
||||
for (int i = fromIndex; i != toIndex; i += next) {
|
||||
final View child = mCallback.getChildAt(i);
|
||||
final int childStart = mCallback.getChildStart(child);
|
||||
final int childEnd = mCallback.getChildEnd(child);
|
||||
mBoundFlags.setBounds(start, end, childStart, childEnd);
|
||||
if (preferredBoundFlags != 0) {
|
||||
mBoundFlags.resetFlags();
|
||||
mBoundFlags.addFlags(preferredBoundFlags);
|
||||
if (mBoundFlags.boundsMatch()) {
|
||||
// found a perfect match
|
||||
return child;
|
||||
}
|
||||
}
|
||||
if (acceptableBoundFlags != 0) {
|
||||
mBoundFlags.resetFlags();
|
||||
mBoundFlags.addFlags(acceptableBoundFlags);
|
||||
if (mBoundFlags.boundsMatch()) {
|
||||
acceptableMatch = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return acceptableMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified view lies within the boundary condition of its parent view.
|
||||
* @param child The child view to be checked.
|
||||
* @param boundsFlags The flag against which the child view and parent view are matched.
|
||||
* @return True if the view meets the boundsFlag, false otherwise.
|
||||
*/
|
||||
boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) {
|
||||
mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(),
|
||||
mCallback.getChildStart(child), mCallback.getChildEnd(child));
|
||||
if (boundsFlags != 0) {
|
||||
mBoundFlags.resetFlags();
|
||||
mBoundFlags.addFlags(boundsFlags);
|
||||
return mBoundFlags.boundsMatch();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback provided by the user of this class in order to retrieve information about child and
|
||||
* parent boundaries.
|
||||
*/
|
||||
interface Callback {
|
||||
View getChildAt(int index);
|
||||
int getParentStart();
|
||||
int getParentEnd();
|
||||
int getChildStart(View view);
|
||||
int getChildEnd(View view);
|
||||
}
|
||||
}
|
||||
11
ui-utils/ToolbarBottomSheet/build.gradle.kts
Normal file
11
ui-utils/ToolbarBottomSheet/build.gradle.kts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.android)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.android.material)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.k9mail.ui.utils.bottomsheet"
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package app.k9mail.ui.utils.bottomsheet
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
|
||||
/**
|
||||
* Work around the fact that [BottomSheetCallback.onLayout] is not public.
|
||||
*/
|
||||
class LayoutAwareBottomSheetBehavior<V : View>(context: Context, attrs: AttributeSet?) :
|
||||
BottomSheetBehavior<V>(context, attrs) {
|
||||
|
||||
private val callbacks = mutableSetOf<LayoutAwareBottomSheetCallback>()
|
||||
|
||||
internal fun addBottomSheetCallback(callback: LayoutAwareBottomSheetCallback) {
|
||||
callbacks.add(callback)
|
||||
super.addBottomSheetCallback(callback)
|
||||
}
|
||||
|
||||
internal fun removeBottomSheetCallback(callback: LayoutAwareBottomSheetCallback) {
|
||||
super.removeBottomSheetCallback(callback)
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
|
||||
val layoutResult = super.onLayoutChild(parent, child, layoutDirection)
|
||||
|
||||
for (callback in callbacks) {
|
||||
callback.onBottomSheetLayout(child)
|
||||
}
|
||||
|
||||
return layoutResult
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <V : View> from(view: V): LayoutAwareBottomSheetBehavior<V> {
|
||||
val params = view.layoutParams
|
||||
require(params is CoordinatorLayout.LayoutParams) { "The view is not a child of CoordinatorLayout" }
|
||||
|
||||
val behavior = params.behavior
|
||||
require(behavior is LayoutAwareBottomSheetBehavior<*>) {
|
||||
"The view is not associated with ToolbarBottomSheetBehavior"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return behavior as LayoutAwareBottomSheetBehavior<V>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package app.k9mail.ui.utils.bottomsheet
|
||||
|
||||
import android.view.View
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
|
||||
/**
|
||||
* Extends [BottomSheetCallback] so we can notify listeners of layout events.
|
||||
*
|
||||
* See [LayoutAwareBottomSheetBehavior].
|
||||
*/
|
||||
internal abstract class LayoutAwareBottomSheetCallback : BottomSheetCallback() {
|
||||
abstract fun onBottomSheetLayout(bottomSheet: View)
|
||||
}
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
* Copyright (C) 2023 K-9 Mail contributors
|
||||
* Copyright (C) 2015 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 app.k9mail.ui.utils.bottomsheet
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.AccessibilityDelegateCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
|
||||
/**
|
||||
* Modal bottom sheet that displays a toolbar when it is fully expanded. Only meant to be used with
|
||||
* [ToolbarBottomSheetDialogFragment].
|
||||
*
|
||||
* Based on [com.google.android.material.bottomsheet.BottomSheetDialog].
|
||||
*/
|
||||
class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes theme: Int) :
|
||||
AppCompatDialog(context, getThemeResId(context, theme)) {
|
||||
|
||||
private var internalBehavior: LayoutAwareBottomSheetBehavior<FrameLayout>? = null
|
||||
val behavior: LayoutAwareBottomSheetBehavior<*>
|
||||
get() {
|
||||
if (internalBehavior == null) {
|
||||
ensureContainerAndBehavior()
|
||||
}
|
||||
|
||||
return internalBehavior ?: error("Missing ToolbarBottomSheetBehavior")
|
||||
}
|
||||
|
||||
private var container: FrameLayout? = null
|
||||
private var coordinator: CoordinatorLayout? = null
|
||||
private var bottomSheet: FrameLayout? = null
|
||||
|
||||
var toolbar: Toolbar? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* `true` if dismissing will perform the swipe down animation on the bottom sheet, rather than the window animation
|
||||
* for the dialog.
|
||||
*/
|
||||
var isDismissWithAnimation = false
|
||||
|
||||
private var toolbarVisibilityCallback: LayoutAwareBottomSheetCallback? = null
|
||||
|
||||
init {
|
||||
// We hide the title bar for any style configuration. Otherwise, there will be a gap above the bottom sheet
|
||||
// when it is expanded.
|
||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setCancelable(true)
|
||||
|
||||
val window = window
|
||||
if (window != null) {
|
||||
// The status bar should always be transparent because of the window animation.
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
|
||||
if (VERSION.SDK_INT < VERSION_CODES.M) {
|
||||
// It can be transparent for API 23 and above because we will handle switching the status
|
||||
// bar icons to light or dark as appropriate. For API 21 and API 22 we just set the
|
||||
// translucent status bar.
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
}
|
||||
|
||||
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setContentView(@LayoutRes layoutResID: Int) {
|
||||
error("Not supported")
|
||||
}
|
||||
|
||||
override fun setContentView(view: View) {
|
||||
super.setContentView(wrapInBottomSheet(view))
|
||||
}
|
||||
|
||||
override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {
|
||||
error("Not supported")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
internalBehavior?.let { behavior ->
|
||||
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
val window = window
|
||||
if (window != null) {
|
||||
container?.fitsSystemWindows = false
|
||||
coordinator?.fitsSystemWindows = false
|
||||
|
||||
val flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or flags
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function can be called from a few different use cases, including Swiping the dialog down or calling
|
||||
* `dismiss()` from a `BottomSheetDialogFragment`, tapping outside a dialog, etc…
|
||||
*
|
||||
* The default animation to dismiss this dialog is a fade-out transition through a
|
||||
* windowAnimation. Call `setDismissWithAnimation(true)` if you want to utilize the BottomSheet animation
|
||||
* instead.
|
||||
*
|
||||
* If this function is called from a swipe down interaction, or dismissWithAnimation is false,
|
||||
* then keep the default behavior.
|
||||
*
|
||||
* Else, since this is a terminal event which will finish this dialog, we override the attached
|
||||
* [BottomSheetCallback] to call this function, after [BottomSheetBehavior.STATE_HIDDEN] is set. This
|
||||
* will enforce the swipe down animation before canceling this dialog.
|
||||
*/
|
||||
override fun cancel() {
|
||||
val behavior = this.behavior
|
||||
|
||||
if (!isDismissWithAnimation || behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
super.cancel()
|
||||
} else {
|
||||
behavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureContainerAndBehavior(): FrameLayout? {
|
||||
if (container == null) {
|
||||
val container = View.inflate(context, R.layout.design_bottom_sheet_dialog, null) as FrameLayout
|
||||
this.container = container
|
||||
|
||||
coordinator = container.findViewById(R.id.coordinator) ?: error("View not found")
|
||||
val bottomSheet = container.findViewById<FrameLayout>(R.id.design_bottom_sheet) ?: error("View not found")
|
||||
this.bottomSheet = bottomSheet
|
||||
|
||||
toolbar = bottomSheet.findViewById(R.id.toolbar)
|
||||
|
||||
val behavior = LayoutAwareBottomSheetBehavior.from(bottomSheet).apply {
|
||||
addBottomSheetCallback(cancelDialogCallback)
|
||||
isHideable = true
|
||||
}
|
||||
this.internalBehavior = behavior
|
||||
|
||||
container.doOnLayout {
|
||||
behavior.peekHeight = container.height / 2
|
||||
}
|
||||
|
||||
bottomSheet.doOnLayout {
|
||||
// Don't draw the toolbar underneath the status bar if the bottom sheet doesn't cover the whole screen
|
||||
// anyway.
|
||||
if (bottomSheet.width < container.width) {
|
||||
container.fitsSystemWindows = true
|
||||
coordinator?.fitsSystemWindows = true
|
||||
setToolbarVisibilityCallback(topInset = 0)
|
||||
} else {
|
||||
container.fitsSystemWindows = false
|
||||
coordinator?.fitsSystemWindows = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun wrapInBottomSheet(view: View): View {
|
||||
ensureContainerAndBehavior()
|
||||
|
||||
val container = checkNotNull(container)
|
||||
val coordinator = checkNotNull(coordinator)
|
||||
val bottomSheet = checkNotNull(bottomSheet)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(bottomSheet) { _, windowInsets ->
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val topInset = insets.top
|
||||
|
||||
setToolbarVisibilityCallback(topInset)
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
while (bottomSheet.childCount > NUMBER_OF_OWN_BOTTOM_SHEET_CHILDREN) {
|
||||
bottomSheet.removeViewAt(0)
|
||||
}
|
||||
|
||||
bottomSheet.addView(view, 0)
|
||||
|
||||
// We treat the CoordinatorLayout as outside the dialog though it is technically inside
|
||||
coordinator.findViewById<View>(R.id.touch_outside).setOnClickListener {
|
||||
if (isShowing) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle accessibility events
|
||||
ViewCompat.setAccessibilityDelegate(
|
||||
bottomSheet,
|
||||
object : AccessibilityDelegateCompat() {
|
||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info)
|
||||
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS)
|
||||
info.isDismissable = true
|
||||
}
|
||||
|
||||
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
|
||||
if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS) {
|
||||
cancel()
|
||||
return true
|
||||
}
|
||||
|
||||
return super.performAccessibilityAction(host, action, args)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
bottomSheet.setOnTouchListener { _, _ ->
|
||||
// Consume the event and prevent it from falling through
|
||||
true
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
private fun setToolbarVisibilityCallback(topInset: Int) {
|
||||
val window = checkNotNull(window)
|
||||
val bottomSheet = checkNotNull(bottomSheet)
|
||||
val toolbar = checkNotNull(toolbar)
|
||||
val behavior = checkNotNull(internalBehavior)
|
||||
|
||||
toolbarVisibilityCallback?.let { oldCallback ->
|
||||
behavior.removeBottomSheetCallback(oldCallback)
|
||||
}
|
||||
|
||||
val windowInsetsController = WindowCompat.getInsetsController(window, bottomSheet)
|
||||
val newCallback = ToolbarVisibilityCallback(windowInsetsController, toolbar, topInset, behavior)
|
||||
behavior.addBottomSheetCallback(newCallback)
|
||||
|
||||
this.toolbarVisibilityCallback = newCallback
|
||||
}
|
||||
|
||||
fun removeDefaultCallback() {
|
||||
checkNotNull(internalBehavior).removeBottomSheetCallback(cancelDialogCallback)
|
||||
}
|
||||
|
||||
private val cancelDialogCallback: BottomSheetCallback = object : BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, @BottomSheetBehavior.State newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||
}
|
||||
|
||||
private class ToolbarVisibilityCallback(
|
||||
private val windowInsetsController: WindowInsetsControllerCompat,
|
||||
private val toolbar: Toolbar,
|
||||
private val topInset: Int,
|
||||
private val behavior: BottomSheetBehavior<FrameLayout>
|
||||
) : LayoutAwareBottomSheetCallback() {
|
||||
|
||||
private val lightStatusBar: Boolean = windowInsetsController.isAppearanceLightStatusBars
|
||||
private var lightToolbar = false
|
||||
|
||||
init {
|
||||
// Try to find the background color to automatically change the status bar icons so they will
|
||||
// still be visible when the bottomsheet slides underneath the status bar.
|
||||
val toolbarBackground = toolbar.background
|
||||
val backgroundTint = if (toolbarBackground is MaterialShapeDrawable) {
|
||||
toolbarBackground.fillColor
|
||||
} else {
|
||||
ViewCompat.getBackgroundTintList(toolbar)
|
||||
}
|
||||
|
||||
lightToolbar = if (backgroundTint != null) {
|
||||
// First check for a tint
|
||||
MaterialColors.isColorLight(backgroundTint.defaultColor)
|
||||
} else if (toolbar.background is ColorDrawable) {
|
||||
// Then check for the background color
|
||||
MaterialColors.isColorLight((toolbar.background as ColorDrawable).color)
|
||||
} else {
|
||||
// Otherwise don't change the status bar color
|
||||
lightStatusBar
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
handleToolbarVisibility(bottomSheet)
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
handleToolbarVisibility(bottomSheet)
|
||||
}
|
||||
|
||||
override fun onBottomSheetLayout(bottomSheet: View) {
|
||||
handleToolbarVisibility(bottomSheet)
|
||||
}
|
||||
|
||||
private fun handleToolbarVisibility(bottomSheet: View) {
|
||||
setToolbarState(bottomSheet)
|
||||
setStatusBarColor(bottomSheet)
|
||||
}
|
||||
|
||||
private fun setToolbarState(bottomSheet: View) {
|
||||
val top = bottomSheet.top
|
||||
|
||||
val collapsedOffset = (bottomSheet.parent as View).height / 2
|
||||
if (top >= collapsedOffset || behavior.expandedOffset > 0) {
|
||||
toolbar.isInvisible = true
|
||||
bottomSheet.setPadding(0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
val toolbarHeight = toolbar.height - toolbar.paddingTop
|
||||
val toolbarHeightAndInset = toolbarHeight + topInset
|
||||
val expandedPercentage =
|
||||
((collapsedOffset - top).toFloat() / (collapsedOffset - behavior.expandedOffset)).coerceAtMost(1f)
|
||||
|
||||
// Add top padding to bottom sheet to make room for the toolbar
|
||||
val paddingTop = (toolbarHeightAndInset * expandedPercentage).toInt().coerceAtLeast(0)
|
||||
bottomSheet.setPadding(0, paddingTop, 0, 0)
|
||||
|
||||
// Start showing the toolbar when the bottom sheet is a toolbar height away from the top of the screen.
|
||||
// This value was chosen rather arbitrarily because it looked nice enough.
|
||||
val toolbarPercentage =
|
||||
((toolbarHeight - top).toFloat() / (toolbarHeight - behavior.expandedOffset)).coerceAtLeast(0f)
|
||||
|
||||
if (toolbarPercentage > 0) {
|
||||
toolbar.isVisible = true
|
||||
|
||||
// Set the toolbar's top padding so the toolbar covers the bottom sheet's whole top padding
|
||||
val toolbarPaddingTop = (paddingTop - toolbarHeight).coerceAtLeast(0)
|
||||
toolbar.setPadding(0, toolbarPaddingTop, 0, 0)
|
||||
|
||||
// Translate the toolbar view so it is drawn on top of the bottom sheet's top padding
|
||||
toolbar.translationY = -paddingTop.toFloat()
|
||||
|
||||
toolbar.alpha = toolbarPercentage
|
||||
} else {
|
||||
toolbar.isInvisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setStatusBarColor(bottomSheet: View) {
|
||||
val toolbarLightThreshold = topInset / 2
|
||||
if (bottomSheet.top < toolbarLightThreshold) {
|
||||
windowInsetsController.isAppearanceLightStatusBars = lightToolbar
|
||||
} else if (bottomSheet.top != 0) {
|
||||
windowInsetsController.isAppearanceLightStatusBars = lightStatusBar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NUMBER_OF_OWN_BOTTOM_SHEET_CHILDREN = 1
|
||||
|
||||
private fun getThemeResId(context: Context, themeId: Int): Int {
|
||||
if (themeId != 0) return themeId
|
||||
|
||||
val outValue = TypedValue()
|
||||
|
||||
val wasAttributeResolved = context.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.bottomSheetDialogTheme,
|
||||
outValue,
|
||||
true
|
||||
)
|
||||
|
||||
return if (wasAttributeResolved) {
|
||||
outValue.resourceId
|
||||
} else {
|
||||
error("Missing bottomSheetDialogTheme attribute in theme")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2023 K-9 Mail contributors
|
||||
* Copyright (C) 2015 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 app.k9mail.ui.utils.bottomsheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
||||
/**
|
||||
* Modal bottom sheet that displays a toolbar when it is fully expanded.
|
||||
*
|
||||
* Based on [com.google.android.material.bottomsheet.BottomSheetDialogFragment].
|
||||
*/
|
||||
open class ToolbarBottomSheetDialogFragment : AppCompatDialogFragment() {
|
||||
/**
|
||||
* Tracks if we are waiting for a dismissAllowingStateLoss or a regular dismiss once the BottomSheet is hidden and
|
||||
* onStateChanged() is called.
|
||||
*/
|
||||
private var waitingForDismissAllowingStateLoss = false
|
||||
|
||||
val toolbar: Toolbar?
|
||||
get() = dialog?.toolbar
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return ToolbarBottomSheetDialog(requireContext(), theme)
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
if (!tryDismissWithAnimation(false)) {
|
||||
super.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismissAllowingStateLoss() {
|
||||
if (!tryDismissWithAnimation(true)) {
|
||||
super.dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDialog(): ToolbarBottomSheetDialog? {
|
||||
return super.getDialog() as ToolbarBottomSheetDialog?
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible, false otherwise.
|
||||
*/
|
||||
private fun tryDismissWithAnimation(allowingStateLoss: Boolean): Boolean {
|
||||
val dialog = dialog
|
||||
if (dialog != null) {
|
||||
val behavior = dialog.behavior
|
||||
if (behavior.isHideable && dialog.isDismissWithAnimation) {
|
||||
dismissWithAnimation(behavior, allowingStateLoss)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun dismissWithAnimation(behavior: BottomSheetBehavior<*>, allowingStateLoss: Boolean) {
|
||||
waitingForDismissAllowingStateLoss = allowingStateLoss
|
||||
|
||||
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
dismissAfterAnimation()
|
||||
} else {
|
||||
dialog?.removeDefaultCallback()
|
||||
behavior.addBottomSheetCallback(BottomSheetDismissCallback())
|
||||
behavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissAfterAnimation() {
|
||||
if (waitingForDismissAllowingStateLoss) {
|
||||
super.dismissAllowingStateLoss()
|
||||
} else {
|
||||
super.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class BottomSheetDismissCallback : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
dismissAfterAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2023 K-9 Mail contributors
|
||||
~ Copyright (C) 2015 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.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<View
|
||||
android:id="@+id/touch_outside"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:focusable="false"
|
||||
android:importantForAccessibility="no"
|
||||
android:soundEffectsEnabled="false"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/design_bottom_sheet"
|
||||
style="?attr/bottomSheetStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|top"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_behavior="app.k9mail.ui.utils.bottomsheet.LayoutAwareBottomSheetBehavior">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="?attr/toolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</FrameLayout>
|
||||
Loading…
Add table
Add a link
Reference in a new issue