WindowAlignment.java
/*
* Copyright 2021 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 androidx.leanback.widget;
import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_BOTH_EDGE;
import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_HIGH_EDGE;
import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_LOW_EDGE;
import static androidx.leanback.widget.BaseGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED;
import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL;
/**
* Maintains Window Alignment information of two axis.
*/
final class WindowAlignment {
/**
* Maintains alignment information in one direction.
*/
static final class Axis {
private static final int PF_KEYLINE_OVER_LOW_EDGE = 1;
private static final int PF_KEYLINE_OVER_HIGH_EDGE = 1 << 1;
/**
* Right or bottom edge of last child.
*/
private int mMaxEdge;
/**
* Left or top edge of first child
*/
private int mMinEdge;
/**
* Scroll distance to align last child, it defines limit of scroll.
*/
private int mMaxScroll;
/**
* Scroll distance to align first child, it defines limit of scroll.
*/
private int mMinScroll;
/**
* By default we prefer low edge over keyline, prefer keyline over high edge.
*/
private int mPreferredKeyLine = PF_KEYLINE_OVER_HIGH_EDGE;
private int mWindowAlignment = WINDOW_ALIGN_BOTH_EDGE;
private int mWindowAlignmentOffset = 0;
private float mWindowAlignmentOffsetPercent = 50f;
private int mSize;
/**
* Padding at the min edge, it is the left or top padding.
*/
private int mPaddingMin;
/**
* Padding at the max edge, it is the right or bottom padding.
*/
private int mPaddingMax;
private boolean mReversedFlow;
Axis(String name) {
reset();
}
public int getWindowAlignment() {
return mWindowAlignment;
}
public void setWindowAlignment(int windowAlignment) {
mWindowAlignment = windowAlignment;
}
void setPreferKeylineOverLowEdge(boolean keylineOverLowEdge) {
mPreferredKeyLine = keylineOverLowEdge
? mPreferredKeyLine | PF_KEYLINE_OVER_LOW_EDGE
: mPreferredKeyLine & ~PF_KEYLINE_OVER_LOW_EDGE;
}
void setPreferKeylineOverHighEdge(boolean keylineOverHighEdge) {
mPreferredKeyLine = keylineOverHighEdge
? mPreferredKeyLine | PF_KEYLINE_OVER_HIGH_EDGE
: mPreferredKeyLine & ~PF_KEYLINE_OVER_HIGH_EDGE;
}
boolean isPreferKeylineOverHighEdge() {
return (mPreferredKeyLine & PF_KEYLINE_OVER_HIGH_EDGE) != 0;
}
boolean isPreferKeylineOverLowEdge() {
return (mPreferredKeyLine & PF_KEYLINE_OVER_LOW_EDGE) != 0;
}
public int getWindowAlignmentOffset() {
return mWindowAlignmentOffset;
}
public void setWindowAlignmentOffset(int offset) {
mWindowAlignmentOffset = offset;
}
public void setWindowAlignmentOffsetPercent(float percent) {
if ((percent < 0 || percent > 100)
&& percent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
throw new IllegalArgumentException();
}
mWindowAlignmentOffsetPercent = percent;
}
public float getWindowAlignmentOffsetPercent() {
return mWindowAlignmentOffsetPercent;
}
/**
* Returns scroll distance to align min child.
*/
public int getMinScroll() {
return mMinScroll;
}
public void invalidateScrollMin() {
mMinEdge = Integer.MIN_VALUE;
mMinScroll = Integer.MIN_VALUE;
}
/**
* Returns scroll distance to align max child.
*/
public int getMaxScroll() {
return mMaxScroll;
}
public void invalidateScrollMax() {
mMaxEdge = Integer.MAX_VALUE;
mMaxScroll = Integer.MAX_VALUE;
}
void reset() {
mMinEdge = Integer.MIN_VALUE;
mMaxEdge = Integer.MAX_VALUE;
}
public boolean isMinUnknown() {
return mMinEdge == Integer.MIN_VALUE;
}
public boolean isMaxUnknown() {
return mMaxEdge == Integer.MAX_VALUE;
}
public void setSize(int size) {
mSize = size;
}
public int getSize() {
return mSize;
}
public void setPadding(int paddingMin, int paddingMax) {
mPaddingMin = paddingMin;
mPaddingMax = paddingMax;
}
public int getPaddingMin() {
return mPaddingMin;
}
public int getPaddingMax() {
return mPaddingMax;
}
public int getClientSize() {
return mSize - mPaddingMin - mPaddingMax;
}
int calculateKeyline() {
int keyLine;
if (!mReversedFlow) {
if (mWindowAlignmentOffset >= 0) {
keyLine = mWindowAlignmentOffset;
} else {
keyLine = mSize + mWindowAlignmentOffset;
}
if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
keyLine += (int) (mSize * mWindowAlignmentOffsetPercent / 100);
}
} else {
if (mWindowAlignmentOffset >= 0) {
keyLine = mSize - mWindowAlignmentOffset;
} else {
keyLine = -mWindowAlignmentOffset;
}
if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
keyLine -= (int) (mSize * mWindowAlignmentOffsetPercent / 100);
}
}
return keyLine;
}
/**
* Returns scroll distance to move viewCenterPosition to keyLine.
*/
int calculateScrollToKeyLine(int viewCenterPosition, int keyLine) {
return viewCenterPosition - keyLine;
}
/**
* Update {@link #getMinScroll()} and {@link #getMaxScroll()}
*/
public void updateMinMax(int minEdge, int maxEdge,
int minChildViewCenter, int maxChildViewCenter) {
mMinEdge = minEdge;
mMaxEdge = maxEdge;
final int clientSize = getClientSize();
final int keyLine = calculateKeyline();
final boolean isMinUnknown = isMinUnknown();
final boolean isMaxUnknown = isMaxUnknown();
if (!isMinUnknown) {
if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
: (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
// calculate scroll distance to move current mMinEdge to padding at min edge
mMinScroll = mMinEdge - mPaddingMin;
} else {
// calculate scroll distance to move min child center to key line
mMinScroll = calculateScrollToKeyLine(minChildViewCenter, keyLine);
}
}
if (!isMaxUnknown) {
if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
: (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
// calculate scroll distance to move current mMaxEdge to padding at max edge
mMaxScroll = mMaxEdge - mPaddingMin - clientSize;
} else {
// calculate scroll distance to move max child center to key line
mMaxScroll = calculateScrollToKeyLine(maxChildViewCenter, keyLine);
}
}
if (!isMaxUnknown && !isMinUnknown) {
if (!mReversedFlow) {
if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
if (isPreferKeylineOverLowEdge()) {
// if we prefer key line, might align max child to key line for
// minScroll
mMinScroll = Math.min(mMinScroll,
calculateScrollToKeyLine(maxChildViewCenter, keyLine));
}
// don't over scroll max
mMaxScroll = Math.max(mMinScroll, mMaxScroll);
} else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
if (isPreferKeylineOverHighEdge()) {
// if we prefer key line, might align min child to key line for
// maxScroll
mMaxScroll = Math.max(mMaxScroll,
calculateScrollToKeyLine(minChildViewCenter, keyLine));
}
// don't over scroll min
mMinScroll = Math.min(mMinScroll, mMaxScroll);
}
} else {
if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
if (isPreferKeylineOverLowEdge()) {
// if we prefer key line, might align min child to key line for
// maxScroll
mMaxScroll = Math.max(mMaxScroll,
calculateScrollToKeyLine(minChildViewCenter, keyLine));
}
// don't over scroll min
mMinScroll = Math.min(mMinScroll, mMaxScroll);
} else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
if (isPreferKeylineOverHighEdge()) {
// if we prefer key line, might align max child to key line for
// minScroll
mMinScroll = Math.min(mMinScroll,
calculateScrollToKeyLine(maxChildViewCenter, keyLine));
}
// don't over scroll max
mMaxScroll = Math.max(mMinScroll, mMaxScroll);
}
}
}
}
/**
* Get scroll distance of align an item (depends on ALIGN_LOW_EDGE, ALIGN_HIGH_EDGE or the
* item should be aligned to key line). The scroll distance will be capped by
* {@link #getMinScroll()} and {@link #getMaxScroll()}.
*/
public int getScroll(int viewCenter) {
final int size = getSize();
final int keyLine = calculateKeyline();
final boolean isMinUnknown = isMinUnknown();
final boolean isMaxUnknown = isMaxUnknown();
if (!isMinUnknown) {
final int keyLineToMinEdge = keyLine - mPaddingMin;
if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
: (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0)
&& (viewCenter - mMinEdge <= keyLineToMinEdge)) {
// view center is before key line: align the min edge (first child) to padding.
int alignToMin = mMinEdge - mPaddingMin;
// Also we need make sure don't over scroll
if (!isMaxUnknown && alignToMin > mMaxScroll) {
alignToMin = mMaxScroll;
}
return alignToMin;
}
}
if (!isMaxUnknown) {
final int keyLineToMaxEdge = size - keyLine - mPaddingMax;
if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
: (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0)
&& (mMaxEdge - viewCenter <= keyLineToMaxEdge)) {
// view center is after key line: align the max edge (last child) to padding.
int alignToMax = mMaxEdge - (size - mPaddingMax);
// Also we need make sure don't over scroll
if (!isMinUnknown && alignToMax < mMinScroll) {
alignToMax = mMinScroll;
}
return alignToMax;
}
}
// else put view center at key line.
return calculateScrollToKeyLine(viewCenter, keyLine);
}
public void setReversedFlow(boolean reversedFlow) {
mReversedFlow = reversedFlow;
}
@Override
public String toString() {
return " min:" + mMinEdge + " " + mMinScroll + " max:" + mMaxEdge + " " + mMaxScroll;
}
}
private int mOrientation = HORIZONTAL;
public final Axis vertical = new Axis("vertical");
public final Axis horizontal = new Axis("horizontal");
private Axis mMainAxis = horizontal;
private Axis mSecondAxis = vertical;
public Axis mainAxis() {
return mMainAxis;
}
public Axis secondAxis() {
return mSecondAxis;
}
public void setOrientation(int orientation) {
mOrientation = orientation;
if (mOrientation == HORIZONTAL) {
mMainAxis = horizontal;
mSecondAxis = vertical;
} else {
mMainAxis = vertical;
mSecondAxis = horizontal;
}
}
public int getOrientation() {
return mOrientation;
}
public void reset() {
mainAxis().reset();
}
@Override
public String toString() {
return "horizontal=" + horizontal + "; vertical=" + vertical;
}
}