Draw vertical data usage sweep labels.

Using template string, ask axis to build label for sweeps.  Also
clean up margins exposed to parent view, since we offset when label
is larger than sweep drawable.

Bug: 4598460
Change-Id: If71bc8ec8c952023325c80b9bc00b63c23609c7a
This commit is contained in:
Jeff Sharkey
2011-07-09 01:02:56 -07:00
parent 890e352d5f
commit ec3be8a4a5
8 changed files with 226 additions and 89 deletions

View File

@@ -55,24 +55,14 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
settings:sweepDrawable="@drawable/data_sweep_left" settings:sweepDrawable="@drawable/data_sweep_left"
settings:followAxis="horizontal" settings:followAxis="horizontal" />
settings:showLabel="false" />
<com.android.settings.widget.ChartSweepView <com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_right" android:id="@+id/sweep_right"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
settings:sweepDrawable="@drawable/data_sweep_right" settings:sweepDrawable="@drawable/data_sweep_right"
settings:followAxis="horizontal" settings:followAxis="horizontal" />
settings:showLabel="false" />
<com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_limit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
settings:sweepDrawable="@drawable/data_sweep_limit"
settings:followAxis="vertical"
settings:showLabel="true" />
<com.android.settings.widget.ChartSweepView <com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_warning" android:id="@+id/sweep_warning"
@@ -80,6 +70,18 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
settings:sweepDrawable="@drawable/data_sweep_warning" settings:sweepDrawable="@drawable/data_sweep_warning"
settings:followAxis="vertical" settings:followAxis="vertical"
settings:showLabel="true" /> settings:labelSize="60dip"
settings:labelTemplate="@string/data_usage_sweep_warning"
settings:labelColor="#f7931d" />
<com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_limit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
settings:sweepDrawable="@drawable/data_sweep_limit"
settings:followAxis="vertical"
settings:labelSize="60dip"
settings:labelTemplate="@string/data_usage_sweep_limit"
settings:labelColor="#c01a2c" />
</com.android.settings.widget.DataUsageChartView> </com.android.settings.widget.DataUsageChartView>

View File

@@ -56,14 +56,16 @@
<enum name="horizontal" value="0" /> <enum name="horizontal" value="0" />
<enum name="vertical" value="1" /> <enum name="vertical" value="1" />
</attr> </attr>
<attr name="showLabel" format="boolean" /> <attr name="labelSize" format="dimension" />
<attr name="labelTemplate" format="reference" />
<attr name="labelColor" format="color" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ChartGridView"> <declare-styleable name="ChartGridView">
<attr name="primaryDrawable" format="reference" /> <attr name="primaryDrawable" format="reference" />
<attr name="secondaryDrawable" format="reference" /> <attr name="secondaryDrawable" format="reference" />
<attr name="borderDrawable" format="reference" /> <attr name="borderDrawable" format="reference" />
<attr name="labelColor" format="color" /> <attr name="labelColor" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ChartNetworkSeriesView"> <declare-styleable name="ChartNetworkSeriesView">

View File

@@ -3420,7 +3420,9 @@ found in the list of installed applications.</string>
<string name="data_usage_disabled_dialog_enable">Re-enable data</string> <string name="data_usage_disabled_dialog_enable">Re-enable data</string>
<!-- Label displaying current network data usage warning threshold. [CHAR LIMIT=18] --> <!-- Label displaying current network data usage warning threshold. [CHAR LIMIT=18] -->
<string name="data_usage_sweep_warning"><font size="32"><xliff:g id="number" example="128">%1$s</xliff:g></font> <font size="12"><xliff:g id="unit" example="KB">%2$s</xliff:g></font>\n<font size="12">warning</font></string> <string name="data_usage_sweep_warning"><font size="21"><xliff:g id="number" example="128">^1</xliff:g></font> <font size="9"><xliff:g id="unit" example="KB">^2</xliff:g></font>\n<font size="12">warning</font></string>
<!-- Label displaying current network data usage limit threshold. [CHAR LIMIT=18] -->
<string name="data_usage_sweep_limit"><font size="21"><xliff:g id="number" example="128">^1</xliff:g></font> <font size="9"><xliff:g id="unit" example="KB">^2</xliff:g></font>\n<font size="12">limit</font></string>
<!-- Button at the bottom of the CryptKeeper screen to make an emergency call. --> <!-- Button at the bottom of the CryptKeeper screen to make an emergency call. -->
<string name="cryptkeeper_emergency_call">Emergency call</string> <string name="cryptkeeper_emergency_call">Emergency call</string>

View File

@@ -16,6 +16,9 @@
package com.android.settings.widget; package com.android.settings.widget;
import android.content.res.Resources;
import android.text.SpannableStringBuilder;
/** /**
* Axis along a {@link ChartView} that knows how to convert between raw point * Axis along a {@link ChartView} that knows how to convert between raw point
* and screen coordinate systems. * and screen coordinate systems.
@@ -28,8 +31,7 @@ public interface ChartAxis {
public float convertToPoint(long value); public float convertToPoint(long value);
public long convertToValue(float point); public long convertToValue(float point);
public CharSequence getLabel(long value); public void buildLabel(Resources res, SpannableStringBuilder builder, long value);
public CharSequence getShortLabel(long value);
public float[] getTickPoints(); public float[] getTickPoints();

View File

@@ -19,8 +19,15 @@ package com.android.settings.widget;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.DynamicLayout;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.MathUtils; import android.util.MathUtils;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -36,13 +43,20 @@ import com.google.common.base.Preconditions;
*/ */
public class ChartSweepView extends FrameLayout { public class ChartSweepView extends FrameLayout {
// TODO: paint label when requested
private Drawable mSweep; private Drawable mSweep;
private Rect mSweepMargins = new Rect(); private Rect mSweepPadding = new Rect();
private Point mSweepOffset = new Point();
private Rect mMargins = new Rect();
private int mFollowAxis; private int mFollowAxis;
private boolean mShowLabel;
private int mLabelSize;
private int mLabelTemplateRes;
private int mLabelColor;
private SpannableStringBuilder mLabelTemplate;
private DynamicLayout mLabelLayout;
private ChartAxis mAxis; private ChartAxis mAxis;
private long mValue; private long mValue;
@@ -73,7 +87,10 @@ public class ChartSweepView extends FrameLayout {
setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable)); setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable));
setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
setShowLabel(a.getBoolean(R.styleable.ChartSweepView_showLabel, false));
setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0));
setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
a.recycle(); a.recycle();
@@ -90,27 +107,23 @@ public class ChartSweepView extends FrameLayout {
return mFollowAxis; return mFollowAxis;
} }
/** public Rect getMargins() {
* Return margins of {@link #setSweepDrawable(Drawable)}, indicating how the return mMargins;
* sweep should be displayed around a content region.
*/
public Rect getSweepMargins() {
return mSweepMargins;
} }
/** /**
* Return the number of pixels that the "target" area is inset from the * Return the number of pixels that the "target" area is inset from the
* {@link View} edge, along the current {@link #setFollowAxis(int)}. * {@link View} edge, along the current {@link #setFollowAxis(int)}.
*/ */
public float getTargetInset() { private float getTargetInset() {
if (mFollowAxis == VERTICAL) { if (mFollowAxis == VERTICAL) {
final float targetHeight = mSweep.getIntrinsicHeight() - mSweepMargins.top final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
- mSweepMargins.bottom; - mSweepPadding.bottom;
return mSweepMargins.top + (targetHeight / 2); return mSweepPadding.top + (targetHeight / 2);
} else { } else {
final float targetWidth = mSweep.getIntrinsicWidth() - mSweepMargins.left final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
- mSweepMargins.right; - mSweepPadding.right;
return mSweepMargins.left + (targetWidth / 2); return mSweepPadding.left + (targetWidth / 2);
} }
} }
@@ -124,6 +137,12 @@ public class ChartSweepView extends FrameLayout {
} }
} }
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
requestLayout();
}
public void setSweepDrawable(Drawable sweep) { public void setSweepDrawable(Drawable sweep) {
if (mSweep != null) { if (mSweep != null) {
mSweep.setCallback(null); mSweep.setCallback(null);
@@ -137,7 +156,7 @@ public class ChartSweepView extends FrameLayout {
} }
sweep.setVisible(getVisibility() == VISIBLE, false); sweep.setVisible(getVisibility() == VISIBLE, false);
mSweep = sweep; mSweep = sweep;
sweep.getPadding(mSweepMargins); sweep.getPadding(mSweepPadding);
} else { } else {
mSweep = null; mSweep = null;
} }
@@ -149,9 +168,49 @@ public class ChartSweepView extends FrameLayout {
mFollowAxis = followAxis; mFollowAxis = followAxis;
} }
public void setShowLabel(boolean showLabel) { public void setLabelSize(int size) {
mShowLabel = showLabel; mLabelSize = size;
invalidateLabelTemplate();
}
public void setLabelTemplate(int resId) {
mLabelTemplateRes = resId;
invalidateLabelTemplate();
}
public void setLabelColor(int color) {
mLabelColor = color;
invalidateLabelTemplate();
}
private void invalidateLabelTemplate() {
if (mLabelTemplateRes != 0) {
final CharSequence template = getResources().getText(mLabelTemplateRes);
final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
paint.density = getResources().getDisplayMetrics().density;
paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
paint.setColor(mLabelColor);
mLabelTemplate = new SpannableStringBuilder(template);
mLabelLayout = new DynamicLayout(
mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false);
invalidateLabel();
} else {
mLabelTemplate = null;
mLabelLayout = null;
}
invalidate(); invalidate();
requestLayout();
}
private void invalidateLabel() {
if (mLabelTemplate != null && mAxis != null) {
mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
invalidate();
}
} }
@Override @Override
@@ -181,6 +240,7 @@ public class ChartSweepView extends FrameLayout {
public void setValue(long value) { public void setValue(long value) {
mValue = value; mValue = value;
invalidateLabel();
} }
public long getValue() { public long getValue() {
@@ -207,9 +267,9 @@ public class ChartSweepView extends FrameLayout {
// only start tracking when in sweet spot // only start tracking when in sweet spot
final boolean accept; final boolean accept;
if (mFollowAxis == VERTICAL) { if (mFollowAxis == VERTICAL) {
accept = event.getX() > getWidth() - (mSweepMargins.right * 2); accept = event.getX() > getWidth() - (mSweepPadding.right * 2);
} else { } else {
accept = event.getY() > getHeight() - (mSweepMargins.bottom * 2); accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2);
} }
if (accept) { if (accept) {
@@ -222,42 +282,40 @@ public class ChartSweepView extends FrameLayout {
case MotionEvent.ACTION_MOVE: { case MotionEvent.ACTION_MOVE: {
getParent().requestDisallowInterceptTouchEvent(true); getParent().requestDisallowInterceptTouchEvent(true);
final Rect sweepMargins = mSweepMargins;
// content area of parent // content area of parent
final Rect parentContent = new Rect(parent.getPaddingLeft(), parent.getPaddingTop(), final Rect parentContent = new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getWidth() - parent.getPaddingRight(), parent.getWidth() - parent.getPaddingRight(),
parent.getHeight() - parent.getPaddingBottom()); parent.getHeight() - parent.getPaddingBottom());
if (mFollowAxis == VERTICAL) { if (mFollowAxis == VERTICAL) {
final float currentTargetY = getTop() + getTargetInset(); final float currentTargetY = getTop() - mMargins.top;
final float requestedTargetY = currentTargetY final float requestedTargetY = currentTargetY
+ (event.getRawY() - mTracking.getRawY()); + (event.getRawY() - mTracking.getRawY());
final float clampedTargetY = MathUtils.constrain( final float clampedTargetY = MathUtils.constrain(
requestedTargetY, parentContent.top, parentContent.bottom); requestedTargetY, parentContent.top, parentContent.bottom);
setTranslationY(clampedTargetY - currentTargetY); setTranslationY(clampedTargetY - currentTargetY);
mValue = mAxis.convertToValue(clampedTargetY - parentContent.top); setValue(mAxis.convertToValue(clampedTargetY - parentContent.top));
dispatchOnSweep(false);
} else { } else {
final float currentTargetX = getLeft() + getTargetInset(); final float currentTargetX = getLeft() - mMargins.left;
final float requestedTargetX = currentTargetX final float requestedTargetX = currentTargetX
+ (event.getRawX() - mTracking.getRawX()); + (event.getRawX() - mTracking.getRawX());
final float clampedTargetX = MathUtils.constrain( final float clampedTargetX = MathUtils.constrain(
requestedTargetX, parentContent.left, parentContent.right); requestedTargetX, parentContent.left, parentContent.right);
setTranslationX(clampedTargetX - currentTargetX); setTranslationX(clampedTargetX - currentTargetX);
mValue = mAxis.convertToValue(clampedTargetX - parentContent.left); setValue(mAxis.convertToValue(clampedTargetX - parentContent.left));
dispatchOnSweep(false);
} }
dispatchOnSweep(false);
return true; return true;
} }
case MotionEvent.ACTION_UP: { case MotionEvent.ACTION_UP: {
mTracking = null; mTracking = null;
dispatchOnSweep(true);
setTranslationX(0); setTranslationX(0);
setTranslationY(0); setTranslationY(0);
requestLayout(); requestLayout();
dispatchOnSweep(true);
return true; return true;
} }
default: { default: {
@@ -276,15 +334,62 @@ public class ChartSweepView extends FrameLayout {
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO: handle vertical labels
if (isEnabled() && mLabelLayout != null) {
final int sweepHeight = mSweep.getIntrinsicHeight();
final int templateHeight = mLabelLayout.getHeight();
mSweepOffset.x = 0;
mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
} else {
mSweepOffset.x = 0;
mSweepOffset.y = 0;
setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight()); setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight());
} }
if (mFollowAxis == VERTICAL) {
final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
- mSweepPadding.bottom;
mMargins.top = -(mSweepPadding.top + (targetHeight / 2));
mMargins.bottom = 0;
mMargins.left = -mSweepPadding.left;
mMargins.right = mSweepPadding.right;
} else {
final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
- mSweepPadding.right;
mMargins.left = -(mSweepPadding.left + (targetWidth / 2));
mMargins.right = 0;
mMargins.top = -mSweepPadding.top;
mMargins.bottom = mSweepPadding.bottom;
}
mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
}
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
final int width = getWidth(); final int width = getWidth();
final int height = getHeight(); final int height = getHeight();
mSweep.setBounds(0, 0, width, height); final int labelSize;
if (isEnabled() && mLabelLayout != null) {
mLabelLayout.draw(canvas);
labelSize = mLabelSize;
} else {
labelSize = 0;
}
if (mFollowAxis == VERTICAL) {
mSweep.setBounds(labelSize, mSweepOffset.y, width,
mSweepOffset.y + mSweep.getIntrinsicHeight());
} else {
mSweep.setBounds(mSweepOffset.x, labelSize,
mSweepOffset.x + mSweep.getIntrinsicWidth(), height);
}
mSweep.draw(canvas); mSweep.draw(canvas);
} }

View File

@@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@@ -93,22 +92,22 @@ public class ChartView extends FrameLayout {
} else if (child instanceof ChartSweepView) { } else if (child instanceof ChartSweepView) {
// sweep is always placed along specific dimension // sweep is always placed along specific dimension
final ChartSweepView sweep = (ChartSweepView) child; final ChartSweepView sweep = (ChartSweepView) child;
final Rect sweepMargins = sweep.getSweepMargins(); final Rect sweepMargins = sweep.getMargins();
if (sweep.getFollowAxis() == ChartSweepView.HORIZONTAL) { if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) {
parentRect.left = parentRect.right = parentRect.top += sweepMargins.top + (int) sweep.getPoint();
(int) (sweep.getPoint() - sweep.getTargetInset()) + getPaddingLeft(); parentRect.bottom = parentRect.top;
parentRect.top -= sweepMargins.top; parentRect.left += sweepMargins.left;
parentRect.bottom += sweepMargins.bottom; parentRect.right += sweepMargins.right;
Gravity.apply(SWEEP_GRAVITY, child.getMeasuredWidth(), parentRect.height(), Gravity.apply(SWEEP_GRAVITY, parentRect.width(), child.getMeasuredHeight(),
parentRect, childRect); parentRect, childRect);
} else { } else {
parentRect.top = parentRect.bottom = parentRect.left += sweepMargins.left + (int) sweep.getPoint();
(int) (sweep.getPoint() - sweep.getTargetInset()) + getPaddingTop(); parentRect.right = parentRect.left;
parentRect.left -= sweepMargins.left; parentRect.top += sweepMargins.top;
parentRect.right += sweepMargins.right; parentRect.bottom += sweepMargins.bottom;
Gravity.apply(SWEEP_GRAVITY, parentRect.width(), child.getMeasuredHeight(), Gravity.apply(SWEEP_GRAVITY, child.getMeasuredWidth(), parentRect.height(),
parentRect, childRect); parentRect, childRect);
} }
} }

View File

@@ -17,8 +17,12 @@
package com.android.settings.widget; package com.android.settings.widget;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.net.NetworkPolicy; import android.net.NetworkPolicy;
import android.net.NetworkStatsHistory; import android.net.NetworkStatsHistory;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -283,15 +287,9 @@ public class DataUsageChartView extends ChartView {
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
public CharSequence getLabel(long value) { public void buildLabel(Resources res, SpannableStringBuilder builder, long value) {
// TODO: convert to string // TODO: convert to better string
return Long.toString(value); builder.replace(0, builder.length(), Long.toString(value));
}
/** {@inheritDoc} */
public CharSequence getShortLabel(long value) {
// TODO: convert to string
return Long.toString(value);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@@ -345,16 +343,33 @@ public class DataUsageChartView extends ChartView {
return (long) fraction; return (long) fraction;
} }
/** {@inheritDoc} */ private static final Object sSpanSize = new Object();
public CharSequence getLabel(long value) { private static final Object sSpanUnit = new Object();
// TODO: use exploded string here
return Long.toString(value);
}
/** {@inheritDoc} */ /** {@inheritDoc} */
public CharSequence getShortLabel(long value) { public void buildLabel(Resources res, SpannableStringBuilder builder, long value) {
// TODO: convert to string
return Long.toString(value); float result = value;
final CharSequence unit;
if (result <= 100 * MB_IN_BYTES) {
unit = res.getText(com.android.internal.R.string.megabyteShort);
result /= MB_IN_BYTES;
} else {
unit = res.getText(com.android.internal.R.string.gigabyteShort);
result /= GB_IN_BYTES;
}
final CharSequence size;
if (result < 10) {
size = String.format("%.1f", result);
} else {
size = String.format("%.0f", result);
}
final int[] sizeBounds = findOrCreateSpan(builder, sSpanSize, "^1");
builder.replace(sizeBounds[0], sizeBounds[1], size);
final int[] unitBounds = findOrCreateSpan(builder, sSpanUnit, "^2");
builder.replace(unitBounds[0], unitBounds[1], unit);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@@ -372,4 +387,16 @@ public class DataUsageChartView extends ChartView {
} }
} }
private static int[] findOrCreateSpan(
SpannableStringBuilder builder, Object key, CharSequence bootstrap) {
int start = builder.getSpanStart(key);
int end = builder.getSpanEnd(key);
if (start == -1) {
start = TextUtils.indexOf(builder, bootstrap);
end = start + bootstrap.length();
builder.setSpan(key, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
return new int[] { start, end };
}
} }

View File

@@ -16,6 +16,9 @@
package com.android.settings.widget; package com.android.settings.widget;
import android.content.res.Resources;
import android.text.SpannableStringBuilder;
/** /**
* Utility to invert another {@link ChartAxis}. * Utility to invert another {@link ChartAxis}.
*/ */
@@ -49,13 +52,8 @@ public class InvertedChartAxis implements ChartAxis {
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
public CharSequence getLabel(long value) { public void buildLabel(Resources res, SpannableStringBuilder builder, long value) {
return mWrapped.getLabel(value); mWrapped.buildLabel(res, builder, value);
}
/** {@inheritDoc} */
public CharSequence getShortLabel(long value) {
return mWrapped.getShortLabel(value);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */