fix: 引入Settings的Module
This commit is contained in:
92
Settings/src/com/android/settings/widget/AddPreference.java
Normal file
92
Settings/src/com/android/settings/widget/AddPreference.java
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.RestrictedPreference;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
/**
|
||||
* A preference with a plus button on the side representing an "add" action. The plus button will
|
||||
* only be visible when a non-null click listener is registered.
|
||||
*/
|
||||
public class AddPreference extends RestrictedPreference implements View.OnClickListener {
|
||||
|
||||
private OnAddClickListener mListener;
|
||||
private View mWidgetFrame;
|
||||
private View mAddWidget;
|
||||
|
||||
public AddPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int getAddWidgetResId() {
|
||||
return R.id.add_preference_widget;
|
||||
}
|
||||
|
||||
/** Sets a listener for clicks on the plus button. Passing null will cause the button to be
|
||||
* hidden. */
|
||||
public void setOnAddClickListener(OnAddClickListener listener) {
|
||||
mListener = listener;
|
||||
if (mWidgetFrame != null) {
|
||||
mWidgetFrame.setVisibility(shouldHideSecondTarget() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAddWidgetEnabled(boolean enabled) {
|
||||
if (mAddWidget != null) {
|
||||
mAddWidget.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSecondTargetResId() {
|
||||
return R.layout.preference_widget_add;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldHideSecondTarget() {
|
||||
return mListener == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mWidgetFrame = holder.findViewById(android.R.id.widget_frame);
|
||||
mAddWidget = holder.findViewById(getAddWidgetResId());
|
||||
mAddWidget.setEnabled(true);
|
||||
mAddWidget.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (view.getId() == getAddWidgetResId() && mListener != null) {
|
||||
mListener.onAddClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnAddClickListener {
|
||||
void onAddClick(AddPreference p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* {@link CheckBoxPreference} that used only to display app
|
||||
*/
|
||||
public class AppCheckBoxPreference extends CheckBoxPreference {
|
||||
public AppCheckBoxPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setLayoutResource(com.android.settingslib.widget.preference.app.R.layout.preference_app);
|
||||
}
|
||||
|
||||
public AppCheckBoxPreference(Context context) {
|
||||
super(context);
|
||||
setLayoutResource(com.android.settingslib.widget.preference.app.R.layout.preference_app);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
final LinearLayout layout = (LinearLayout) holder.findViewById(R.id.summary_container);
|
||||
if (layout != null) {
|
||||
// If summary doesn't exist, make it gone
|
||||
layout.setVisibility(TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* A {@link FrameLayout} with customizable aspect ratio.
|
||||
* This is used to avoid dynamically calculating the height for the frame. Default aspect
|
||||
* ratio will be 1 if none is set in layout attribute.
|
||||
*/
|
||||
public final class AspectRatioFrameLayout extends FrameLayout {
|
||||
|
||||
private static final float ASPECT_RATIO_CHANGE_THREASHOLD = 0.01f;
|
||||
|
||||
@VisibleForTesting
|
||||
float mAspectRatio = 1.0f;
|
||||
|
||||
public AspectRatioFrameLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AspectRatioFrameLayout(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AspectRatioFrameLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
if (attrs != null) {
|
||||
TypedArray array =
|
||||
context.obtainStyledAttributes(attrs, R.styleable.AspectRatioFrameLayout);
|
||||
mAspectRatio = array.getFloat(
|
||||
R.styleable.AspectRatioFrameLayout_aspectRatio, 1.0f);
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAspectRatio(float aspectRadio) {
|
||||
mAspectRatio = aspectRadio;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int width = getMeasuredWidth();
|
||||
int height = getMeasuredHeight();
|
||||
if (width == 0 || height == 0) {
|
||||
return;
|
||||
}
|
||||
final float viewAspectRatio = (float) width / height;
|
||||
final float aspectRatioDiff = mAspectRatio - viewAspectRatio;
|
||||
if (Math.abs(aspectRatioDiff) <= ASPECT_RATIO_CHANGE_THREASHOLD) {
|
||||
// Close enough, skip.
|
||||
return;
|
||||
}
|
||||
|
||||
width = (int) (height * mAspectRatio);
|
||||
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* An extension of LinearLayout that automatically switches to vertical
|
||||
* orientation when it can't fit its child views horizontally.
|
||||
*
|
||||
* Main logic in this class comes from {@link androidx.appcompat.widget.ButtonBarLayout}.
|
||||
* Compared with {@link androidx.appcompat.widget.ButtonBarLayout}, this layout won't reverse
|
||||
* children's order and won't update the minimum height
|
||||
*/
|
||||
public class BottomLabelLayout extends LinearLayout {
|
||||
private static final String TAG = "BottomLabelLayout";
|
||||
|
||||
public BottomLabelLayout(Context context,
|
||||
@Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
final boolean isStacked = isStacked();
|
||||
boolean needsRemeasure = false;
|
||||
|
||||
// If we're not stacked, make sure the measure spec is AT_MOST rather
|
||||
// than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
|
||||
// know to stack the buttons.
|
||||
final int initialWidthMeasureSpec;
|
||||
if (!isStacked && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
|
||||
initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
|
||||
|
||||
// We'll need to remeasure again to fill excess space.
|
||||
needsRemeasure = true;
|
||||
} else {
|
||||
initialWidthMeasureSpec = widthMeasureSpec;
|
||||
}
|
||||
|
||||
super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
|
||||
if (!isStacked) {
|
||||
final int measuredWidth = getMeasuredWidthAndState();
|
||||
final int measuredWidthState = measuredWidth & View.MEASURED_STATE_MASK;
|
||||
|
||||
if (measuredWidthState == View.MEASURED_STATE_TOO_SMALL) {
|
||||
setStacked(true);
|
||||
// Measure again in the new orientation.
|
||||
needsRemeasure = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRemeasure) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setStacked(boolean stacked) {
|
||||
setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
|
||||
setGravity(stacked ? Gravity.START : Gravity.BOTTOM);
|
||||
|
||||
final View spacer = findViewById(com.android.settings.R.id.spacer);
|
||||
if (spacer != null) {
|
||||
spacer.setVisibility(stacked ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStacked() {
|
||||
return getOrientation() == LinearLayout.VERTICAL;
|
||||
}
|
||||
}
|
||||
123
Settings/src/com/android/settings/widget/CardPreference.kt
Normal file
123
Settings/src/com/android/settings/widget/CardPreference.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 com.android.settings.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.util.AttributeSet
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.android.settings.spa.preference.ComposePreference
|
||||
import com.android.settingslib.spa.widget.card.CardButton
|
||||
import com.android.settingslib.spa.widget.card.CardModel
|
||||
import com.android.settingslib.spa.widget.card.SettingsCard
|
||||
|
||||
/** A preference for settings banner tips card. */
|
||||
class CardPreference
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
context: Context,
|
||||
attr: AttributeSet? = null,
|
||||
) : ComposePreference(context, attr) {
|
||||
|
||||
/** A icon resource id for displaying icon on tips card. */
|
||||
var iconResId: Int? = null
|
||||
|
||||
/** The primary button's text. */
|
||||
var primaryButtonText: String = ""
|
||||
|
||||
/** The accessibility content description of the primary button. */
|
||||
var primaryButtonContentDescription: String? = null
|
||||
|
||||
/** The action for click on primary button. */
|
||||
var primaryButtonAction: () -> Unit = {}
|
||||
|
||||
/** The visibility of primary button on tips card. The default value is `false`. */
|
||||
var primaryButtonVisibility: Boolean = false
|
||||
|
||||
/** The text on the second button of this [SettingsCard]. */
|
||||
var secondaryButtonText: String = ""
|
||||
|
||||
/** The accessibility content description of the secondary button. */
|
||||
var secondaryButtonContentDescription: String? = null
|
||||
|
||||
/** The action for click on secondary button. */
|
||||
var secondaryButtonAction: () -> Unit = {}
|
||||
|
||||
/** The visibility of secondary button on tips card. The default value is `false`. */
|
||||
var secondaryButtonVisibility: Boolean = false
|
||||
|
||||
var onClick: (() -> Unit)? = null
|
||||
|
||||
/** The callback for click on card preference itself. */
|
||||
private var onDismiss: (() -> Unit)? = null
|
||||
|
||||
/** Enable the dismiss button on tips card. */
|
||||
fun enableDismiss(enable: Boolean) =
|
||||
if (enable) onDismiss = { isVisible = false } else onDismiss = null
|
||||
|
||||
/** Clear layout state if needed. */
|
||||
fun resetLayoutState() {
|
||||
primaryButtonVisibility = false
|
||||
secondaryButtonVisibility = false
|
||||
notifyChanged()
|
||||
}
|
||||
|
||||
/** Build the tips card content to apply any changes of this card's property. */
|
||||
fun buildContent() {
|
||||
setContent {
|
||||
SettingsCard(
|
||||
CardModel(
|
||||
title = title?.toString() ?: "",
|
||||
text = summary?.toString() ?: "",
|
||||
buttons = listOfNotNull(configPrimaryButton(), configSecondaryButton()),
|
||||
onDismiss = onDismiss,
|
||||
imageVector =
|
||||
iconResId
|
||||
?.takeIf { it != Resources.ID_NULL }
|
||||
?.let { ImageVector.vectorResource(it) },
|
||||
onClick = onClick,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configPrimaryButton(): CardButton? {
|
||||
return if (primaryButtonVisibility)
|
||||
CardButton(
|
||||
text = primaryButtonText,
|
||||
contentDescription = primaryButtonContentDescription,
|
||||
onClick = primaryButtonAction,
|
||||
)
|
||||
else null
|
||||
}
|
||||
|
||||
private fun configSecondaryButton(): CardButton? {
|
||||
return if (secondaryButtonVisibility)
|
||||
CardButton(
|
||||
text = secondaryButtonText,
|
||||
contentDescription = secondaryButtonContentDescription,
|
||||
onClick = secondaryButtonAction,
|
||||
)
|
||||
else null
|
||||
}
|
||||
|
||||
override fun notifyChanged() {
|
||||
buildContent()
|
||||
super.notifyChanged()
|
||||
}
|
||||
}
|
||||
53
Settings/src/com/android/settings/widget/ChartAxis.java
Normal file
53
Settings/src/com/android/settings/widget/ChartAxis.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 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
|
||||
* and screen coordinate systems.
|
||||
*/
|
||||
public interface ChartAxis {
|
||||
|
||||
/** Set range of raw values this axis should cover. */
|
||||
public boolean setBounds(long min, long max);
|
||||
/** Set range of screen points this axis should cover. */
|
||||
public boolean setSize(float size);
|
||||
|
||||
/** Convert raw value into screen point. */
|
||||
public float convertToPoint(long value);
|
||||
/** Convert screen point into raw value. */
|
||||
public long convertToValue(float point);
|
||||
|
||||
/**
|
||||
* Build label that describes given raw value. If the label is rounded for
|
||||
* display, return the rounded value.
|
||||
*/
|
||||
public long buildLabel(Resources res, SpannableStringBuilder builder, long value);
|
||||
|
||||
/** Return list of tick points for drawing a grid. */
|
||||
public float[] getTickPoints();
|
||||
|
||||
/**
|
||||
* Test if given raw value should cause the axis to grow or shrink;
|
||||
* returning positive value to grow and negative to shrink.
|
||||
*/
|
||||
public int shouldAdjustAxis(long value);
|
||||
|
||||
}
|
||||
178
Settings/src/com/android/settings/widget/ChartGridView.java
Normal file
178
Settings/src/com/android/settings/widget/ChartGridView.java
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 com.android.settings.widget;
|
||||
|
||||
import static com.android.settings.Utils.formatDateRange;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* Background of {@link ChartView} that renders grid lines as requested by
|
||||
* {@link ChartAxis#getTickPoints()}.
|
||||
*/
|
||||
public class ChartGridView extends View {
|
||||
|
||||
private ChartAxis mHoriz;
|
||||
private ChartAxis mVert;
|
||||
|
||||
private Drawable mPrimary;
|
||||
private Drawable mSecondary;
|
||||
private Drawable mBorder;
|
||||
|
||||
private int mLabelSize;
|
||||
private int mLabelColor;
|
||||
|
||||
private Layout mLabelStart;
|
||||
private Layout mLabelMid;
|
||||
private Layout mLabelEnd;
|
||||
|
||||
public ChartGridView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public ChartGridView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ChartGridView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
setWillNotDraw(false);
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.ChartGridView, defStyle, 0);
|
||||
|
||||
mPrimary = a.getDrawable(R.styleable.ChartGridView_primaryDrawable);
|
||||
mSecondary = a.getDrawable(R.styleable.ChartGridView_secondaryDrawable);
|
||||
mBorder = a.getDrawable(R.styleable.ChartGridView_borderDrawable);
|
||||
|
||||
final int taId = a.getResourceId(R.styleable.ChartGridView_android_textAppearance, -1);
|
||||
final TypedArray ta = context.obtainStyledAttributes(taId,
|
||||
com.android.internal.R.styleable.TextAppearance);
|
||||
mLabelSize = ta.getDimensionPixelSize(
|
||||
com.android.internal.R.styleable.TextAppearance_textSize, 0);
|
||||
ta.recycle();
|
||||
|
||||
final ColorStateList labelColor = a.getColorStateList(
|
||||
R.styleable.ChartGridView_android_textColor);
|
||||
mLabelColor = labelColor.getDefaultColor();
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
void init(ChartAxis horiz, ChartAxis vert) {
|
||||
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
|
||||
mVert = Preconditions.checkNotNull(vert, "missing vert");
|
||||
}
|
||||
|
||||
void setBounds(long start, long end) {
|
||||
final Context context = getContext();
|
||||
final long mid = (start + end) / 2;
|
||||
mLabelStart = makeLabel(formatDateRange(context, start, start));
|
||||
mLabelMid = makeLabel(formatDateRange(context, mid, mid));
|
||||
mLabelEnd = makeLabel(formatDateRange(context, end, end));
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
final int width = getWidth();
|
||||
final int height = getHeight() - getPaddingBottom();
|
||||
|
||||
final Drawable secondary = mSecondary;
|
||||
if (secondary != null) {
|
||||
final int secondaryHeight = secondary.getIntrinsicHeight();
|
||||
|
||||
final float[] vertTicks = mVert.getTickPoints();
|
||||
for (float y : vertTicks) {
|
||||
final int bottom = (int) Math.min(y + secondaryHeight, height);
|
||||
secondary.setBounds(0, (int) y, width, bottom);
|
||||
secondary.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
final Drawable primary = mPrimary;
|
||||
if (primary != null) {
|
||||
final int primaryWidth = primary.getIntrinsicWidth();
|
||||
final int primaryHeight = primary.getIntrinsicHeight();
|
||||
|
||||
final float[] horizTicks = mHoriz.getTickPoints();
|
||||
for (float x : horizTicks) {
|
||||
final int right = (int) Math.min(x + primaryWidth, width);
|
||||
primary.setBounds((int) x, 0, right, height);
|
||||
primary.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
mBorder.setBounds(0, 0, width, height);
|
||||
mBorder.draw(canvas);
|
||||
|
||||
final int padding = mLabelStart != null ? mLabelStart.getHeight() / 8 : 0;
|
||||
|
||||
final Layout start = mLabelStart;
|
||||
if (start != null) {
|
||||
final int saveCount = canvas.save();
|
||||
canvas.translate(0, height + padding);
|
||||
start.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
final Layout mid = mLabelMid;
|
||||
if (mid != null) {
|
||||
final int saveCount = canvas.save();
|
||||
canvas.translate((width - mid.getWidth()) / 2, height + padding);
|
||||
mid.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
final Layout end = mLabelEnd;
|
||||
if (end != null) {
|
||||
final int saveCount = canvas.save();
|
||||
canvas.translate(width - end.getWidth(), height + padding);
|
||||
end.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
}
|
||||
|
||||
private Layout makeLabel(CharSequence text) {
|
||||
final Resources res = getResources();
|
||||
final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.density = res.getDisplayMetrics().density;
|
||||
paint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
|
||||
paint.setColor(mLabelColor);
|
||||
paint.setTextSize(mLabelSize);
|
||||
|
||||
return StaticLayout.Builder.obtain(text, 0, text.length(), paint,
|
||||
(int) Math.ceil(Layout.getDesiredWidth(text, paint)))
|
||||
.setUseLineSpacingFromFallbacks(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
754
Settings/src/com/android/settings/widget/ChartSweepView.java
Normal file
754
Settings/src/com/android/settings/widget/ChartSweepView.java
Normal file
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.DynamicLayout;
|
||||
import android.text.Layout;
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextPaint;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.MathUtils;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which
|
||||
* a user can drag.
|
||||
*/
|
||||
public class ChartSweepView extends View {
|
||||
|
||||
private static final boolean DRAW_OUTLINE = false;
|
||||
|
||||
// TODO: clean up all the various padding/offset/margins
|
||||
|
||||
private Drawable mSweep;
|
||||
private Rect mSweepPadding = new Rect();
|
||||
|
||||
/** Offset of content inside this view. */
|
||||
private Rect mContentOffset = new Rect();
|
||||
/** Offset of {@link #mSweep} inside this view. */
|
||||
private Point mSweepOffset = new Point();
|
||||
|
||||
private Rect mMargins = new Rect();
|
||||
private float mNeighborMargin;
|
||||
private int mSafeRegion;
|
||||
|
||||
private int mFollowAxis;
|
||||
|
||||
private int mLabelMinSize;
|
||||
private float mLabelSize;
|
||||
|
||||
private int mLabelTemplateRes;
|
||||
private int mLabelColor;
|
||||
|
||||
private SpannableStringBuilder mLabelTemplate;
|
||||
private DynamicLayout mLabelLayout;
|
||||
|
||||
private ChartAxis mAxis;
|
||||
private long mValue;
|
||||
private long mLabelValue;
|
||||
|
||||
private long mValidAfter;
|
||||
private long mValidBefore;
|
||||
private ChartSweepView mValidAfterDynamic;
|
||||
private ChartSweepView mValidBeforeDynamic;
|
||||
|
||||
private float mLabelOffset;
|
||||
|
||||
private Paint mOutlinePaint = new Paint();
|
||||
|
||||
public static final int HORIZONTAL = 0;
|
||||
public static final int VERTICAL = 1;
|
||||
|
||||
private int mTouchMode = MODE_NONE;
|
||||
|
||||
private static final int MODE_NONE = 0;
|
||||
private static final int MODE_DRAG = 1;
|
||||
private static final int MODE_LABEL = 2;
|
||||
|
||||
private static final int LARGE_WIDTH = 1024;
|
||||
|
||||
private long mDragInterval = 1;
|
||||
|
||||
public interface OnSweepListener {
|
||||
public void onSweep(ChartSweepView sweep, boolean sweepDone);
|
||||
public void requestEdit(ChartSweepView sweep);
|
||||
}
|
||||
|
||||
private OnSweepListener mListener;
|
||||
|
||||
private float mTrackingStart;
|
||||
private MotionEvent mTracking;
|
||||
|
||||
private ChartSweepView[] mNeighbors = new ChartSweepView[0];
|
||||
|
||||
public ChartSweepView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ChartSweepView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.ChartSweepView, defStyle, 0);
|
||||
|
||||
final int color = a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE);
|
||||
setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable), color);
|
||||
setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
|
||||
setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0));
|
||||
setSafeRegion(a.getDimensionPixelSize(R.styleable.ChartSweepView_safeRegion, 0));
|
||||
|
||||
setLabelMinSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0));
|
||||
setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
|
||||
setLabelColor(color);
|
||||
|
||||
// TODO: moved focused state directly into assets
|
||||
setBackgroundResource(R.drawable.data_usage_sweep_background);
|
||||
|
||||
mOutlinePaint.setColor(Color.RED);
|
||||
mOutlinePaint.setStrokeWidth(1f);
|
||||
mOutlinePaint.setStyle(Style.STROKE);
|
||||
|
||||
a.recycle();
|
||||
|
||||
setClickable(true);
|
||||
setOnClickListener(mClickListener);
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
private OnClickListener mClickListener = new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
dispatchRequestEdit();
|
||||
}
|
||||
};
|
||||
|
||||
void init(ChartAxis axis) {
|
||||
mAxis = Preconditions.checkNotNull(axis, "missing axis");
|
||||
}
|
||||
|
||||
public void setNeighbors(ChartSweepView... neighbors) {
|
||||
mNeighbors = neighbors;
|
||||
}
|
||||
|
||||
public int getFollowAxis() {
|
||||
return mFollowAxis;
|
||||
}
|
||||
|
||||
public Rect getMargins() {
|
||||
return mMargins;
|
||||
}
|
||||
|
||||
public void setDragInterval(long dragInterval) {
|
||||
mDragInterval = dragInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of pixels that the "target" area is inset from the
|
||||
* {@link View} edge, along the current {@link #setFollowAxis(int)}.
|
||||
*/
|
||||
private float getTargetInset() {
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
|
||||
- mSweepPadding.bottom;
|
||||
return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y;
|
||||
} else {
|
||||
final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
|
||||
- mSweepPadding.right;
|
||||
return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x;
|
||||
}
|
||||
}
|
||||
|
||||
public void addOnSweepListener(OnSweepListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private void dispatchOnSweep(boolean sweepDone) {
|
||||
if (mListener != null) {
|
||||
mListener.onSweep(this, sweepDone);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchRequestEdit() {
|
||||
if (mListener != null) {
|
||||
mListener.requestEdit(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
setFocusable(enabled);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void setSweepDrawable(Drawable sweep, int color) {
|
||||
if (mSweep != null) {
|
||||
mSweep.setCallback(null);
|
||||
unscheduleDrawable(mSweep);
|
||||
}
|
||||
|
||||
if (sweep != null) {
|
||||
sweep.setCallback(this);
|
||||
if (sweep.isStateful()) {
|
||||
sweep.setState(getDrawableState());
|
||||
}
|
||||
sweep.setVisible(getVisibility() == VISIBLE, false);
|
||||
mSweep = sweep;
|
||||
// Match the text.
|
||||
mSweep.setTint(color);
|
||||
sweep.getPadding(mSweepPadding);
|
||||
} else {
|
||||
mSweep = null;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setFollowAxis(int followAxis) {
|
||||
mFollowAxis = followAxis;
|
||||
}
|
||||
|
||||
public void setLabelMinSize(int minSize) {
|
||||
mLabelMinSize = minSize;
|
||||
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 = DynamicLayout.Builder.obtain(mLabelTemplate, paint, LARGE_WIDTH)
|
||||
.setAlignment(Alignment.ALIGN_RIGHT)
|
||||
.setIncludePad(false)
|
||||
.setUseLineSpacingFromFallbacks(true)
|
||||
.build();
|
||||
invalidateLabel();
|
||||
|
||||
} else {
|
||||
mLabelTemplate = null;
|
||||
mLabelLayout = null;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void invalidateLabel() {
|
||||
if (mLabelTemplate != null && mAxis != null) {
|
||||
mLabelValue = mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
|
||||
setContentDescription(mLabelTemplate);
|
||||
invalidateLabelOffset();
|
||||
invalidate();
|
||||
} else {
|
||||
mLabelValue = mValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When overlapping with neighbor, split difference and push label.
|
||||
*/
|
||||
public void invalidateLabelOffset() {
|
||||
float margin;
|
||||
float labelOffset = 0;
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
if (mValidAfterDynamic != null) {
|
||||
mLabelSize = Math.max(getLabelWidth(this), getLabelWidth(mValidAfterDynamic));
|
||||
margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this);
|
||||
if (margin < 0) {
|
||||
labelOffset = margin / 2;
|
||||
}
|
||||
} else if (mValidBeforeDynamic != null) {
|
||||
mLabelSize = Math.max(getLabelWidth(this), getLabelWidth(mValidBeforeDynamic));
|
||||
margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic);
|
||||
if (margin < 0) {
|
||||
labelOffset = -margin / 2;
|
||||
}
|
||||
} else {
|
||||
mLabelSize = getLabelWidth(this);
|
||||
}
|
||||
} else {
|
||||
// TODO: implement horizontal labels
|
||||
}
|
||||
|
||||
mLabelSize = Math.max(mLabelSize, mLabelMinSize);
|
||||
|
||||
// when offsetting label, neighbor probably needs to offset too
|
||||
if (labelOffset != mLabelOffset) {
|
||||
mLabelOffset = labelOffset;
|
||||
invalidate();
|
||||
if (mValidAfterDynamic != null) mValidAfterDynamic.invalidateLabelOffset();
|
||||
if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidateLabelOffset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jumpDrawablesToCurrentState() {
|
||||
super.jumpDrawablesToCurrentState();
|
||||
if (mSweep != null) {
|
||||
mSweep.jumpToCurrentState();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(int visibility) {
|
||||
super.setVisibility(visibility);
|
||||
if (mSweep != null) {
|
||||
mSweep.setVisible(visibility == VISIBLE, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean verifyDrawable(Drawable who) {
|
||||
return who == mSweep || super.verifyDrawable(who);
|
||||
}
|
||||
|
||||
public ChartAxis getAxis() {
|
||||
return mAxis;
|
||||
}
|
||||
|
||||
public void setValue(long value) {
|
||||
mValue = value;
|
||||
invalidateLabel();
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public long getLabelValue() {
|
||||
return mLabelValue;
|
||||
}
|
||||
|
||||
public float getPoint() {
|
||||
if (isEnabled()) {
|
||||
return mAxis.convertToPoint(mValue);
|
||||
} else {
|
||||
// when disabled, show along top edge
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set valid range this sweep can move within, in {@link #mAxis} values. The
|
||||
* most restrictive combination of all valid ranges is used.
|
||||
*/
|
||||
public void setValidRange(long validAfter, long validBefore) {
|
||||
mValidAfter = validAfter;
|
||||
mValidBefore = validBefore;
|
||||
}
|
||||
|
||||
public void setNeighborMargin(float neighborMargin) {
|
||||
mNeighborMargin = neighborMargin;
|
||||
}
|
||||
|
||||
public void setSafeRegion(int safeRegion) {
|
||||
mSafeRegion = safeRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set valid range this sweep can move within, defined by the given
|
||||
* {@link ChartSweepView}. The most restrictive combination of all valid
|
||||
* ranges is used.
|
||||
*/
|
||||
public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) {
|
||||
mValidAfterDynamic = validAfter;
|
||||
mValidBeforeDynamic = validBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if given {@link MotionEvent} is closer to another
|
||||
* {@link ChartSweepView} compared to ourselves.
|
||||
*/
|
||||
public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) {
|
||||
final float selfDist = getTouchDistanceFromTarget(eventInParent);
|
||||
final float anotherDist = another.getTouchDistanceFromTarget(eventInParent);
|
||||
return anotherDist < selfDist;
|
||||
}
|
||||
|
||||
private float getTouchDistanceFromTarget(MotionEvent eventInParent) {
|
||||
if (mFollowAxis == HORIZONTAL) {
|
||||
return Math.abs(eventInParent.getX() - (getX() + getTargetInset()));
|
||||
} else {
|
||||
return Math.abs(eventInParent.getY() - (getY() + getTargetInset()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (!isEnabled()) return false;
|
||||
|
||||
final View parent = (View) getParent();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
|
||||
// only start tracking when in sweet spot
|
||||
final boolean acceptDrag;
|
||||
final boolean acceptLabel;
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
acceptDrag = event.getX() > getWidth() - (mSweepPadding.right * 8);
|
||||
acceptLabel = mLabelLayout != null ? event.getX() < mLabelLayout.getWidth()
|
||||
: false;
|
||||
} else {
|
||||
acceptDrag = event.getY() > getHeight() - (mSweepPadding.bottom * 8);
|
||||
acceptLabel = mLabelLayout != null ? event.getY() < mLabelLayout.getHeight()
|
||||
: false;
|
||||
}
|
||||
|
||||
final MotionEvent eventInParent = event.copy();
|
||||
eventInParent.offsetLocation(getLeft(), getTop());
|
||||
|
||||
// ignore event when closer to a neighbor
|
||||
for (ChartSweepView neighbor : mNeighbors) {
|
||||
if (isTouchCloserTo(eventInParent, neighbor)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptDrag) {
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
mTrackingStart = getTop() - mMargins.top;
|
||||
} else {
|
||||
mTrackingStart = getLeft() - mMargins.left;
|
||||
}
|
||||
mTracking = event.copy();
|
||||
mTouchMode = MODE_DRAG;
|
||||
|
||||
// starting drag should activate entire chart
|
||||
if (!parent.isActivated()) {
|
||||
parent.setActivated(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (acceptLabel) {
|
||||
mTouchMode = MODE_LABEL;
|
||||
return true;
|
||||
} else {
|
||||
mTouchMode = MODE_NONE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (mTouchMode == MODE_LABEL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
|
||||
// content area of parent
|
||||
final Rect parentContent = getParentContentRect();
|
||||
final Rect clampRect = computeClampRect(parentContent);
|
||||
if (clampRect.isEmpty()) return true;
|
||||
|
||||
long value;
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
final float currentTargetY = getTop() - mMargins.top;
|
||||
final float requestedTargetY = mTrackingStart
|
||||
+ (event.getRawY() - mTracking.getRawY());
|
||||
final float clampedTargetY = MathUtils.constrain(
|
||||
requestedTargetY, clampRect.top, clampRect.bottom);
|
||||
setTranslationY(clampedTargetY - currentTargetY);
|
||||
|
||||
value = mAxis.convertToValue(clampedTargetY - parentContent.top);
|
||||
} else {
|
||||
final float currentTargetX = getLeft() - mMargins.left;
|
||||
final float requestedTargetX = mTrackingStart
|
||||
+ (event.getRawX() - mTracking.getRawX());
|
||||
final float clampedTargetX = MathUtils.constrain(
|
||||
requestedTargetX, clampRect.left, clampRect.right);
|
||||
setTranslationX(clampedTargetX - currentTargetX);
|
||||
|
||||
value = mAxis.convertToValue(clampedTargetX - parentContent.left);
|
||||
}
|
||||
|
||||
// round value from drag to nearest increment
|
||||
value -= value % mDragInterval;
|
||||
setValue(value);
|
||||
|
||||
dispatchOnSweep(false);
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (mTouchMode == MODE_LABEL) {
|
||||
performClick();
|
||||
} else if (mTouchMode == MODE_DRAG) {
|
||||
mTrackingStart = 0;
|
||||
mTracking = null;
|
||||
mValue = mLabelValue;
|
||||
dispatchOnSweep(true);
|
||||
setTranslationX(0);
|
||||
setTranslationY(0);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
mTouchMode = MODE_NONE;
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update {@link #mValue} based on current position, including any
|
||||
* {@link #onTouchEvent(MotionEvent)} in progress. Typically used when
|
||||
* {@link ChartAxis} changes during sweep adjustment.
|
||||
*/
|
||||
public void updateValueFromPosition() {
|
||||
final Rect parentContent = getParentContentRect();
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
final float effectiveY = getY() - mMargins.top - parentContent.top;
|
||||
setValue(mAxis.convertToValue(effectiveY));
|
||||
} else {
|
||||
final float effectiveX = getX() - mMargins.left - parentContent.left;
|
||||
setValue(mAxis.convertToValue(effectiveX));
|
||||
}
|
||||
}
|
||||
|
||||
public int shouldAdjustAxis() {
|
||||
return mAxis.shouldAdjustAxis(getValue());
|
||||
}
|
||||
|
||||
private Rect getParentContentRect() {
|
||||
final View parent = (View) getParent();
|
||||
return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
|
||||
parent.getWidth() - parent.getPaddingRight(),
|
||||
parent.getHeight() - parent.getPaddingBottom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
|
||||
// ignored to keep LayoutTransition from animating us
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
|
||||
// ignored to keep LayoutTransition from animating us
|
||||
}
|
||||
|
||||
private long getValidAfterDynamic() {
|
||||
final ChartSweepView dynamic = mValidAfterDynamic;
|
||||
return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE;
|
||||
}
|
||||
|
||||
private long getValidBeforeDynamic() {
|
||||
final ChartSweepView dynamic = mValidBeforeDynamic;
|
||||
return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute {@link Rect} in {@link #getParent()} coordinates that we should
|
||||
* be clamped inside of, usually from {@link #setValidRange(long, long)}
|
||||
* style rules.
|
||||
*/
|
||||
private Rect computeClampRect(Rect parentContent) {
|
||||
// create two rectangles, and pick most restrictive combination
|
||||
final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f);
|
||||
final Rect dynamicRect = buildClampRect(
|
||||
parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin);
|
||||
|
||||
if (!rect.intersect(dynamicRect)) {
|
||||
rect.setEmpty();
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
private Rect buildClampRect(
|
||||
Rect parentContent, long afterValue, long beforeValue, float margin) {
|
||||
if (mAxis instanceof InvertedChartAxis) {
|
||||
long temp = beforeValue;
|
||||
beforeValue = afterValue;
|
||||
afterValue = temp;
|
||||
}
|
||||
|
||||
final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE;
|
||||
final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE;
|
||||
|
||||
final float afterPoint = mAxis.convertToPoint(afterValue) + margin;
|
||||
final float beforePoint = mAxis.convertToPoint(beforeValue) - margin;
|
||||
|
||||
final Rect clampRect = new Rect(parentContent);
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint;
|
||||
if (afterValid) clampRect.top += afterPoint;
|
||||
} else {
|
||||
if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint;
|
||||
if (afterValid) clampRect.left += afterPoint;
|
||||
}
|
||||
return clampRect;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawableStateChanged() {
|
||||
super.drawableStateChanged();
|
||||
if (mSweep.isStateful()) {
|
||||
mSweep.setState(getDrawableState());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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 = 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());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
mContentOffset.set(0, 0, 0, 0);
|
||||
|
||||
// make touch target area larger
|
||||
final int widthBefore = getMeasuredWidth();
|
||||
final int heightBefore = getMeasuredHeight();
|
||||
if (mFollowAxis == HORIZONTAL) {
|
||||
final int widthAfter = widthBefore * 3;
|
||||
setMeasuredDimension(widthAfter, heightBefore);
|
||||
mContentOffset.left = (widthAfter - widthBefore) / 2;
|
||||
|
||||
final int offset = mSweepPadding.bottom * 2;
|
||||
mContentOffset.bottom -= offset;
|
||||
mMargins.bottom += offset;
|
||||
} else {
|
||||
final int heightAfter = heightBefore * 2;
|
||||
setMeasuredDimension(widthBefore, heightAfter);
|
||||
mContentOffset.offset(0, (heightAfter - heightBefore) / 2);
|
||||
|
||||
final int offset = mSweepPadding.right * 2;
|
||||
mContentOffset.right -= offset;
|
||||
mMargins.right += offset;
|
||||
}
|
||||
|
||||
mSweepOffset.offset(mContentOffset.left, mContentOffset.top);
|
||||
mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
invalidateLabelOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
|
||||
final int labelSize;
|
||||
if (isEnabled() && mLabelLayout != null) {
|
||||
final int count = canvas.save();
|
||||
{
|
||||
final float alignOffset = mLabelSize - LARGE_WIDTH;
|
||||
canvas.translate(
|
||||
mContentOffset.left + alignOffset, mContentOffset.top + mLabelOffset);
|
||||
mLabelLayout.draw(canvas);
|
||||
}
|
||||
canvas.restoreToCount(count);
|
||||
labelSize = (int) mLabelSize + mSafeRegion;
|
||||
} else {
|
||||
labelSize = 0;
|
||||
}
|
||||
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
mSweep.setBounds(labelSize, mSweepOffset.y, width + mContentOffset.right,
|
||||
mSweepOffset.y + mSweep.getIntrinsicHeight());
|
||||
} else {
|
||||
mSweep.setBounds(mSweepOffset.x, labelSize, mSweepOffset.x + mSweep.getIntrinsicWidth(),
|
||||
height + mContentOffset.bottom);
|
||||
}
|
||||
|
||||
mSweep.draw(canvas);
|
||||
|
||||
if (DRAW_OUTLINE) {
|
||||
mOutlinePaint.setColor(Color.RED);
|
||||
canvas.drawRect(0, 0, width, height, mOutlinePaint);
|
||||
}
|
||||
}
|
||||
|
||||
public static float getLabelTop(ChartSweepView view) {
|
||||
return view.getY() + view.mContentOffset.top;
|
||||
}
|
||||
|
||||
public static float getLabelBottom(ChartSweepView view) {
|
||||
return getLabelTop(view) + view.mLabelLayout.getHeight();
|
||||
}
|
||||
|
||||
public static float getLabelWidth(ChartSweepView view) {
|
||||
return Layout.getDesiredWidth(view.mLabelLayout.getText(), view.mLabelLayout.getPaint());
|
||||
}
|
||||
}
|
||||
157
Settings/src/com/android/settings/widget/ChartView.java
Normal file
157
Settings/src/com/android/settings/widget/ChartView.java
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewDebug;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* Container for two-dimensional chart, drawn with a combination of
|
||||
* {@link ChartGridView} and {@link ChartSweepView} children. The entire chart uses
|
||||
* {@link ChartAxis} to map between raw values
|
||||
* and screen coordinates.
|
||||
*/
|
||||
public class ChartView extends FrameLayout {
|
||||
// TODO: extend something that supports two-dimensional scrolling
|
||||
|
||||
private static final int SWEEP_GRAVITY = Gravity.TOP | Gravity.START;
|
||||
|
||||
ChartAxis mHoriz;
|
||||
ChartAxis mVert;
|
||||
|
||||
@ViewDebug.ExportedProperty
|
||||
private int mOptimalWidth = -1;
|
||||
private float mOptimalWidthWeight = 0;
|
||||
|
||||
private Rect mContent = new Rect();
|
||||
|
||||
public ChartView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public ChartView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ChartView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.ChartView, defStyle, 0);
|
||||
setOptimalWidth(a.getDimensionPixelSize(R.styleable.ChartView_optimalWidth, -1),
|
||||
a.getFloat(R.styleable.ChartView_optimalWidthWeight, 0));
|
||||
a.recycle();
|
||||
|
||||
setClipToPadding(false);
|
||||
setClipChildren(false);
|
||||
}
|
||||
|
||||
void init(ChartAxis horiz, ChartAxis vert) {
|
||||
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
|
||||
mVert = Preconditions.checkNotNull(vert, "missing vert");
|
||||
}
|
||||
|
||||
public void setOptimalWidth(int optimalWidth, float optimalWidthWeight) {
|
||||
mOptimalWidth = optimalWidth;
|
||||
mOptimalWidthWeight = optimalWidthWeight;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
final int slack = getMeasuredWidth() - mOptimalWidth;
|
||||
if (mOptimalWidth > 0 && slack > 0) {
|
||||
final int targetWidth = (int) (mOptimalWidth + (slack * mOptimalWidthWeight));
|
||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),
|
||||
b - t - getPaddingBottom());
|
||||
final int width = mContent.width();
|
||||
final int height = mContent.height();
|
||||
|
||||
// no scrolling yet, so tell dimensions to fill exactly
|
||||
mHoriz.setSize(width);
|
||||
mVert.setSize(height);
|
||||
|
||||
final Rect parentRect = new Rect();
|
||||
final Rect childRect = new Rect();
|
||||
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
final View child = getChildAt(i);
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
|
||||
parentRect.set(mContent);
|
||||
|
||||
if (child instanceof ChartGridView) {
|
||||
// Grid uses some extra room for labels
|
||||
Gravity.apply(params.gravity, width, height, parentRect, childRect);
|
||||
child.layout(childRect.left, childRect.top, childRect.right,
|
||||
childRect.bottom + child.getPaddingBottom());
|
||||
|
||||
} else if (child instanceof ChartSweepView) {
|
||||
layoutSweep((ChartSweepView) child, parentRect, childRect);
|
||||
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void layoutSweep(ChartSweepView sweep) {
|
||||
final Rect parentRect = new Rect(mContent);
|
||||
final Rect childRect = new Rect();
|
||||
|
||||
layoutSweep(sweep, parentRect, childRect);
|
||||
sweep.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
|
||||
}
|
||||
|
||||
protected void layoutSweep(ChartSweepView sweep, Rect parentRect, Rect childRect) {
|
||||
final Rect sweepMargins = sweep.getMargins();
|
||||
|
||||
// sweep is always placed along specific dimension
|
||||
if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) {
|
||||
parentRect.top += sweepMargins.top + (int) sweep.getPoint();
|
||||
parentRect.bottom = parentRect.top;
|
||||
parentRect.left += sweepMargins.left;
|
||||
parentRect.right += sweepMargins.right;
|
||||
Gravity.apply(SWEEP_GRAVITY, parentRect.width(), sweep.getMeasuredHeight(),
|
||||
parentRect, childRect);
|
||||
|
||||
} else {
|
||||
parentRect.left += sweepMargins.left + (int) sweep.getPoint();
|
||||
parentRect.right = parentRect.left;
|
||||
parentRect.top += sweepMargins.top;
|
||||
parentRect.bottom += sweepMargins.bottom;
|
||||
Gravity.apply(SWEEP_GRAVITY, sweep.getMeasuredWidth(), parentRect.height(),
|
||||
parentRect, childRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Checkable;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A RelativeLayout which implements {@link Checkable}. With this implementation, it could be used
|
||||
* in the list item layout for {@link android.widget.AbsListView} to change UI after item click.
|
||||
* Its checked state would be propagated to the checkable child.
|
||||
*
|
||||
* <p>
|
||||
* To support accessibility, the state description is from the checkable view and is
|
||||
* changed with {@link #setChecked(boolean)}. We make the checkable child unclickable, unfocusable
|
||||
* and non-important for accessibility, so that the announcement wouldn't include
|
||||
* the checkable view.
|
||||
* <
|
||||
*/
|
||||
public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
|
||||
|
||||
private Checkable mCheckable;
|
||||
private View mCheckableChild;
|
||||
private boolean mChecked;
|
||||
|
||||
public CheckableRelativeLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CheckableRelativeLayout(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
mCheckableChild = findFirstCheckableView(this);
|
||||
if (mCheckableChild != null) {
|
||||
mCheckableChild.setClickable(false);
|
||||
mCheckableChild.setFocusable(false);
|
||||
mCheckableChild.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
mCheckable = (Checkable) mCheckableChild;
|
||||
mCheckable.setChecked(isChecked());
|
||||
setStateDescriptionIfNeeded();
|
||||
}
|
||||
super.onFinishInflate();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static View findFirstCheckableView(@NonNull ViewGroup viewGroup) {
|
||||
final int childCount = viewGroup.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = viewGroup.getChildAt(i);
|
||||
if (child instanceof Checkable) {
|
||||
return child;
|
||||
}
|
||||
if (child instanceof ViewGroup) {
|
||||
findFirstCheckableView((ViewGroup) child);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
if (mChecked != checked) {
|
||||
mChecked = checked;
|
||||
if (mCheckable != null) {
|
||||
mCheckable.setChecked(checked);
|
||||
}
|
||||
}
|
||||
setStateDescriptionIfNeeded();
|
||||
notifyViewAccessibilityStateChangedIfNeeded(
|
||||
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
|
||||
}
|
||||
|
||||
private void setStateDescriptionIfNeeded() {
|
||||
if (mCheckableChild == null) {
|
||||
return;
|
||||
}
|
||||
setStateDescription(mCheckableChild.getStateDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return mChecked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggle() {
|
||||
setChecked(!mChecked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setChecked(mChecked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setChecked(mChecked);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
public class DefaultIndicatorSeekBar extends SeekBar {
|
||||
|
||||
private int mDefaultProgress = -1;
|
||||
|
||||
public DefaultIndicatorSeekBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public DefaultIndicatorSeekBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public DefaultIndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public DefaultIndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* N.B. Only draws the default indicator tick mark, NOT equally spaced tick marks.
|
||||
*/
|
||||
@Override
|
||||
protected void drawTickMarks(Canvas canvas) {
|
||||
if (isEnabled() && mDefaultProgress <= getMax() && mDefaultProgress >= getMin()) {
|
||||
final Drawable defaultIndicator = getTickMark();
|
||||
|
||||
// Adjust the drawable's bounds to center it at the point where it's drawn.
|
||||
final int w = defaultIndicator.getIntrinsicWidth();
|
||||
final int h = defaultIndicator.getIntrinsicHeight();
|
||||
final int halfW = w >= 0 ? w / 2 : 1;
|
||||
final int halfH = h >= 0 ? h / 2 : 1;
|
||||
defaultIndicator.setBounds(-halfW, -halfH, halfW, halfH);
|
||||
|
||||
// This mimics the computation of the thumb position, to get the true "default."
|
||||
final int availableWidth = getWidth() - mPaddingLeft - mPaddingRight;
|
||||
final int range = getMax() - getMin();
|
||||
final float scale = range > 0f ? mDefaultProgress / (float) range : 0f;
|
||||
final int offset = (int) ((scale * availableWidth) + 0.5f);
|
||||
final int indicatorPosition = isLayoutRtl() && getMirrorForRtl()
|
||||
? availableWidth - offset + mPaddingRight : offset + mPaddingLeft;
|
||||
|
||||
final int saveCount = canvas.save();
|
||||
canvas.translate(indicatorPosition, getHeight() / 2);
|
||||
defaultIndicator.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* N.B. This sets the default *unadjusted* progress, i.e. in the SeekBar's [0 - max] terms.
|
||||
*/
|
||||
public void setDefaultProgress(int defaultProgress) {
|
||||
if (mDefaultProgress != defaultProgress) {
|
||||
mDefaultProgress = defaultProgress;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public int getDefaultProgress() {
|
||||
return mDefaultProgress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
/**
|
||||
* A CheckboxPreference that can disable its checkbox separate from its text.
|
||||
* Differs from CheckboxPreference.setDisabled() in that the text is not dimmed.
|
||||
*/
|
||||
public class DisabledCheckBoxPreference extends CheckBoxPreference {
|
||||
private PreferenceViewHolder mViewHolder;
|
||||
private View mCheckBox;
|
||||
private boolean mEnabledCheckBox;
|
||||
|
||||
public DisabledCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setupDisabledCheckBoxPreference(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public DisabledCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setupDisabledCheckBoxPreference(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public DisabledCheckBoxPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setupDisabledCheckBoxPreference(context, attrs, 0, 0);
|
||||
}
|
||||
|
||||
public DisabledCheckBoxPreference(Context context) {
|
||||
super(context);
|
||||
setupDisabledCheckBoxPreference(context, null, 0, 0);
|
||||
}
|
||||
|
||||
private void setupDisabledCheckBoxPreference(Context context, AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
final TypedArray a = context.obtainStyledAttributes(
|
||||
attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes);
|
||||
for (int i = a.getIndexCount() - 1; i >= 0; i--) {
|
||||
int attr = a.getIndex(i);
|
||||
switch (attr) {
|
||||
case com.android.internal.R.styleable.Preference_enabled:
|
||||
mEnabledCheckBox = a.getBoolean(attr, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
a.recycle();
|
||||
|
||||
// Always tell super class this preference is enabled.
|
||||
// We manually enable/disable checkbox using enableCheckBox.
|
||||
super.setEnabled(true);
|
||||
enableCheckbox(mEnabledCheckBox);
|
||||
}
|
||||
|
||||
public void enableCheckbox(boolean enabled) {
|
||||
mEnabledCheckBox = enabled;
|
||||
if (mViewHolder != null && mCheckBox != null) {
|
||||
mCheckBox.setEnabled(mEnabledCheckBox);
|
||||
mViewHolder.itemView.setEnabled(mEnabledCheckBox);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mViewHolder = holder;
|
||||
mCheckBox = holder.findViewById(android.R.id.checkbox);
|
||||
|
||||
enableCheckbox(mEnabledCheckBox);
|
||||
|
||||
TextView title = (TextView) holder.findViewById(android.R.id.title);
|
||||
if (title != null) {
|
||||
title.setSingleLine(false);
|
||||
title.setMaxLines(2);
|
||||
title.setEllipsize(TextUtils.TruncateAt.END);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performClick(View view) {
|
||||
// only perform clicks if the checkbox is enabled
|
||||
if (mEnabledCheckBox) {
|
||||
super.performClick(view);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
920
Settings/src/com/android/settings/widget/DotsPageIndicator.java
Normal file
920
Settings/src/com/android/settings/widget/DotsPageIndicator.java
Normal file
@@ -0,0 +1,920 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import static android.view.animation.AnimationUtils.loadInterpolator;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.DataSetObserver;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Custom pager indicator for use with a {@code ViewPager}.
|
||||
*/
|
||||
public class DotsPageIndicator extends View implements ViewPager.OnPageChangeListener {
|
||||
|
||||
public static final String TAG = DotsPageIndicator.class.getSimpleName();
|
||||
|
||||
// defaults
|
||||
private static final int DEFAULT_DOT_SIZE = 8; // dp
|
||||
private static final int DEFAULT_GAP = 12; // dp
|
||||
private static final int DEFAULT_ANIM_DURATION = 400; // ms
|
||||
private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff; // 50% white
|
||||
private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff; // 100% white
|
||||
|
||||
// constants
|
||||
private static final float INVALID_FRACTION = -1f;
|
||||
private static final float MINIMAL_REVEAL = 0.00001f;
|
||||
|
||||
// configurable attributes
|
||||
private int dotDiameter;
|
||||
private int gap;
|
||||
private long animDuration;
|
||||
private int unselectedColour;
|
||||
private int selectedColour;
|
||||
|
||||
// derived from attributes
|
||||
private float dotRadius;
|
||||
private float halfDotRadius;
|
||||
private long animHalfDuration;
|
||||
private float dotTopY;
|
||||
private float dotCenterY;
|
||||
private float dotBottomY;
|
||||
|
||||
// ViewPager
|
||||
private ViewPager viewPager;
|
||||
private ViewPager.OnPageChangeListener pageChangeListener;
|
||||
|
||||
// state
|
||||
private int pageCount;
|
||||
private int currentPage;
|
||||
private float selectedDotX;
|
||||
private boolean selectedDotInPosition;
|
||||
private float[] dotCenterX;
|
||||
private float[] joiningFractions;
|
||||
private float retreatingJoinX1;
|
||||
private float retreatingJoinX2;
|
||||
private float[] dotRevealFractions;
|
||||
private boolean attachedState;
|
||||
|
||||
// drawing
|
||||
private final Paint unselectedPaint;
|
||||
private final Paint selectedPaint;
|
||||
private final Path combinedUnselectedPath;
|
||||
private final Path unselectedDotPath;
|
||||
private final Path unselectedDotLeftPath;
|
||||
private final Path unselectedDotRightPath;
|
||||
private final RectF rectF;
|
||||
|
||||
// animation
|
||||
private ValueAnimator moveAnimation;
|
||||
private ValueAnimator[] joiningAnimations;
|
||||
private AnimatorSet joiningAnimationSet;
|
||||
private PendingRetreatAnimator retreatAnimation;
|
||||
private PendingRevealAnimator[] revealAnimations;
|
||||
private final Interpolator interpolator;
|
||||
|
||||
// working values for beziers
|
||||
float endX1;
|
||||
float endY1;
|
||||
float endX2;
|
||||
float endY2;
|
||||
float controlX1;
|
||||
float controlY1;
|
||||
float controlX2;
|
||||
float controlY2;
|
||||
|
||||
public DotsPageIndicator(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public DotsPageIndicator(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public DotsPageIndicator(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
final int scaledDensity = (int) context.getResources().getDisplayMetrics().scaledDensity;
|
||||
|
||||
// Load attributes
|
||||
final TypedArray typedArray = getContext().obtainStyledAttributes(
|
||||
attrs, R.styleable.DotsPageIndicator, defStyle, 0);
|
||||
dotDiameter = typedArray.getDimensionPixelSize(R.styleable.DotsPageIndicator_dotDiameter,
|
||||
DEFAULT_DOT_SIZE * scaledDensity);
|
||||
dotRadius = dotDiameter / 2;
|
||||
halfDotRadius = dotRadius / 2;
|
||||
gap = typedArray.getDimensionPixelSize(R.styleable.DotsPageIndicator_dotGap,
|
||||
DEFAULT_GAP * scaledDensity);
|
||||
animDuration = (long) typedArray.getInteger(R.styleable.DotsPageIndicator_animationDuration,
|
||||
DEFAULT_ANIM_DURATION);
|
||||
animHalfDuration = animDuration / 2;
|
||||
unselectedColour = typedArray.getColor(R.styleable.DotsPageIndicator_pageIndicatorColor,
|
||||
DEFAULT_UNSELECTED_COLOUR);
|
||||
selectedColour = typedArray.getColor(R.styleable.DotsPageIndicator_currentPageIndicatorColor,
|
||||
DEFAULT_SELECTED_COLOUR);
|
||||
typedArray.recycle();
|
||||
unselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
unselectedPaint.setColor(unselectedColour);
|
||||
selectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
selectedPaint.setColor(selectedColour);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
interpolator = loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
|
||||
} else {
|
||||
interpolator = loadInterpolator(context, android.R.anim.accelerate_decelerate_interpolator);
|
||||
}
|
||||
|
||||
// create paths & rect now – reuse & rewind later
|
||||
combinedUnselectedPath = new Path();
|
||||
unselectedDotPath = new Path();
|
||||
unselectedDotLeftPath = new Path();
|
||||
unselectedDotRightPath = new Path();
|
||||
rectF = new RectF();
|
||||
|
||||
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
attachedState = true;
|
||||
}
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
attachedState = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setViewPager(ViewPager viewPager) {
|
||||
this.viewPager = viewPager;
|
||||
viewPager.setOnPageChangeListener(this);
|
||||
setPageCount(viewPager.getAdapter().getCount());
|
||||
viewPager.getAdapter().registerDataSetObserver(new DataSetObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
setPageCount(DotsPageIndicator.this.viewPager.getAdapter().getCount());
|
||||
}
|
||||
});
|
||||
setCurrentPageImmediate();
|
||||
}
|
||||
|
||||
/***
|
||||
* As this class <b>must</b> act as the {@link ViewPager.OnPageChangeListener} for the ViewPager
|
||||
* (as set by {@link #setViewPager(androidx.viewpager.widget.ViewPager)}). Applications may set a
|
||||
* listener here to be notified of the ViewPager events.
|
||||
*
|
||||
* @param onPageChangeListener
|
||||
*/
|
||||
public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
|
||||
pageChangeListener = onPageChangeListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
// nothing to do – just forward onward to any registered listener
|
||||
if (pageChangeListener != null) {
|
||||
pageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (attachedState) {
|
||||
// this is the main event we're interested in!
|
||||
setSelectedPage(position);
|
||||
} else {
|
||||
// when not attached, don't animate the move, just store immediately
|
||||
setCurrentPageImmediate();
|
||||
}
|
||||
|
||||
// forward onward to any registered listener
|
||||
if (pageChangeListener != null) {
|
||||
pageChangeListener.onPageSelected(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
// nothing to do – just forward onward to any registered listener
|
||||
if (pageChangeListener != null) {
|
||||
pageChangeListener.onPageScrollStateChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPageCount(int pages) {
|
||||
pageCount = pages;
|
||||
calculateDotPositions();
|
||||
resetState();
|
||||
}
|
||||
|
||||
private void calculateDotPositions() {
|
||||
int left = getPaddingLeft();
|
||||
int top = getPaddingTop();
|
||||
int right = getWidth() - getPaddingRight();
|
||||
int requiredWidth = getRequiredWidth();
|
||||
float startLeft = left + ((right - left - requiredWidth) / 2) + dotRadius;
|
||||
dotCenterX = new float[pageCount];
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
dotCenterX[i] = startLeft + i * (dotDiameter + gap);
|
||||
}
|
||||
// todo just top aligning for now… should make this smarter
|
||||
dotTopY = top;
|
||||
dotCenterY = top + dotRadius;
|
||||
dotBottomY = top + dotDiameter;
|
||||
setCurrentPageImmediate();
|
||||
}
|
||||
|
||||
private void setCurrentPageImmediate() {
|
||||
if (viewPager != null) {
|
||||
currentPage = viewPager.getCurrentItem();
|
||||
} else {
|
||||
currentPage = 0;
|
||||
}
|
||||
|
||||
if (pageCount > 0) {
|
||||
selectedDotX = dotCenterX[currentPage];
|
||||
}
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
if (pageCount > 0) {
|
||||
joiningFractions = new float[pageCount - 1];
|
||||
Arrays.fill(joiningFractions, 0f);
|
||||
dotRevealFractions = new float[pageCount];
|
||||
Arrays.fill(dotRevealFractions, 0f);
|
||||
retreatingJoinX1 = INVALID_FRACTION;
|
||||
retreatingJoinX2 = INVALID_FRACTION;
|
||||
selectedDotInPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int desiredHeight = getDesiredHeight();
|
||||
int height;
|
||||
switch (MeasureSpec.getMode(heightMeasureSpec)) {
|
||||
case MeasureSpec.EXACTLY:
|
||||
height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
break;
|
||||
case MeasureSpec.AT_MOST:
|
||||
height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
|
||||
break;
|
||||
default: // MeasureSpec.UNSPECIFIED
|
||||
height = desiredHeight;
|
||||
break;
|
||||
}
|
||||
int desiredWidth = getDesiredWidth();
|
||||
int width;
|
||||
switch (MeasureSpec.getMode(widthMeasureSpec)) {
|
||||
case MeasureSpec.EXACTLY:
|
||||
width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
break;
|
||||
case MeasureSpec.AT_MOST:
|
||||
width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
|
||||
break;
|
||||
default: // MeasureSpec.UNSPECIFIED
|
||||
width = desiredWidth;
|
||||
break;
|
||||
}
|
||||
setMeasuredDimension(width, height);
|
||||
calculateDotPositions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||
setMeasuredDimension(width, height);
|
||||
calculateDotPositions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAnimation() {
|
||||
super.clearAnimation();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
cancelRunningAnimations();
|
||||
}
|
||||
}
|
||||
|
||||
private int getDesiredHeight() {
|
||||
return getPaddingTop() + dotDiameter + getPaddingBottom();
|
||||
}
|
||||
|
||||
private int getRequiredWidth() {
|
||||
return pageCount * dotDiameter + (pageCount - 1) * gap;
|
||||
}
|
||||
|
||||
private int getDesiredWidth() {
|
||||
return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (viewPager == null || pageCount == 0) {
|
||||
return;
|
||||
}
|
||||
drawUnselected(canvas);
|
||||
drawSelected(canvas);
|
||||
}
|
||||
|
||||
private void drawUnselected(Canvas canvas) {
|
||||
combinedUnselectedPath.rewind();
|
||||
|
||||
// draw any settled, revealing or joining dots
|
||||
for (int page = 0; page < pageCount; page++) {
|
||||
int nextXIndex = page == pageCount - 1 ? page : page + 1;
|
||||
// todo Path.op should be supported in KitKat but causes the app to hang for Nexus 5.
|
||||
// For now disabling for all pre-L devices.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Path unselectedPath = getUnselectedPath(page,
|
||||
dotCenterX[page],
|
||||
dotCenterX[nextXIndex],
|
||||
page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page],
|
||||
dotRevealFractions[page]);
|
||||
combinedUnselectedPath.op(unselectedPath, Path.Op.UNION);
|
||||
} else {
|
||||
canvas.drawCircle(dotCenterX[page], dotCenterY, dotRadius, unselectedPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// draw any retreating joins
|
||||
if (retreatingJoinX1 != INVALID_FRACTION) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
combinedUnselectedPath.op(getRetreatingJoinPath(), Path.Op.UNION);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(combinedUnselectedPath, unselectedPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselected dots can be in 6 states:
|
||||
*
|
||||
* #1 At rest
|
||||
* #2 Joining neighbour, still separate
|
||||
* #3 Joining neighbour, combined curved
|
||||
* #4 Joining neighbour, combined straight
|
||||
* #5 Join retreating
|
||||
* #6 Dot re-showing / revealing
|
||||
*
|
||||
* It can also be in a combination of these states e.g. joining one neighbour while
|
||||
* retreating from another. We therefore create a Path so that we can examine each
|
||||
* dot pair separately and later take the union for these cases.
|
||||
*
|
||||
* This function returns a path for the given dot **and any action to it's right** e.g. joining
|
||||
* or retreating from it's neighbour
|
||||
*
|
||||
* @param page
|
||||
*/
|
||||
private Path getUnselectedPath(int page,
|
||||
float centerX,
|
||||
float nextCenterX,
|
||||
float joiningFraction,
|
||||
float dotRevealFraction) {
|
||||
unselectedDotPath.rewind();
|
||||
|
||||
if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION)
|
||||
&& dotRevealFraction == 0f
|
||||
&& !(page == currentPage && selectedDotInPosition == true)) {
|
||||
// case #1 – At rest
|
||||
unselectedDotPath.addCircle(dotCenterX[page], dotCenterY, dotRadius, Path.Direction.CW);
|
||||
}
|
||||
|
||||
if (joiningFraction > 0f && joiningFraction < 0.5f && retreatingJoinX1 == INVALID_FRACTION) {
|
||||
// case #2 – Joining neighbour, still separate
|
||||
// start with the left dot
|
||||
unselectedDotLeftPath.rewind();
|
||||
|
||||
// start at the bottom center
|
||||
unselectedDotLeftPath.moveTo(centerX, dotBottomY);
|
||||
|
||||
// semi circle to the top center
|
||||
rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
|
||||
unselectedDotLeftPath.arcTo(rectF, 90, 180, true);
|
||||
|
||||
// cubic to the right middle
|
||||
endX1 = centerX + dotRadius + (joiningFraction * gap);
|
||||
endY1 = dotCenterY;
|
||||
controlX1 = centerX + halfDotRadius;
|
||||
controlY1 = dotTopY;
|
||||
controlX2 = endX1;
|
||||
controlY2 = endY1 - halfDotRadius;
|
||||
unselectedDotLeftPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
|
||||
|
||||
// cubic back to the bottom center
|
||||
endX2 = centerX;
|
||||
endY2 = dotBottomY;
|
||||
controlX1 = endX1;
|
||||
controlY1 = endY1 + halfDotRadius;
|
||||
controlX2 = centerX + halfDotRadius;
|
||||
controlY2 = dotBottomY;
|
||||
unselectedDotLeftPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
unselectedDotPath.op(unselectedDotLeftPath, Path.Op.UNION);
|
||||
}
|
||||
|
||||
// now do the next dot to the right
|
||||
unselectedDotRightPath.rewind();
|
||||
|
||||
// start at the bottom center
|
||||
unselectedDotRightPath.moveTo(nextCenterX, dotBottomY);
|
||||
|
||||
// semi circle to the top center
|
||||
rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
|
||||
unselectedDotRightPath.arcTo(rectF, 90, -180, true);
|
||||
|
||||
// cubic to the left middle
|
||||
endX1 = nextCenterX - dotRadius - (joiningFraction * gap);
|
||||
endY1 = dotCenterY;
|
||||
controlX1 = nextCenterX - halfDotRadius;
|
||||
controlY1 = dotTopY;
|
||||
controlX2 = endX1;
|
||||
controlY2 = endY1 - halfDotRadius;
|
||||
unselectedDotRightPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
|
||||
|
||||
// cubic back to the bottom center
|
||||
endX2 = nextCenterX;
|
||||
endY2 = dotBottomY;
|
||||
controlX1 = endX1;
|
||||
controlY1 = endY1 + halfDotRadius;
|
||||
controlX2 = endX2 - halfDotRadius;
|
||||
controlY2 = dotBottomY;
|
||||
unselectedDotRightPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
unselectedDotPath.op(unselectedDotRightPath, Path.Op.UNION);
|
||||
}
|
||||
}
|
||||
|
||||
if (joiningFraction > 0.5f && joiningFraction < 1f && retreatingJoinX1 == INVALID_FRACTION) {
|
||||
// case #3 – Joining neighbour, combined curved
|
||||
// start in the bottom left
|
||||
unselectedDotPath.moveTo(centerX, dotBottomY);
|
||||
|
||||
// semi-circle to the top left
|
||||
rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
|
||||
unselectedDotPath.arcTo(rectF, 90, 180, true);
|
||||
|
||||
// bezier to the middle top of the join
|
||||
endX1 = centerX + dotRadius + (gap / 2);
|
||||
endY1 = dotCenterY - (joiningFraction * dotRadius);
|
||||
controlX1 = endX1 - (joiningFraction * dotRadius);
|
||||
controlY1 = dotTopY;
|
||||
controlX2 = endX1 - ((1 - joiningFraction) * dotRadius);
|
||||
controlY2 = endY1;
|
||||
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
|
||||
|
||||
// bezier to the top right of the join
|
||||
endX2 = nextCenterX;
|
||||
endY2 = dotTopY;
|
||||
controlX1 = endX1 + ((1 - joiningFraction) * dotRadius);
|
||||
controlY1 = endY1;
|
||||
controlX2 = endX1 + (joiningFraction * dotRadius);
|
||||
controlY2 = dotTopY;
|
||||
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
|
||||
|
||||
// semi-circle to the bottom right
|
||||
rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
|
||||
unselectedDotPath.arcTo(rectF, 270, 180, true);
|
||||
|
||||
// bezier to the middle bottom of the join
|
||||
// endX1 stays the same
|
||||
endY1 = dotCenterY + (joiningFraction * dotRadius);
|
||||
controlX1 = endX1 + (joiningFraction * dotRadius);
|
||||
controlY1 = dotBottomY;
|
||||
controlX2 = endX1 + ((1 - joiningFraction) * dotRadius);
|
||||
controlY2 = endY1;
|
||||
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
|
||||
|
||||
// bezier back to the start point in the bottom left
|
||||
endX2 = centerX;
|
||||
endY2 = dotBottomY;
|
||||
controlX1 = endX1 - ((1 - joiningFraction) * dotRadius);
|
||||
controlY1 = endY1;
|
||||
controlX2 = endX1 - (joiningFraction * dotRadius);
|
||||
controlY2 = endY2;
|
||||
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
|
||||
}
|
||||
|
||||
if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) {
|
||||
// case #4 Joining neighbour, combined straight
|
||||
// technically we could use case 3 for this situation as well
|
||||
// but assume that this is an optimization rather than faffing around with beziers
|
||||
// just to draw a rounded rect
|
||||
rectF.set(centerX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
|
||||
unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
|
||||
}
|
||||
|
||||
// case #5 is handled by #getRetreatingJoinPath()
|
||||
// this is done separately so that we can have a single retreating path spanning
|
||||
// multiple dots and therefore animate it's movement smoothly
|
||||
if (dotRevealFraction > MINIMAL_REVEAL) {
|
||||
// case #6 – previously hidden dot revealing
|
||||
unselectedDotPath.addCircle(centerX, dotCenterY, dotRevealFraction * dotRadius,
|
||||
Path.Direction.CW);
|
||||
}
|
||||
|
||||
return unselectedDotPath;
|
||||
}
|
||||
|
||||
private Path getRetreatingJoinPath() {
|
||||
unselectedDotPath.rewind();
|
||||
rectF.set(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY);
|
||||
unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
|
||||
return unselectedDotPath;
|
||||
}
|
||||
|
||||
private void drawSelected(Canvas canvas) {
|
||||
canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint);
|
||||
}
|
||||
|
||||
private void setSelectedPage(int now) {
|
||||
if (now == currentPage || pageCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int was = currentPage;
|
||||
currentPage = now;
|
||||
|
||||
// These animations are not supported in pre-JB versions.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
cancelRunningAnimations();
|
||||
|
||||
// create the anim to move the selected dot – this animator will kick off
|
||||
// retreat animations when it has moved 75% of the way.
|
||||
// The retreat animation in turn will kick of reveal anims when the
|
||||
// retreat has passed any dots to be revealed
|
||||
final int steps = Math.abs(now - was);
|
||||
moveAnimation = createMoveSelectedAnimator(dotCenterX[now], was, now, steps);
|
||||
|
||||
// create animators for joining the dots. This runs independently of the above and relies
|
||||
// on good timing. Like comedy.
|
||||
// if joining multiple dots, each dot after the first is delayed by 1/8 of the duration
|
||||
joiningAnimations = new ValueAnimator[steps];
|
||||
for (int i = 0; i < steps; i++) {
|
||||
joiningAnimations[i] = createJoiningAnimator(now > was ? was + i : was - 1 - i,
|
||||
i * (animDuration / 8L));
|
||||
}
|
||||
moveAnimation.start();
|
||||
startJoiningAnimations();
|
||||
} else {
|
||||
setCurrentPageImmediate();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private ValueAnimator createMoveSelectedAnimator(final float moveTo, int was, int now,
|
||||
int steps) {
|
||||
// create the actual move animator
|
||||
ValueAnimator moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo);
|
||||
|
||||
// also set up a pending retreat anim – this starts when the move is 75% complete
|
||||
retreatAnimation = new PendingRetreatAnimator(was, now, steps,
|
||||
now > was
|
||||
? new RightwardStartPredicate(moveTo - ((moveTo - selectedDotX) * 0.25f))
|
||||
: new LeftwardStartPredicate(moveTo + ((selectedDotX - moveTo) * 0.25f)));
|
||||
|
||||
moveSelected.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
// todo avoid autoboxing
|
||||
selectedDotX = (Float) valueAnimator.getAnimatedValue();
|
||||
retreatAnimation.startIfNecessary(selectedDotX);
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
moveSelected.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
// set a flag so that we continue to draw the unselected dot in the target position
|
||||
// until the selected dot has finished moving into place
|
||||
selectedDotInPosition = false;
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
// set a flag when anim finishes so that we don't draw both selected & unselected
|
||||
// page dots
|
||||
selectedDotInPosition = true;
|
||||
}
|
||||
});
|
||||
|
||||
// slightly delay the start to give the joins a chance to run
|
||||
// unless dot isn't in position yet – then don't delay!
|
||||
moveSelected.setStartDelay(selectedDotInPosition ? animDuration / 4L : 0L);
|
||||
moveSelected.setDuration(animDuration * 3L / 4L);
|
||||
moveSelected.setInterpolator(interpolator);
|
||||
return moveSelected;
|
||||
}
|
||||
|
||||
private ValueAnimator createJoiningAnimator(final int leftJoiningDot, final long startDelay) {
|
||||
// animate the joining fraction for the given dot
|
||||
ValueAnimator joining = ValueAnimator.ofFloat(0f, 1.0f);
|
||||
joining.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
setJoiningFraction(leftJoiningDot, valueAnimator.getAnimatedFraction());
|
||||
}
|
||||
});
|
||||
joining.setDuration(animHalfDuration);
|
||||
joining.setStartDelay(startDelay);
|
||||
joining.setInterpolator(interpolator);
|
||||
return joining;
|
||||
}
|
||||
|
||||
private void setJoiningFraction(int leftDot, float fraction) {
|
||||
joiningFractions[leftDot] = fraction;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
private void clearJoiningFractions() {
|
||||
Arrays.fill(joiningFractions, 0f);
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
private void setDotRevealFraction(int dot, float fraction) {
|
||||
dotRevealFractions[dot] = fraction;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
private void cancelRunningAnimations() {
|
||||
cancelMoveAnimation();
|
||||
cancelJoiningAnimations();
|
||||
cancelRetreatAnimation();
|
||||
cancelRevealAnimations();
|
||||
resetState();
|
||||
}
|
||||
|
||||
private void cancelMoveAnimation() {
|
||||
if (moveAnimation != null && moveAnimation.isRunning()) {
|
||||
moveAnimation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void startJoiningAnimations() {
|
||||
joiningAnimationSet = new AnimatorSet();
|
||||
joiningAnimationSet.playTogether(joiningAnimations);
|
||||
joiningAnimationSet.start();
|
||||
}
|
||||
|
||||
private void cancelJoiningAnimations() {
|
||||
if (joiningAnimationSet != null && joiningAnimationSet.isRunning()) {
|
||||
joiningAnimationSet.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelRetreatAnimation() {
|
||||
if (retreatAnimation != null && retreatAnimation.isRunning()) {
|
||||
retreatAnimation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelRevealAnimations() {
|
||||
if (revealAnimations != null) {
|
||||
for (PendingRevealAnimator reveal : revealAnimations) {
|
||||
reveal.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getUnselectedColour() {
|
||||
return unselectedColour;
|
||||
}
|
||||
|
||||
int getSelectedColour() {
|
||||
return selectedColour;
|
||||
}
|
||||
|
||||
float getDotCenterY() {
|
||||
return dotCenterY;
|
||||
}
|
||||
|
||||
float getDotCenterX(int page) {
|
||||
return dotCenterX[page];
|
||||
}
|
||||
|
||||
float getSelectedDotX() {
|
||||
return selectedDotX;
|
||||
}
|
||||
|
||||
int getCurrentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link android.animation.ValueAnimator} that starts once a given predicate returns true.
|
||||
*/
|
||||
public abstract class PendingStartAnimator extends ValueAnimator {
|
||||
|
||||
protected boolean hasStarted;
|
||||
protected StartPredicate predicate;
|
||||
|
||||
public PendingStartAnimator(StartPredicate predicate) {
|
||||
super();
|
||||
this.predicate = predicate;
|
||||
hasStarted = false;
|
||||
}
|
||||
|
||||
public void startIfNecessary(float currentValue) {
|
||||
if (!hasStarted && predicate.shouldStart(currentValue)) {
|
||||
start();
|
||||
hasStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Animator that shows and then shrinks a retreating join between the previous and newly
|
||||
* selected pages. This also sets up some pending dot reveals – to be started when the retreat
|
||||
* has passed the dot to be revealed.
|
||||
*/
|
||||
public class PendingRetreatAnimator extends PendingStartAnimator {
|
||||
|
||||
public PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) {
|
||||
super(predicate);
|
||||
setDuration(animHalfDuration);
|
||||
setInterpolator(interpolator);
|
||||
|
||||
// work out the start/end values of the retreating join from the direction we're
|
||||
// travelling in. Also look at the current selected dot position, i.e. we're moving on
|
||||
// before a prior anim has finished.
|
||||
final float initialX1 = now > was ? Math.min(dotCenterX[was], selectedDotX) - dotRadius
|
||||
: dotCenterX[now] - dotRadius;
|
||||
final float finalX1 = now > was ? dotCenterX[now] - dotRadius
|
||||
: dotCenterX[now] - dotRadius;
|
||||
final float initialX2 = now > was ? dotCenterX[now] + dotRadius
|
||||
: Math.max(dotCenterX[was], selectedDotX) + dotRadius;
|
||||
final float finalX2 = now > was ? dotCenterX[now] + dotRadius
|
||||
: dotCenterX[now] + dotRadius;
|
||||
revealAnimations = new PendingRevealAnimator[steps];
|
||||
|
||||
// hold on to the indexes of the dots that will be hidden by the retreat so that
|
||||
// we can initialize their revealFraction's i.e. make sure they're hidden while the
|
||||
// reveal animation runs
|
||||
final int[] dotsToHide = new int[steps];
|
||||
if (initialX1 != finalX1) { // rightward retreat
|
||||
setFloatValues(initialX1, finalX1);
|
||||
// create the reveal animations that will run when the retreat passes them
|
||||
for (int i = 0; i < steps; i++) {
|
||||
revealAnimations[i] = new PendingRevealAnimator(was + i,
|
||||
new RightwardStartPredicate(dotCenterX[was + i]));
|
||||
dotsToHide[i] = was + i;
|
||||
}
|
||||
addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
// todo avoid autoboxing
|
||||
retreatingJoinX1 = (Float) valueAnimator.getAnimatedValue();
|
||||
postInvalidateOnAnimation();
|
||||
// start any reveal animations if we've passed them
|
||||
for (PendingRevealAnimator pendingReveal : revealAnimations) {
|
||||
pendingReveal.startIfNecessary(retreatingJoinX1);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else { // (initialX2 != finalX2) leftward retreat
|
||||
setFloatValues(initialX2, finalX2);
|
||||
// create the reveal animations that will run when the retreat passes them
|
||||
for (int i = 0; i < steps; i++) {
|
||||
revealAnimations[i] = new PendingRevealAnimator(was - i,
|
||||
new LeftwardStartPredicate(dotCenterX[was - i]));
|
||||
dotsToHide[i] = was - i;
|
||||
}
|
||||
addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
// todo avoid autoboxing
|
||||
retreatingJoinX2 = (Float) valueAnimator.getAnimatedValue();
|
||||
postInvalidateOnAnimation();
|
||||
// start any reveal animations if we've passed them
|
||||
for (PendingRevealAnimator pendingReveal : revealAnimations) {
|
||||
pendingReveal.startIfNecessary(retreatingJoinX2);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
cancelJoiningAnimations();
|
||||
clearJoiningFractions();
|
||||
// we need to set this so that the dots are hidden until the reveal anim runs
|
||||
for (int dot : dotsToHide) {
|
||||
setDotRevealFraction(dot, MINIMAL_REVEAL);
|
||||
}
|
||||
retreatingJoinX1 = initialX1;
|
||||
retreatingJoinX2 = initialX2;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
retreatingJoinX1 = INVALID_FRACTION;
|
||||
retreatingJoinX2 = INVALID_FRACTION;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Animator that animates a given dot's revealFraction i.e. scales it up
|
||||
*/
|
||||
public class PendingRevealAnimator extends PendingStartAnimator {
|
||||
|
||||
private final int dot;
|
||||
|
||||
public PendingRevealAnimator(int dot, StartPredicate predicate) {
|
||||
super(predicate);
|
||||
this.dot = dot;
|
||||
setFloatValues(MINIMAL_REVEAL, 1f);
|
||||
setDuration(animHalfDuration);
|
||||
setInterpolator(interpolator);
|
||||
|
||||
addUpdateListener(new AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
// todo avoid autoboxing
|
||||
setDotRevealFraction(PendingRevealAnimator.this.dot,
|
||||
(Float) valueAnimator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
|
||||
addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setDotRevealFraction(PendingRevealAnimator.this.dot, 0f);
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate used to start an animation when a test passes
|
||||
*/
|
||||
public abstract class StartPredicate {
|
||||
|
||||
protected float thresholdValue;
|
||||
|
||||
public StartPredicate(float thresholdValue) {
|
||||
this.thresholdValue = thresholdValue;
|
||||
}
|
||||
|
||||
abstract boolean shouldStart(float currentValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate used to start an animation when a given value is greater than a threshold
|
||||
*/
|
||||
public class RightwardStartPredicate extends StartPredicate {
|
||||
|
||||
public RightwardStartPredicate(float thresholdValue) {
|
||||
super(thresholdValue);
|
||||
}
|
||||
|
||||
boolean shouldStart(float currentValue) {
|
||||
return currentValue > thresholdValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate used to start an animation then a given value is less than a threshold
|
||||
*/
|
||||
public class LeftwardStartPredicate extends StartPredicate {
|
||||
|
||||
public LeftwardStartPredicate(float thresholdValue) {
|
||||
super(thresholdValue);
|
||||
}
|
||||
|
||||
boolean shouldStart(float currentValue) {
|
||||
return currentValue < thresholdValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 com.android.settings.widget;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
|
||||
public abstract class EmptyTextSettings extends SettingsPreferenceFragment {
|
||||
|
||||
private TextView mEmpty;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mEmpty = new TextView(getContext());
|
||||
mEmpty.setGravity(Gravity.CENTER);
|
||||
final int textPadding = getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.empty_text_padding);
|
||||
mEmpty.setPadding(textPadding, 0 /* top */, textPadding, 0 /* bottom */);
|
||||
TypedValue value = new TypedValue();
|
||||
getContext().getTheme().resolveAttribute(android.R.attr.textAppearanceMedium, value, true);
|
||||
mEmpty.setTextAppearance(value.resourceId);
|
||||
final int layoutHeight = getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.empty_text_layout_height);
|
||||
((ViewGroup) view.findViewById(android.R.id.list_container)).addView(mEmpty,
|
||||
new LayoutParams(LayoutParams.MATCH_PARENT, layoutHeight));
|
||||
setEmptyView(mEmpty);
|
||||
}
|
||||
|
||||
protected void setEmptyText(int text) {
|
||||
mEmpty.setText(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import static com.android.settings.spa.app.appinfo.AppInfoSettingsProvider.startAppInfoSettings;
|
||||
|
||||
import android.annotation.IdRes;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.Activity;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.UserHandle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.applications.ApplicationsState;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
public class EntityHeaderController {
|
||||
|
||||
@IntDef({ActionType.ACTION_NONE,
|
||||
ActionType.ACTION_NOTIF_PREFERENCE,
|
||||
ActionType.ACTION_EDIT_PREFERENCE,})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface ActionType {
|
||||
int ACTION_NONE = 0;
|
||||
int ACTION_NOTIF_PREFERENCE = 1;
|
||||
int ACTION_EDIT_PREFERENCE = 2;
|
||||
}
|
||||
|
||||
public static final String PREF_KEY_APP_HEADER = "pref_app_header";
|
||||
|
||||
private static final String TAG = "AppDetailFeature";
|
||||
|
||||
private final Context mAppContext;
|
||||
private final Fragment mFragment;
|
||||
private final int mMetricsCategory;
|
||||
private final View mHeader;
|
||||
private Drawable mIcon;
|
||||
private int mPrefOrder = -1000;
|
||||
private String mIconContentDescription;
|
||||
private CharSequence mLabel;
|
||||
private CharSequence mSummary;
|
||||
// Required for hearing aid devices.
|
||||
private CharSequence mSecondSummary;
|
||||
private String mPackageName;
|
||||
private Intent mAppNotifPrefIntent;
|
||||
@UserIdInt
|
||||
private int mUid = UserHandle.USER_NULL;
|
||||
@ActionType
|
||||
private int mAction1;
|
||||
@ActionType
|
||||
private int mAction2;
|
||||
|
||||
private boolean mHasAppInfoLink;
|
||||
|
||||
private boolean mIsInstantApp;
|
||||
|
||||
private View.OnClickListener mEditOnClickListener;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the controller.
|
||||
*
|
||||
* @param fragment The fragment that header will be placed in.
|
||||
* @param header Optional: header view if it's already created.
|
||||
*/
|
||||
public static EntityHeaderController newInstance(Activity activity, Fragment fragment,
|
||||
View header) {
|
||||
return new EntityHeaderController(activity, fragment, header);
|
||||
}
|
||||
|
||||
private EntityHeaderController(Activity activity, Fragment fragment, View header) {
|
||||
mAppContext = activity.getApplicationContext();
|
||||
mFragment = fragment;
|
||||
mMetricsCategory = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
|
||||
.getMetricsCategory(fragment);
|
||||
if (header != null) {
|
||||
mHeader = header;
|
||||
} else {
|
||||
mHeader = LayoutInflater.from(fragment.getContext())
|
||||
.inflate(com.android.settingslib.widget.preference.layout.R.layout.settings_entity_header,
|
||||
null /* root */);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the icon in the header. Callers should also consider calling setIconContentDescription
|
||||
* to provide a description of this icon for accessibility purposes.
|
||||
*/
|
||||
public EntityHeaderController setIcon(Drawable icon) {
|
||||
if (icon != null) {
|
||||
final Drawable.ConstantState state = icon.getConstantState();
|
||||
mIcon = state != null ? state.newDrawable(mAppContext.getResources()) : icon;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to set the header icon from an ApplicationsState.AppEntry. Callers should
|
||||
* also consider calling setIconContentDescription to provide a description of this icon for
|
||||
* accessibility purposes.
|
||||
*/
|
||||
public EntityHeaderController setIcon(ApplicationsState.AppEntry appEntry) {
|
||||
mIcon = Utils.getBadgedIcon(mAppContext, appEntry.info);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setIconContentDescription(String contentDescription) {
|
||||
mIconContentDescription = contentDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setLabel(CharSequence label) {
|
||||
mLabel = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setLabel(ApplicationsState.AppEntry appEntry) {
|
||||
mLabel = appEntry.label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setSummary(CharSequence summary) {
|
||||
mSummary = summary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setSummary(PackageInfo packageInfo) {
|
||||
if (packageInfo != null) {
|
||||
mSummary = packageInfo.versionName;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setSecondSummary(CharSequence summary) {
|
||||
mSecondSummary = summary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setHasAppInfoLink(boolean hasAppInfoLink) {
|
||||
mHasAppInfoLink = hasAppInfoLink;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setButtonActions(@ActionType int action1,
|
||||
@ActionType int action2) {
|
||||
mAction1 = action1;
|
||||
mAction2 = action2;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setPackageName(String packageName) {
|
||||
mPackageName = packageName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setUid(int uid) {
|
||||
mUid = uid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setAppNotifPrefIntent(Intent appNotifPrefIntent) {
|
||||
mAppNotifPrefIntent = appNotifPrefIntent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setIsInstantApp(boolean isInstantApp) {
|
||||
mIsInstantApp = isInstantApp;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntityHeaderController setEditListener(View.OnClickListener listener) {
|
||||
mEditOnClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets this preference order. */
|
||||
public EntityHeaderController setOrder(int order) {
|
||||
mPrefOrder = order;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Done mutating entity header, rebinds everything and return a new {@link LayoutPreference}.
|
||||
*/
|
||||
public LayoutPreference done(Context uiContext) {
|
||||
final LayoutPreference pref = new LayoutPreference(uiContext, done());
|
||||
// Makes sure it's the first preference onscreen.
|
||||
pref.setOrder(mPrefOrder);
|
||||
pref.setSelectable(false);
|
||||
pref.setKey(PREF_KEY_APP_HEADER);
|
||||
pref.setAllowDividerBelow(true);
|
||||
return pref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Done mutating entity header, rebinds everything (optionally skip rebinding buttons).
|
||||
*/
|
||||
public View done(boolean rebindActions) {
|
||||
ImageView iconView = mHeader.findViewById(R.id.entity_header_icon);
|
||||
if (iconView != null) {
|
||||
iconView.setImageDrawable(mIcon);
|
||||
iconView.setContentDescription(mIconContentDescription);
|
||||
}
|
||||
setText(R.id.entity_header_title, mLabel);
|
||||
setText(R.id.entity_header_summary, mSummary);
|
||||
setText(com.android.settingslib.widget.preference.layout.R.id.entity_header_second_summary, mSecondSummary);
|
||||
if (mIsInstantApp) {
|
||||
setText(com.android.settingslib.widget.preference.layout.R.id.install_type,
|
||||
mHeader.getResources().getString(R.string.install_type_instant));
|
||||
}
|
||||
|
||||
if (rebindActions) {
|
||||
bindHeaderButtons();
|
||||
}
|
||||
|
||||
return mHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only binds entity header with button actions.
|
||||
*/
|
||||
public EntityHeaderController bindHeaderButtons() {
|
||||
final View entityHeaderContent = mHeader.findViewById(
|
||||
com.android.settingslib.widget.preference.layout.R.id.entity_header_content);
|
||||
final ImageButton button1 = mHeader.findViewById(android.R.id.button1);
|
||||
final ImageButton button2 = mHeader.findViewById(android.R.id.button2);
|
||||
bindAppInfoLink(entityHeaderContent);
|
||||
bindButton(button1, mAction1);
|
||||
bindButton(button2, mAction2);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void bindAppInfoLink(View entityHeaderContent) {
|
||||
if (!mHasAppInfoLink) {
|
||||
// Caller didn't ask for app link, skip.
|
||||
return;
|
||||
}
|
||||
if (entityHeaderContent == null
|
||||
|| mPackageName == null
|
||||
|| mPackageName.equals(Utils.OS_PKG)
|
||||
|| mUid == UserHandle.USER_NULL) {
|
||||
Log.w(TAG, "Missing ingredients to build app info link, skip");
|
||||
return;
|
||||
}
|
||||
entityHeaderContent.setOnClickListener(v -> startAppInfoSettings(
|
||||
mPackageName, mUid, mFragment, 0 /* request */,
|
||||
mMetricsCategory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Done mutating entity header, rebinds everything.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
View done() {
|
||||
return done(true /* rebindActions */);
|
||||
}
|
||||
|
||||
private void bindButton(ImageButton button, @ActionType int action) {
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case ActionType.ACTION_EDIT_PREFERENCE: {
|
||||
if (mEditOnClickListener == null) {
|
||||
button.setVisibility(View.GONE);
|
||||
} else {
|
||||
button.setImageResource(com.android.internal.R.drawable.ic_mode_edit);
|
||||
button.setVisibility(View.VISIBLE);
|
||||
button.setOnClickListener(mEditOnClickListener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case ActionType.ACTION_NOTIF_PREFERENCE: {
|
||||
if (mAppNotifPrefIntent == null) {
|
||||
button.setVisibility(View.GONE);
|
||||
} else {
|
||||
button.setOnClickListener(v -> {
|
||||
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
|
||||
.action(SettingsEnums.PAGE_UNKNOWN,
|
||||
SettingsEnums.ACTION_OPEN_APP_NOTIFICATION_SETTING,
|
||||
mMetricsCategory,
|
||||
null, 0);
|
||||
mFragment.startActivity(mAppNotifPrefIntent);
|
||||
});
|
||||
button.setVisibility(View.VISIBLE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case ActionType.ACTION_NONE: {
|
||||
button.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setText(@IdRes int id, CharSequence text) {
|
||||
TextView textView = mHeader.findViewById(id);
|
||||
if (textView != null) {
|
||||
textView.setText(text);
|
||||
textView.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settingslib.RestrictedSwitchPreference;
|
||||
|
||||
/**
|
||||
* This widget with enabled filterTouchesWhenObscured attribute use to replace
|
||||
* the {@link RestrictedSwitchPreference} in the Special access app pages for
|
||||
* security.
|
||||
*/
|
||||
public class FilterTouchesRestrictedSwitchPreference extends RestrictedSwitchPreference {
|
||||
public FilterTouchesRestrictedSwitchPreference(Context context, AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public FilterTouchesRestrictedSwitchPreference(Context context, AttributeSet attrs,
|
||||
int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public FilterTouchesRestrictedSwitchPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public FilterTouchesRestrictedSwitchPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
|
||||
if (switchView != null) {
|
||||
final View rootView = switchView.getRootView();
|
||||
rootView.setFilterTouchesWhenObscured(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
/**
|
||||
* This widget with enabled filterTouchesWhenObscured attribute use to replace
|
||||
* the {@link SwitchPreferenceCompat} in the Special access app pages for security.
|
||||
*/
|
||||
public class FilterTouchesSwitchPreference extends SwitchPreferenceCompat {
|
||||
|
||||
public FilterTouchesSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public FilterTouchesSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public FilterTouchesSwitchPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public FilterTouchesSwitchPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
|
||||
if (switchView != null) {
|
||||
final View rootView = switchView.getRootView();
|
||||
rootView.setFilterTouchesWhenObscured(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* A preference whose summary text will only span one single line.
|
||||
*/
|
||||
public class FixedLineSummaryPreference extends Preference {
|
||||
|
||||
private int mSummaryLineCount;
|
||||
|
||||
public FixedLineSummaryPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedLineSummaryPreference,
|
||||
0, 0);
|
||||
if (a.hasValue(R.styleable.FixedLineSummaryPreference_summaryLineCount)) {
|
||||
mSummaryLineCount = a.getInteger(
|
||||
R.styleable.FixedLineSummaryPreference_summaryLineCount, 1);
|
||||
} else {
|
||||
mSummaryLineCount = 1;
|
||||
}
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
public void setSummaryLineCount(int count) {
|
||||
mSummaryLineCount = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
TextView summary = (TextView) holder.findViewById(android.R.id.summary);
|
||||
if (summary != null) {
|
||||
summary.setMinLines(mSummaryLineCount);
|
||||
summary.setMaxLines(mSummaryLineCount);
|
||||
summary.setEllipsize(TruncateAt.END);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
/**
|
||||
* This scrolling view behavior will set the background of the {@link AppBarLayout} as
|
||||
* transparent and without the elevation. Also make header overlapped the scrolling child view.
|
||||
*/
|
||||
public class FloatingAppBarScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {
|
||||
private boolean initialized;
|
||||
|
||||
public FloatingAppBarScrollingViewBehavior(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
|
||||
boolean changed = super.onDependentViewChanged(parent, child, dependency);
|
||||
if (!initialized && dependency instanceof AppBarLayout) {
|
||||
initialized = true;
|
||||
AppBarLayout appBarLayout = (AppBarLayout) dependency;
|
||||
setAppBarLayoutTransparent(appBarLayout);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setAppBarLayoutTransparent(AppBarLayout appBarLayout) {
|
||||
appBarLayout.setBackgroundColor(Color.TRANSPARENT);
|
||||
appBarLayout.setTargetElevation(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldHeaderOverlapScrollingChild() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
97
Settings/src/com/android/settings/widget/GearPreference.java
Normal file
97
Settings/src/com/android/settings/widget/GearPreference.java
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.RestrictedPreference;
|
||||
|
||||
/**
|
||||
* A preference with a Gear on the side
|
||||
*/
|
||||
public class GearPreference extends RestrictedPreference implements View.OnClickListener {
|
||||
// Default true for gear available even if the preference itself is disabled.
|
||||
protected boolean mGearState = true;
|
||||
private OnGearClickListener mOnGearClickListener;
|
||||
|
||||
public GearPreference(Context context, AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public GearPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setOnGearClickListener(OnGearClickListener l) {
|
||||
mOnGearClickListener = l;
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
/** Sets state of gear button. */
|
||||
public void setGearEnabled(boolean enabled) {
|
||||
mGearState = enabled;
|
||||
}
|
||||
|
||||
/** Gets state of gear button. */
|
||||
public boolean isGearEnabled() {
|
||||
return mGearState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSecondTargetResId() {
|
||||
return R.layout.preference_widget_gear;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldHideSecondTarget() {
|
||||
return mOnGearClickListener == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final View gear = holder.findViewById(R.id.settings_button);
|
||||
if (mOnGearClickListener != null) {
|
||||
gear.setVisibility(View.VISIBLE);
|
||||
gear.setOnClickListener(this);
|
||||
} else {
|
||||
gear.setVisibility(View.GONE);
|
||||
gear.setOnClickListener(null);
|
||||
}
|
||||
// Make gear available even if the preference itself is disabled.
|
||||
gear.setEnabled(mGearState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.settings_button) {
|
||||
if (mOnGearClickListener != null) {
|
||||
mOnGearClickListener.onGearClick(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnGearClickListener {
|
||||
void onGearClick(GearPreference p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.settings.widget;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.PrimarySwitchPreference;
|
||||
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
import com.android.settingslib.RestrictedSwitchPreference;
|
||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||
|
||||
/**
|
||||
* The switch controller that is used to update the switch widget in the PrimarySwitchPreference
|
||||
* and RestrictedSwitchPreference layouts.
|
||||
*/
|
||||
public class GenericSwitchController extends SwitchWidgetController implements
|
||||
Preference.OnPreferenceChangeListener {
|
||||
|
||||
private Preference mPreference;
|
||||
private MetricsFeatureProvider mMetricsFeatureProvider;
|
||||
|
||||
public GenericSwitchController(PrimarySwitchPreference preference) {
|
||||
setPreference(preference);
|
||||
}
|
||||
|
||||
public GenericSwitchController(RestrictedSwitchPreference preference) {
|
||||
setPreference(preference);
|
||||
}
|
||||
|
||||
private void setPreference(Preference preference) {
|
||||
mPreference = preference;
|
||||
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startListening() {
|
||||
mPreference.setOnPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopListening() {
|
||||
mPreference.setOnPreferenceChangeListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
if (mPreference instanceof PrimarySwitchPreference) {
|
||||
((PrimarySwitchPreference) mPreference).setChecked(checked);
|
||||
} else if (mPreference instanceof RestrictedSwitchPreference) {
|
||||
((RestrictedSwitchPreference) mPreference).setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
if (mPreference instanceof PrimarySwitchPreference) {
|
||||
return ((PrimarySwitchPreference) mPreference).isChecked();
|
||||
} else if (mPreference instanceof RestrictedSwitchPreference) {
|
||||
return ((RestrictedSwitchPreference) mPreference).isChecked();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
if (mPreference instanceof PrimarySwitchPreference) {
|
||||
((PrimarySwitchPreference) mPreference).setSwitchEnabled(enabled);
|
||||
} else if (mPreference instanceof RestrictedSwitchPreference) {
|
||||
((RestrictedSwitchPreference) mPreference).setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (mListener != null) {
|
||||
final boolean result = mListener.onSwitchToggled((Boolean) newValue);
|
||||
if (result) {
|
||||
mMetricsFeatureProvider.logClickedPreference(preference,
|
||||
preference.getExtras().getInt(DashboardFragment.CATEGORY));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisabledByAdmin(EnforcedAdmin admin) {
|
||||
if (mPreference instanceof PrimarySwitchPreference) {
|
||||
((PrimarySwitchPreference) mPreference).setDisabledByAdmin(admin);
|
||||
} else if (mPreference instanceof RestrictedSwitchPreference) {
|
||||
((RestrictedSwitchPreference) mPreference).setDisabledByAdmin(admin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceGroup;
|
||||
import androidx.preference.PreferenceGroupAdapter;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
|
||||
|
||||
private static final String TAG = "HighlightableAdapter";
|
||||
@VisibleForTesting
|
||||
static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L;
|
||||
@VisibleForTesting
|
||||
static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
|
||||
private static final long HIGHLIGHT_DURATION = 15000L;
|
||||
private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
|
||||
private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
|
||||
|
||||
@VisibleForTesting
|
||||
final int mHighlightColor;
|
||||
@VisibleForTesting
|
||||
boolean mFadeInAnimated;
|
||||
|
||||
private final int mNormalBackgroundRes;
|
||||
private final String mHighlightKey;
|
||||
private boolean mHighlightRequested;
|
||||
private int mHighlightPosition = RecyclerView.NO_POSITION;
|
||||
|
||||
|
||||
/**
|
||||
* Tries to override initial expanded child count.
|
||||
* <p/>
|
||||
* Initial expanded child count will be ignored if:
|
||||
* 1. fragment contains request to highlight a particular row.
|
||||
* 2. count value is invalid.
|
||||
*/
|
||||
public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) {
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
final PreferenceScreen screen = host.getPreferenceScreen();
|
||||
if (screen == null) {
|
||||
return;
|
||||
}
|
||||
final Bundle arguments = host.getArguments();
|
||||
if (arguments != null) {
|
||||
final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY);
|
||||
if (!TextUtils.isEmpty(highlightKey)) {
|
||||
// Has highlight row - expand everything
|
||||
screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final int initialCount = host.getInitialExpandedChildCount();
|
||||
if (initialCount <= 0) {
|
||||
return;
|
||||
}
|
||||
screen.setInitialExpandedChildrenCount(initialCount);
|
||||
}
|
||||
|
||||
public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
|
||||
boolean highlightRequested) {
|
||||
super(preferenceGroup);
|
||||
mHighlightKey = key;
|
||||
mHighlightRequested = highlightRequested;
|
||||
final Context context = preferenceGroup.getContext();
|
||||
final TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
|
||||
outValue, true /* resolveRefs */);
|
||||
mNormalBackgroundRes = outValue.resourceId;
|
||||
mHighlightColor = context.getColor(R.color.preference_highlight_color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
updateBackground(holder, position);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateBackground(PreferenceViewHolder holder, int position) {
|
||||
View v = holder.itemView;
|
||||
if (position == mHighlightPosition
|
||||
&& (mHighlightKey != null
|
||||
&& TextUtils.equals(mHighlightKey, getItem(position).getKey()))) {
|
||||
// This position should be highlighted. If it's highlighted before - skip animation.
|
||||
addHighlightBackground(holder, !mFadeInAnimated);
|
||||
if (v != null) {
|
||||
v.requestAccessibilityFocus();
|
||||
}
|
||||
} else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
|
||||
// View with highlight is reused for a view that should not have highlight
|
||||
removeHighlightBackground(holder, false /* animate */);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function can highlight a specific setting in recycler view.
|
||||
* note: Before highlighting a setting, screen collapses tool bar with an animation.
|
||||
*/
|
||||
public void requestHighlight(View root, RecyclerView recyclerView, AppBarLayout appBarLayout) {
|
||||
if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) {
|
||||
return;
|
||||
}
|
||||
final int position = getPreferenceAdapterPosition(mHighlightKey);
|
||||
if (position < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight request accepted
|
||||
mHighlightRequested = true;
|
||||
// Collapse app bar after 300 milliseconds.
|
||||
if (appBarLayout != null) {
|
||||
root.postDelayed(() -> {
|
||||
appBarLayout.setExpanded(false, true);
|
||||
}, DELAY_COLLAPSE_DURATION_MILLIS);
|
||||
}
|
||||
|
||||
// Remove the animator as early as possible to avoid a RecyclerView crash.
|
||||
recyclerView.setItemAnimator(null);
|
||||
// Scroll to correct position after 600 milliseconds.
|
||||
root.postDelayed(() -> {
|
||||
if (ensureHighlightPosition()) {
|
||||
recyclerView.smoothScrollToPosition(mHighlightPosition);
|
||||
highlightAndFocusTargetItem(recyclerView, mHighlightPosition);
|
||||
}
|
||||
}, DELAY_HIGHLIGHT_DURATION_MILLIS);
|
||||
}
|
||||
|
||||
private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) {
|
||||
ViewHolder target = recyclerView.findViewHolderForAdapterPosition(highlightPosition);
|
||||
if (target != null) { // view already visible
|
||||
notifyItemChanged(mHighlightPosition);
|
||||
target.itemView.requestFocus();
|
||||
} else { // otherwise we're about to scroll to that view (but we might not be scrolling yet)
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
notifyItemChanged(mHighlightPosition);
|
||||
ViewHolder target = recyclerView
|
||||
.findViewHolderForAdapterPosition(highlightPosition);
|
||||
if (target != null) {
|
||||
target.itemView.requestFocus();
|
||||
}
|
||||
recyclerView.removeOnScrollListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure we highlight the real-wanted position in case of preference position already
|
||||
* changed when the delay time comes.
|
||||
*/
|
||||
private boolean ensureHighlightPosition() {
|
||||
if (TextUtils.isEmpty(mHighlightKey)) {
|
||||
return false;
|
||||
}
|
||||
final int position = getPreferenceAdapterPosition(mHighlightKey);
|
||||
final boolean allowHighlight = position >= 0;
|
||||
if (allowHighlight && mHighlightPosition != position) {
|
||||
Log.w(TAG, "EnsureHighlight: position has changed since last highlight request");
|
||||
// Make sure RecyclerView always uses latest correct position to avoid exceptions.
|
||||
mHighlightPosition = position;
|
||||
}
|
||||
return allowHighlight;
|
||||
}
|
||||
|
||||
public boolean isHighlightRequested() {
|
||||
return mHighlightRequested;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void requestRemoveHighlightDelayed(PreferenceViewHolder holder) {
|
||||
final View v = holder.itemView;
|
||||
v.postDelayed(() -> {
|
||||
mHighlightPosition = RecyclerView.NO_POSITION;
|
||||
removeHighlightBackground(holder, true /* animate */);
|
||||
}, HIGHLIGHT_DURATION);
|
||||
}
|
||||
|
||||
private void addHighlightBackground(PreferenceViewHolder holder, boolean animate) {
|
||||
final View v = holder.itemView;
|
||||
v.setTag(R.id.preference_highlighted, true);
|
||||
if (!animate) {
|
||||
v.setBackgroundColor(mHighlightColor);
|
||||
Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
|
||||
requestRemoveHighlightDelayed(holder);
|
||||
return;
|
||||
}
|
||||
mFadeInAnimated = true;
|
||||
final int colorFrom = mNormalBackgroundRes;
|
||||
final int colorTo = mHighlightColor;
|
||||
final ValueAnimator fadeInLoop = ValueAnimator.ofObject(
|
||||
new ArgbEvaluator(), colorFrom, colorTo);
|
||||
fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION);
|
||||
fadeInLoop.addUpdateListener(
|
||||
animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
|
||||
fadeInLoop.setRepeatMode(ValueAnimator.REVERSE);
|
||||
fadeInLoop.setRepeatCount(4);
|
||||
fadeInLoop.start();
|
||||
Log.d(TAG, "AddHighlight: starting fade in animation");
|
||||
holder.setIsRecyclable(false);
|
||||
requestRemoveHighlightDelayed(holder);
|
||||
}
|
||||
|
||||
private void removeHighlightBackground(PreferenceViewHolder holder, boolean animate) {
|
||||
final View v = holder.itemView;
|
||||
if (!animate) {
|
||||
v.setTag(R.id.preference_highlighted, false);
|
||||
v.setBackgroundResource(mNormalBackgroundRes);
|
||||
Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
|
||||
// Not highlighted, no-op
|
||||
Log.d(TAG, "RemoveHighlight: Not highlighted - skipping");
|
||||
return;
|
||||
}
|
||||
int colorFrom = mHighlightColor;
|
||||
int colorTo = mNormalBackgroundRes;
|
||||
|
||||
v.setTag(R.id.preference_highlighted, false);
|
||||
final ValueAnimator colorAnimation = ValueAnimator.ofObject(
|
||||
new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION);
|
||||
colorAnimation.addUpdateListener(
|
||||
animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
|
||||
colorAnimation.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
// Animation complete - the background is now white. Change to mNormalBackgroundRes
|
||||
// so it is white and has ripple on touch.
|
||||
v.setBackgroundResource(mNormalBackgroundRes);
|
||||
holder.setIsRecyclable(true);
|
||||
}
|
||||
});
|
||||
colorAnimation.start();
|
||||
Log.d(TAG, "Starting fade out animation");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceGroup;
|
||||
import androidx.preference.PreferenceGroupAdapter;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.window.embedding.ActivityEmbeddingController;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.homepage.SettingsHomepageActivity;
|
||||
|
||||
/**
|
||||
* Adapter for highlighting top level preferences
|
||||
*/
|
||||
public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapter implements
|
||||
SettingsHomepageActivity.HomepageLoadedListener {
|
||||
|
||||
private static final String TAG = "HighlightableTopLevelAdapter";
|
||||
|
||||
static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 100L;
|
||||
private static final int RES_NORMAL_BACKGROUND =
|
||||
R.drawable.homepage_selectable_item_background;
|
||||
private static final int RES_HIGHLIGHTED_BACKGROUND =
|
||||
R.drawable.homepage_highlighted_item_background;
|
||||
|
||||
private final int mTitleColorNormal;
|
||||
private final int mTitleColorHighlight;
|
||||
private final int mSummaryColorNormal;
|
||||
private final int mSummaryColorHighlight;
|
||||
private final int mIconColorNormal;
|
||||
private final int mIconColorHighlight;
|
||||
|
||||
private final SettingsHomepageActivity mHomepageActivity;
|
||||
private final RecyclerView mRecyclerView;
|
||||
private String mHighlightKey;
|
||||
private int mHighlightPosition = RecyclerView.NO_POSITION;
|
||||
private int mScrollPosition = RecyclerView.NO_POSITION;
|
||||
private boolean mHighlightNeeded;
|
||||
private boolean mScrolled;
|
||||
private SparseArray<PreferenceViewHolder> mViewHolders;
|
||||
|
||||
public HighlightableTopLevelPreferenceAdapter(SettingsHomepageActivity homepageActivity,
|
||||
PreferenceGroup preferenceGroup, RecyclerView recyclerView, String key,
|
||||
boolean scrollNeeded) {
|
||||
super(preferenceGroup);
|
||||
mRecyclerView = recyclerView;
|
||||
mHighlightKey = key;
|
||||
mScrolled = !scrollNeeded;
|
||||
mViewHolders = new SparseArray<>();
|
||||
mHomepageActivity = homepageActivity;
|
||||
Context context = preferenceGroup.getContext();
|
||||
mTitleColorNormal = Utils.getColorAttrDefaultColor(context,
|
||||
android.R.attr.textColorPrimary);
|
||||
mTitleColorHighlight = context.getColor(R.color.accent_select_primary_text);
|
||||
mSummaryColorNormal = Utils.getColorAttrDefaultColor(context,
|
||||
android.R.attr.textColorSecondary);
|
||||
mSummaryColorHighlight = context.getColor(R.color.accent_select_secondary_text);
|
||||
mIconColorNormal = Utils.getHomepageIconColor(context);
|
||||
mIconColorHighlight = Utils.getHomepageIconColorHighlight(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
mViewHolders.put(position, holder);
|
||||
updateBackground(holder, position);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateBackground(PreferenceViewHolder holder, int position) {
|
||||
if (!isHighlightNeeded()) {
|
||||
removeHighlightBackground(holder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (position == mHighlightPosition
|
||||
&& mHighlightKey != null
|
||||
&& TextUtils.equals(mHighlightKey, getItem(position).getKey())) {
|
||||
// This position should be highlighted.
|
||||
addHighlightBackground(holder);
|
||||
} else {
|
||||
removeHighlightBackground(holder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function can highlight a specific setting in recycler view.
|
||||
*/
|
||||
public void requestHighlight() {
|
||||
if (mRecyclerView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int previousPosition = mHighlightPosition;
|
||||
if (TextUtils.isEmpty(mHighlightKey)) {
|
||||
// De-highlight previous preference.
|
||||
mHighlightPosition = RecyclerView.NO_POSITION;
|
||||
mScrolled = true;
|
||||
if (previousPosition >= 0) {
|
||||
notifyItemChanged(previousPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final int position = getPreferenceAdapterPosition(mHighlightKey);
|
||||
if (position < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll before highlight if needed.
|
||||
final boolean highlightNeeded = isHighlightNeeded();
|
||||
if (highlightNeeded) {
|
||||
mScrollPosition = position;
|
||||
scroll();
|
||||
}
|
||||
|
||||
// Turn on/off highlight when screen split mode is changed.
|
||||
if (highlightNeeded != mHighlightNeeded) {
|
||||
Log.d(TAG, "Highlight needed change: " + highlightNeeded);
|
||||
mHighlightNeeded = highlightNeeded;
|
||||
mHighlightPosition = position;
|
||||
notifyItemChanged(position);
|
||||
if (!highlightNeeded) {
|
||||
// De-highlight to prevent a flicker
|
||||
removeHighlightAt(previousPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (position == mHighlightPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
mHighlightPosition = position;
|
||||
Log.d(TAG, "Request highlight position " + position);
|
||||
Log.d(TAG, "Is highlight needed: " + highlightNeeded);
|
||||
if (!highlightNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight preference.
|
||||
notifyItemChanged(position);
|
||||
|
||||
// De-highlight previous preference.
|
||||
if (previousPosition >= 0) {
|
||||
notifyItemChanged(previousPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that highlights a setting by specifying a preference key. Usually used whenever a
|
||||
* preference is clicked.
|
||||
*/
|
||||
public void highlightPreference(String key, boolean scrollNeeded) {
|
||||
mHighlightKey = key;
|
||||
mScrolled = !scrollNeeded;
|
||||
requestHighlight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHomepageLoaded() {
|
||||
scroll();
|
||||
}
|
||||
|
||||
private void scroll() {
|
||||
if (mScrolled || mScrollPosition < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mHomepageActivity.addHomepageLoadedListener(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only when the recyclerView is loaded, it can be scrolled
|
||||
final View view = mRecyclerView.getChildAt(mScrollPosition);
|
||||
if (view == null) {
|
||||
mRecyclerView.postDelayed(() -> scroll(), DELAY_HIGHLIGHT_DURATION_MILLIS);
|
||||
return;
|
||||
}
|
||||
|
||||
mScrolled = true;
|
||||
Log.d(TAG, "Scroll to position " + mScrollPosition);
|
||||
// Scroll to the top to reset the position.
|
||||
mRecyclerView.nestedScrollBy(0, -mRecyclerView.getHeight());
|
||||
|
||||
final int scrollY = view.getTop();
|
||||
if (scrollY > 0) {
|
||||
mRecyclerView.nestedScrollBy(0, scrollY);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeHighlightAt(int position) {
|
||||
if (position >= 0) {
|
||||
// De-highlight the existing preference view holder at an early stage
|
||||
final PreferenceViewHolder holder = mViewHolders.get(position);
|
||||
if (holder != null) {
|
||||
removeHighlightBackground(holder);
|
||||
}
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void addHighlightBackground(PreferenceViewHolder holder) {
|
||||
final View v = holder.itemView;
|
||||
v.setBackgroundResource(RES_HIGHLIGHTED_BACKGROUND);
|
||||
((TextView) v.findViewById(android.R.id.title)).setTextColor(mTitleColorHighlight);
|
||||
((TextView) v.findViewById(android.R.id.summary)).setTextColor(mSummaryColorHighlight);
|
||||
final Drawable drawable = ((ImageView) v.findViewById(android.R.id.icon)).getDrawable();
|
||||
if (drawable != null) {
|
||||
drawable.setTint(mIconColorHighlight);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeHighlightBackground(PreferenceViewHolder holder) {
|
||||
final View v = holder.itemView;
|
||||
v.setBackgroundResource(RES_NORMAL_BACKGROUND);
|
||||
((TextView) v.findViewById(android.R.id.title)).setTextColor(mTitleColorNormal);
|
||||
((TextView) v.findViewById(android.R.id.summary)).setTextColor(mSummaryColorNormal);
|
||||
final Drawable drawable = ((ImageView) v.findViewById(android.R.id.icon)).getDrawable();
|
||||
if (drawable != null) {
|
||||
drawable.setTint(mIconColorNormal);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHighlightNeeded() {
|
||||
return ActivityEmbeddingController.getInstance(mHomepageActivity)
|
||||
.isActivityEmbedded(mHomepageActivity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
/**
|
||||
* This scrolling view behavior will set the background of the {@link AppBarLayout} as
|
||||
* transparent and without the elevation.
|
||||
*/
|
||||
public class HomepageAppBarScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {
|
||||
private boolean mInitialized;
|
||||
|
||||
public HomepageAppBarScrollingViewBehavior(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
|
||||
boolean changed = super.onDependentViewChanged(parent, child, dependency);
|
||||
if (!mInitialized && dependency instanceof AppBarLayout) {
|
||||
mInitialized = true;
|
||||
AppBarLayout appBarLayout = (AppBarLayout) dependency;
|
||||
setAppBarLayoutTransparent(appBarLayout);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setAppBarLayoutTransparent(AppBarLayout appBarLayout) {
|
||||
appBarLayout.setBackgroundColor(Color.TRANSPARENT);
|
||||
appBarLayout.setTargetElevation(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
/** A customized layout for homepage preference. */
|
||||
public class HomepagePreference extends Preference implements
|
||||
HomepagePreferenceLayoutHelper.HomepagePreferenceLayout {
|
||||
|
||||
private final HomepagePreferenceLayoutHelper mHelper;
|
||||
|
||||
public HomepagePreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
public HomepagePreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
public HomepagePreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
public HomepagePreference(Context context) {
|
||||
super(context);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mHelper.onBindViewHolder(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomepagePreferenceLayoutHelper getHelper() {
|
||||
return mHelper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.settings.widget;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/** Helper for homepage preference to manage layout. */
|
||||
public class HomepagePreferenceLayoutHelper {
|
||||
|
||||
private View mIcon;
|
||||
private View mText;
|
||||
private boolean mIconVisible = true;
|
||||
private int mIconPaddingStart = -1;
|
||||
private int mTextPaddingStart = -1;
|
||||
|
||||
/** The interface for managing preference layouts on homepage */
|
||||
public interface HomepagePreferenceLayout {
|
||||
/** Returns a {@link HomepagePreferenceLayoutHelper} */
|
||||
HomepagePreferenceLayoutHelper getHelper();
|
||||
}
|
||||
|
||||
public HomepagePreferenceLayoutHelper(Preference preference) {
|
||||
preference.setLayoutResource(R.layout.homepage_preference);
|
||||
}
|
||||
|
||||
/** Sets whether the icon should be visible */
|
||||
public void setIconVisible(boolean visible) {
|
||||
mIconVisible = visible;
|
||||
if (mIcon != null) {
|
||||
mIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the icon padding start */
|
||||
public void setIconPaddingStart(int paddingStart) {
|
||||
mIconPaddingStart = paddingStart;
|
||||
if (mIcon != null && paddingStart >= 0) {
|
||||
mIcon.setPaddingRelative(paddingStart, mIcon.getPaddingTop(), mIcon.getPaddingEnd(),
|
||||
mIcon.getPaddingBottom());
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the text padding start */
|
||||
public void setTextPaddingStart(int paddingStart) {
|
||||
mTextPaddingStart = paddingStart;
|
||||
if (mText != null && paddingStart >= 0) {
|
||||
mText.setPaddingRelative(paddingStart, mText.getPaddingTop(), mText.getPaddingEnd(),
|
||||
mText.getPaddingBottom());
|
||||
}
|
||||
}
|
||||
|
||||
void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
mIcon = holder.findViewById(R.id.icon_frame);
|
||||
mText = holder.findViewById(R.id.text_frame);
|
||||
setIconVisible(mIconVisible);
|
||||
setIconPaddingStart(mIconPaddingStart);
|
||||
setTextPaddingStart(mTextPaddingStart);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 com.android.settings.widget;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
/**
|
||||
* Utility to invert another {@link ChartAxis}.
|
||||
*/
|
||||
public class InvertedChartAxis implements ChartAxis {
|
||||
private final ChartAxis mWrapped;
|
||||
private float mSize;
|
||||
|
||||
public InvertedChartAxis(ChartAxis wrapped) {
|
||||
mWrapped = wrapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setBounds(long min, long max) {
|
||||
return mWrapped.setBounds(min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setSize(float size) {
|
||||
mSize = size;
|
||||
return mWrapped.setSize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float convertToPoint(long value) {
|
||||
return mSize - mWrapped.convertToPoint(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long convertToValue(float point) {
|
||||
return mWrapped.convertToValue(mSize - point);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long buildLabel(Resources res, SpannableStringBuilder builder, long value) {
|
||||
return mWrapped.buildLabel(res, builder, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getTickPoints() {
|
||||
final float[] points = mWrapped.getTickPoints();
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
points[i] = mSize - points[i];
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int shouldAdjustAxis(long value) {
|
||||
return mWrapped.shouldAdjustAxis(value);
|
||||
}
|
||||
}
|
||||
234
Settings/src/com/android/settings/widget/LabeledSeekBar.java
Normal file
234
Settings/src/com/android/settings/widget/LabeledSeekBar.java
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.customview.widget.ExploreByTouchHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* LabeledSeekBar represent a seek bar assigned with labeled, discrete values.
|
||||
* It pretends to be a group of radio button for AccessibilityServices, in order to adjust the
|
||||
* behavior of these services to keep the mental model of the visual discrete SeekBar.
|
||||
*/
|
||||
public class LabeledSeekBar extends SeekBar {
|
||||
|
||||
private final ExploreByTouchHelper mAccessHelper;
|
||||
|
||||
/** Seek bar change listener set via public method. */
|
||||
private OnSeekBarChangeListener mOnSeekBarChangeListener;
|
||||
|
||||
/** Labels for discrete progress values. */
|
||||
private String[] mLabels;
|
||||
|
||||
private int mLastProgress = -1;
|
||||
|
||||
public LabeledSeekBar(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, com.android.internal.R.attr.seekBarStyle);
|
||||
}
|
||||
|
||||
public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this);
|
||||
ViewCompat.setAccessibilityDelegate(this, mAccessHelper);
|
||||
|
||||
super.setOnSeekBarChangeListener(mProxySeekBarListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setProgress(int progress) {
|
||||
// This method gets called from the constructor, so mAccessHelper may
|
||||
// not have been assigned yet.
|
||||
if (mAccessHelper != null) {
|
||||
mAccessHelper.invalidateRoot();
|
||||
}
|
||||
|
||||
super.setProgress(progress);
|
||||
}
|
||||
|
||||
public void setLabels(String[] labels) {
|
||||
mLabels = labels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
|
||||
// The callback set in the constructor will proxy calls to this
|
||||
// listener.
|
||||
mOnSeekBarChangeListener = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean dispatchHoverEvent(MotionEvent event) {
|
||||
return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
|
||||
}
|
||||
|
||||
private void sendClickEventForAccessibility(int progress) {
|
||||
mAccessHelper.invalidateRoot();
|
||||
mAccessHelper.sendEventForVirtualView(progress, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
}
|
||||
|
||||
private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (mOnSeekBarChangeListener != null) {
|
||||
mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (mOnSeekBarChangeListener != null) {
|
||||
mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (mOnSeekBarChangeListener != null) {
|
||||
mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
|
||||
sendClickEventForAccessibility(progress);
|
||||
}
|
||||
if (progress != mLastProgress) {
|
||||
seekBar.performHapticFeedback(CLOCK_TICK);
|
||||
mLastProgress = progress;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper {
|
||||
|
||||
private boolean mIsLayoutRtl;
|
||||
|
||||
public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) {
|
||||
super(forView);
|
||||
mIsLayoutRtl = forView.getResources().getConfiguration()
|
||||
.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getVirtualViewAt(float x, float y) {
|
||||
return getVirtualViewIdIndexFromX(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getVisibleVirtualViews(List<Integer> list) {
|
||||
for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) {
|
||||
list.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
|
||||
Bundle arguments) {
|
||||
if (virtualViewId == ExploreByTouchHelper.HOST_ID) {
|
||||
// Do nothing
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLICK:
|
||||
LabeledSeekBar.this.setProgress(virtualViewId);
|
||||
sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateNodeForVirtualView(
|
||||
int virtualViewId, AccessibilityNodeInfoCompat node) {
|
||||
node.setClassName(RadioButton.class.getName());
|
||||
node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId));
|
||||
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
node.setContentDescription(mLabels[virtualViewId]);
|
||||
node.setClickable(true);
|
||||
node.setCheckable(true);
|
||||
node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
|
||||
event.setClassName(RadioButton.class.getName());
|
||||
event.setContentDescription(mLabels[virtualViewId]);
|
||||
event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) {
|
||||
node.setClassName(RadioGroup.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateEventForHost(AccessibilityEvent event) {
|
||||
event.setClassName(RadioGroup.class.getName());
|
||||
}
|
||||
|
||||
private int getHalfVirtualViewWidth() {
|
||||
final int width = LabeledSeekBar.this.getWidth();
|
||||
final int barWidth = width - LabeledSeekBar.this.getPaddingStart()
|
||||
- LabeledSeekBar.this.getPaddingEnd();
|
||||
return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2));
|
||||
}
|
||||
|
||||
private int getVirtualViewIdIndexFromX(float x) {
|
||||
int posBase = Math.max(0,
|
||||
((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth());
|
||||
posBase = (posBase + 1) / 2;
|
||||
posBase = Math.min(posBase, LabeledSeekBar.this.getMax());
|
||||
return mIsLayoutRtl ? LabeledSeekBar.this.getMax() - posBase : posBase;
|
||||
}
|
||||
|
||||
private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) {
|
||||
final int updatedVirtualViewId = mIsLayoutRtl
|
||||
? LabeledSeekBar.this.getMax() - virtualViewId : virtualViewId;
|
||||
int left = (updatedVirtualViewId * 2 - 1) * getHalfVirtualViewWidth()
|
||||
+ LabeledSeekBar.this.getPaddingStart();
|
||||
int right = (updatedVirtualViewId * 2 + 1) * getHalfVirtualViewWidth()
|
||||
+ LabeledSeekBar.this.getPaddingStart();
|
||||
|
||||
// Edge case
|
||||
left = updatedVirtualViewId == 0 ? 0 : left;
|
||||
right = updatedVirtualViewId == LabeledSeekBar.this.getMax()
|
||||
? LabeledSeekBar.this.getWidth() : right;
|
||||
|
||||
final Rect r = new Rect();
|
||||
r.set(left, 0, right, LabeledSeekBar.this.getHeight());
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.Utils;
|
||||
|
||||
/**
|
||||
* A labeled {@link SeekBarPreference} with left and right text label, icon label, or both.
|
||||
*
|
||||
* <p>
|
||||
* The component provides the attribute usage below.
|
||||
* <attr name="textStart" format="reference" />
|
||||
* <attr name="textEnd" format="reference" />
|
||||
* <attr name="tickMark" format="reference" />
|
||||
* <attr name="iconStart" format="reference" />
|
||||
* <attr name="iconEnd" format="reference" />
|
||||
* <attr name="iconStartContentDescription" format="reference" />
|
||||
* <attr name="iconEndContentDescription" format="reference" />
|
||||
* </p>
|
||||
*
|
||||
* <p> If you set the attribute values {@code iconStartContentDescription} or {@code
|
||||
* iconEndContentDescription} from XML, you must also set the corresponding attributes {@code
|
||||
* iconStart} or {@code iconEnd}, otherwise throws an {@link IllegalArgumentException}.</p>
|
||||
*/
|
||||
public class LabeledSeekBarPreference extends SeekBarPreference {
|
||||
|
||||
private final int mTextStartId;
|
||||
private final int mTextEndId;
|
||||
private final int mTickMarkId;
|
||||
private final int mIconStartId;
|
||||
private final int mIconEndId;
|
||||
private final int mIconStartContentDescriptionId;
|
||||
private final int mIconEndContentDescriptionId;
|
||||
private OnPreferenceChangeListener mStopListener;
|
||||
private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener;
|
||||
|
||||
private SeekBar mSeekBar;
|
||||
|
||||
public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setLayoutResource(R.layout.preference_labeled_slider);
|
||||
|
||||
final TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.LabeledSeekBarPreference);
|
||||
mTextStartId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_textStart, /* defValue= */ 0);
|
||||
mTextEndId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_textEnd, /* defValue= */ 0);
|
||||
mTickMarkId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_tickMark, /* defValue= */ 0);
|
||||
mIconStartId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_iconStart, /* defValue= */ 0);
|
||||
mIconEndId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_iconEnd, /* defValue= */ 0);
|
||||
|
||||
mIconStartContentDescriptionId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_iconStartContentDescription,
|
||||
/* defValue= */ 0);
|
||||
Preconditions.checkArgument(!(mIconStartContentDescriptionId != 0 && mIconStartId == 0),
|
||||
"The resource of the iconStart attribute may be invalid or not set, "
|
||||
+ "you should set the iconStart attribute and have the valid resource.");
|
||||
|
||||
mIconEndContentDescriptionId = styledAttrs.getResourceId(
|
||||
R.styleable.LabeledSeekBarPreference_iconEndContentDescription,
|
||||
/* defValue= */ 0);
|
||||
Preconditions.checkArgument(!(mIconEndContentDescriptionId != 0 && mIconEndId == 0),
|
||||
"The resource of the iconEnd attribute may be invalid or not set, "
|
||||
+ "you should set the iconEnd attribute and have the valid resource.");
|
||||
styledAttrs.recycle();
|
||||
}
|
||||
|
||||
public LabeledSeekBarPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, TypedArrayUtils.getAttr(context,
|
||||
androidx.preference.R.attr.seekBarPreferenceStyle,
|
||||
com.android.internal.R.attr.seekBarPreferenceStyle), 0);
|
||||
}
|
||||
|
||||
public SeekBar getSeekbar() {
|
||||
return mSeekBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
|
||||
boolean isSummaryVisible = false;
|
||||
if (summaryView != null) {
|
||||
isSummaryVisible = (summaryView.getVisibility() == View.VISIBLE);
|
||||
}
|
||||
final TextView titleView = (TextView) holder.findViewById(android.R.id.title);
|
||||
if (titleView != null && !isSelectable() && isEnabled() && isSummaryVisible) {
|
||||
titleView.setTextColor(
|
||||
Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary));
|
||||
}
|
||||
|
||||
final TextView startText = (TextView) holder.findViewById(android.R.id.text1);
|
||||
if (mTextStartId > 0) {
|
||||
startText.setText(mTextStartId);
|
||||
}
|
||||
|
||||
final TextView endText = (TextView) holder.findViewById(android.R.id.text2);
|
||||
if (mTextEndId > 0) {
|
||||
endText.setText(mTextEndId);
|
||||
}
|
||||
|
||||
final View labelFrame = holder.findViewById(R.id.label_frame);
|
||||
final boolean isValidTextResIdExist = mTextStartId > 0 || mTextEndId > 0;
|
||||
labelFrame.setVisibility(isValidTextResIdExist ? View.VISIBLE : View.GONE);
|
||||
|
||||
mSeekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar);
|
||||
if (mTickMarkId != 0) {
|
||||
final Drawable tickMark = getContext().getDrawable(mTickMarkId);
|
||||
mSeekBar.setTickMark(tickMark);
|
||||
}
|
||||
|
||||
final ViewGroup iconStartFrame = (ViewGroup) holder.findViewById(R.id.icon_start_frame);
|
||||
final ImageView iconStartView = (ImageView) holder.findViewById(R.id.icon_start);
|
||||
updateIconStartIfNeeded(iconStartFrame, iconStartView, mSeekBar);
|
||||
|
||||
final ViewGroup iconEndFrame = (ViewGroup) holder.findViewById(R.id.icon_end_frame);
|
||||
final ImageView iconEndView = (ImageView) holder.findViewById(R.id.icon_end);
|
||||
updateIconEndIfNeeded(iconEndFrame, iconEndView, mSeekBar);
|
||||
}
|
||||
|
||||
public void setOnPreferenceChangeStopListener(OnPreferenceChangeListener listener) {
|
||||
mStopListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
super.onStartTrackingTouch(seekBar);
|
||||
|
||||
if (mSeekBarChangeListener != null) {
|
||||
mSeekBarChangeListener.onStartTrackingTouch(seekBar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
super.onProgressChanged(seekBar, progress, fromUser);
|
||||
|
||||
if (mSeekBarChangeListener != null) {
|
||||
mSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
super.onStopTrackingTouch(seekBar);
|
||||
|
||||
if (mSeekBarChangeListener != null) {
|
||||
mSeekBarChangeListener.onStopTrackingTouch(seekBar);
|
||||
}
|
||||
|
||||
if (mStopListener != null) {
|
||||
mStopListener.onPreferenceChange(this, seekBar.getProgress());
|
||||
}
|
||||
|
||||
// Need to update the icon enabled status
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
public void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener seekBarChangeListener) {
|
||||
mSeekBarChangeListener = seekBarChangeListener;
|
||||
}
|
||||
|
||||
private void updateIconStartIfNeeded(ViewGroup iconFrame, ImageView iconStart,
|
||||
SeekBar seekBar) {
|
||||
if (mIconStartId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (iconStart.getDrawable() == null) {
|
||||
iconStart.setImageResource(mIconStartId);
|
||||
}
|
||||
|
||||
if (mIconStartContentDescriptionId != 0) {
|
||||
final String contentDescription =
|
||||
iconFrame.getContext().getString(mIconStartContentDescriptionId);
|
||||
iconFrame.setContentDescription(contentDescription);
|
||||
}
|
||||
|
||||
iconFrame.setOnClickListener((view) -> {
|
||||
final int progress = getProgress();
|
||||
if (progress > 0) {
|
||||
setProgress(progress - 1);
|
||||
}
|
||||
});
|
||||
|
||||
iconFrame.setVisibility(View.VISIBLE);
|
||||
setIconViewAndFrameEnabled(iconStart, seekBar.getProgress() > 0);
|
||||
}
|
||||
|
||||
private void updateIconEndIfNeeded(ViewGroup iconFrame, ImageView iconEnd, SeekBar seekBar) {
|
||||
if (mIconEndId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (iconEnd.getDrawable() == null) {
|
||||
iconEnd.setImageResource(mIconEndId);
|
||||
}
|
||||
|
||||
if (mIconEndContentDescriptionId != 0) {
|
||||
final String contentDescription =
|
||||
iconFrame.getContext().getString(mIconEndContentDescriptionId);
|
||||
iconFrame.setContentDescription(contentDescription);
|
||||
}
|
||||
|
||||
iconFrame.setOnClickListener((view) -> {
|
||||
final int progress = getProgress();
|
||||
if (progress < getMax()) {
|
||||
setProgress(progress + 1);
|
||||
}
|
||||
});
|
||||
|
||||
iconFrame.setVisibility(View.VISIBLE);
|
||||
setIconViewAndFrameEnabled(iconEnd, seekBar.getProgress() < seekBar.getMax());
|
||||
}
|
||||
|
||||
private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) {
|
||||
iconView.setEnabled(enabled);
|
||||
final ViewGroup iconFrame = (ViewGroup) iconView.getParent();
|
||||
iconFrame.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
/** A preference which supports linkify text in the summary **/
|
||||
public class LinkifySummaryPreference extends Preference {
|
||||
|
||||
public LinkifySummaryPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public LinkifySummaryPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
|
||||
if (summaryView == null || summaryView.getVisibility() != View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
final CharSequence summary = getSummary();
|
||||
if (!TextUtils.isEmpty(summary)) {
|
||||
final SpannableString spannableSummary = new SpannableString(summary);
|
||||
if (spannableSummary.getSpans(0, spannableSummary.length(), ClickableSpan.class)
|
||||
.length > 0) {
|
||||
summaryView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A helper class that manages show/hide loading spinner, content view and empty view (optional).
|
||||
*/
|
||||
public class LoadingViewController {
|
||||
|
||||
private static final long DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS = 100L;
|
||||
|
||||
private final Handler mFgHandler;
|
||||
private final View mLoadingView;
|
||||
private final View mContentView;
|
||||
private final View mEmptyView;
|
||||
|
||||
public LoadingViewController(View loadingView, View contentView) {
|
||||
this(loadingView, contentView, null /* emptyView*/);
|
||||
}
|
||||
|
||||
public LoadingViewController(View loadingView, View contentView, @Nullable View emptyView) {
|
||||
mLoadingView = loadingView;
|
||||
mContentView = contentView;
|
||||
mEmptyView = emptyView;
|
||||
mFgHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
private Runnable mShowLoadingContainerRunnable = new Runnable() {
|
||||
public void run() {
|
||||
showLoadingView();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows content view and hides loading view & empty view.
|
||||
*/
|
||||
public void showContent(boolean animate) {
|
||||
// Cancel any pending task to show the loading animation and show the list of
|
||||
// apps directly.
|
||||
mFgHandler.removeCallbacks(mShowLoadingContainerRunnable);
|
||||
handleLoadingContainer(true /* showContent */, false /* showEmpty*/, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows empty view and hides loading view & content view.
|
||||
*/
|
||||
public void showEmpty(boolean animate) {
|
||||
if (mEmptyView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any pending task to show the loading animation and show the list of
|
||||
// apps directly.
|
||||
mFgHandler.removeCallbacks(mShowLoadingContainerRunnable);
|
||||
handleLoadingContainer(false /* showContent */, true /* showEmpty */, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows loading view and hides content view & empty view.
|
||||
*/
|
||||
public void showLoadingView() {
|
||||
handleLoadingContainer(false /* showContent */, false /* showEmpty */, false /* animate */);
|
||||
}
|
||||
|
||||
public void showLoadingViewDelayed() {
|
||||
mFgHandler.postDelayed(
|
||||
mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS);
|
||||
}
|
||||
|
||||
private void handleLoadingContainer(boolean showContent, boolean showEmpty, boolean animate) {
|
||||
handleLoadingContainer(mLoadingView, mContentView, mEmptyView,
|
||||
showContent, showEmpty, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/hide loading view and content view.
|
||||
*
|
||||
* @param loading The loading spinner view
|
||||
* @param content The content view
|
||||
* @param done If true, content is set visible and loading is set invisible.
|
||||
* @param animate Whether or not content/loading views should animate in/out.
|
||||
*/
|
||||
public static void handleLoadingContainer(View loading, View content, boolean done,
|
||||
boolean animate) {
|
||||
setViewShown(loading, !done, animate);
|
||||
setViewShown(content, done, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/hide loading view and content view and empty view.
|
||||
*
|
||||
* @param loading The loading spinner view
|
||||
* @param content The content view
|
||||
* @param empty The empty view shows no item summary to users.
|
||||
* @param showContent If true, content is set visible and loading is set invisible.
|
||||
* @param showEmpty If true, empty is set visible and loading is set invisible.
|
||||
* @param animate Whether or not content/loading views should animate in/out.
|
||||
*/
|
||||
public static void handleLoadingContainer(View loading, View content, View empty,
|
||||
boolean showContent, boolean showEmpty, boolean animate) {
|
||||
if (empty != null) {
|
||||
setViewShown(empty, showEmpty, animate);
|
||||
}
|
||||
setViewShown(content, showContent, animate);
|
||||
setViewShown(loading, !showContent && !showEmpty, animate);
|
||||
}
|
||||
|
||||
private static void setViewShown(final View view, boolean shown, boolean animate) {
|
||||
if (animate) {
|
||||
Animation animation = AnimationUtils.loadAnimation(view.getContext(),
|
||||
shown ? android.R.anim.fade_in : android.R.anim.fade_out);
|
||||
if (shown) {
|
||||
view.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
animation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
view.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
view.startAnimation(animation);
|
||||
} else {
|
||||
view.clearAnimation();
|
||||
view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.settings.widget;
|
||||
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
|
||||
/**
|
||||
* The switch controller that is used to update the switch widget in the SettingsMainSwitchBar.
|
||||
*/
|
||||
public class MainSwitchBarController extends SwitchWidgetController implements
|
||||
OnCheckedChangeListener {
|
||||
|
||||
private final SettingsMainSwitchBar mMainSwitch;
|
||||
|
||||
public MainSwitchBarController(SettingsMainSwitchBar mainSwitch) {
|
||||
mMainSwitch = mainSwitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupView() {
|
||||
mMainSwitch.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardownView() {
|
||||
mMainSwitch.hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
mMainSwitch.setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startListening() {
|
||||
mMainSwitch.addOnSwitchChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopListening() {
|
||||
mMainSwitch.removeOnSwitchChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
mMainSwitch.setChecked(checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return mMainSwitch.isChecked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
mMainSwitch.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (mListener != null) {
|
||||
mListener.onSwitchToggled(isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisabledByAdmin(RestrictedLockUtils.EnforcedAdmin admin) {
|
||||
mMainSwitch.setDisabledByAdmin(admin);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.settings.widget;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.view.Surface;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A {@link VideoPreference.AnimationController} containing a {@link
|
||||
* MediaPlayer}. The controller is used by {@link VideoPreference} to display
|
||||
* a mp4 resource.
|
||||
*/
|
||||
class MediaAnimationController implements VideoPreference.AnimationController {
|
||||
private MediaPlayer mMediaPlayer;
|
||||
private boolean mVideoReady;
|
||||
private Surface mSurface;
|
||||
|
||||
MediaAnimationController(Context context, int videoId) {
|
||||
final Uri videoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||
.authority(context.getPackageName())
|
||||
.appendPath(String.valueOf(videoId))
|
||||
.build();
|
||||
mMediaPlayer = MediaPlayer.create(context, videoPath);
|
||||
// when the playback res is invalid or others, MediaPlayer create may fail
|
||||
// and return null, so need add the null judgement.
|
||||
if (mMediaPlayer != null) {
|
||||
mMediaPlayer.seekTo(0);
|
||||
mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true);
|
||||
mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVideoWidth() {
|
||||
return mMediaPlayer.getVideoWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVideoHeight() {
|
||||
return mMediaPlayer.getVideoHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
mMediaPlayer.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mMediaPlayer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mMediaPlayer.isPlaying();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() {
|
||||
return mMediaPlayer.getDuration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachView(TextureView video, View preview, View playButton) {
|
||||
updateViewStates(preview, playButton);
|
||||
video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width,
|
||||
int height) {
|
||||
setSurface(surfaceTexture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width,
|
||||
int height) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
|
||||
preview.setVisibility(View.VISIBLE);
|
||||
mSurface = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
|
||||
setSurface(surfaceTexture);
|
||||
if (mVideoReady) {
|
||||
if (preview.getVisibility() == View.VISIBLE) {
|
||||
preview.setVisibility(View.GONE);
|
||||
}
|
||||
if (mMediaPlayer != null
|
||||
&& !mMediaPlayer.isPlaying()) {
|
||||
mMediaPlayer.start();
|
||||
playButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
if (mMediaPlayer != null && !mMediaPlayer.isPlaying()
|
||||
&& playButton.getVisibility() != View.VISIBLE) {
|
||||
playButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
video.setOnClickListener(v -> updateViewStates(preview, playButton));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mMediaPlayer != null) {
|
||||
mMediaPlayer.stop();
|
||||
mMediaPlayer.reset();
|
||||
mMediaPlayer.release();
|
||||
mMediaPlayer = null;
|
||||
mVideoReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateViewStates(View imageView, View playButton) {
|
||||
if (mMediaPlayer.isPlaying()) {
|
||||
mMediaPlayer.pause();
|
||||
playButton.setVisibility(View.VISIBLE);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
imageView.setVisibility(View.GONE);
|
||||
playButton.setVisibility(View.GONE);
|
||||
mMediaPlayer.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void setSurface(SurfaceTexture surfaceTexture) {
|
||||
if (mMediaPlayer != null && mSurface == null) {
|
||||
mSurface = new Surface(surfaceTexture);
|
||||
mMediaPlayer.setSurface(mSurface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.utils.ColorUtil;
|
||||
|
||||
/** A preference with a Gear on the side and mutable Gear color. */
|
||||
public class MutableGearPreference extends GearPreference {
|
||||
private static final int VALUE_ENABLED_ALPHA = 255;
|
||||
|
||||
private ImageView mGear;
|
||||
private Context mContext;
|
||||
private int mDisabledAlphaValue;
|
||||
|
||||
public MutableGearPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
mDisabledAlphaValue = (int) (ColorUtil.getDisabledAlpha(context) * VALUE_ENABLED_ALPHA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGearEnabled(boolean enabled) {
|
||||
if (mGear != null) {
|
||||
mGear.setEnabled(enabled);
|
||||
mGear.setImageAlpha(enabled ? VALUE_ENABLED_ALPHA : mDisabledAlphaValue);
|
||||
}
|
||||
mGearState = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mGear = (ImageView) holder.findViewById(R.id.settings_button);
|
||||
setGearEnabled(mGearState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A controller for generic Preference categories. If all controllers for its children reports
|
||||
* not-available, this controller will also report not-available, and subsequently will be hidden by
|
||||
* UI.
|
||||
*/
|
||||
public class PreferenceCategoryController extends BasePreferenceController {
|
||||
|
||||
private final String mKey;
|
||||
private final List<AbstractPreferenceController> mChildren;
|
||||
|
||||
public PreferenceCategoryController(Context context, String key) {
|
||||
super(context, key);
|
||||
mKey = key;
|
||||
mChildren = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
if (mChildren == null || mChildren.isEmpty()) {
|
||||
return UNSUPPORTED_ON_DEVICE;
|
||||
}
|
||||
// Category is available if any child is available
|
||||
for (AbstractPreferenceController controller : mChildren) {
|
||||
if (controller.isAvailable()) {
|
||||
return AVAILABLE;
|
||||
}
|
||||
}
|
||||
return CONDITIONALLY_UNAVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferenceKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public PreferenceCategoryController setChildren(
|
||||
List<AbstractPreferenceController> childrenController) {
|
||||
mChildren.clear();
|
||||
if (childrenController != null) {
|
||||
mChildren.addAll(childrenController);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.widget.TwoTargetPreference;
|
||||
|
||||
/**
|
||||
* A custom preference that provides inline checkbox. It has a mandatory field for title, and
|
||||
* optional fields for icon and sub-text.
|
||||
*/
|
||||
public class PrimaryCheckBoxPreference extends TwoTargetPreference {
|
||||
|
||||
private CheckBox mCheckBox;
|
||||
private boolean mChecked;
|
||||
private boolean mEnableCheckBox = true;
|
||||
|
||||
public PrimaryCheckBoxPreference(Context context, AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public PrimaryCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public PrimaryCheckBoxPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public PrimaryCheckBoxPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSecondTargetResId() {
|
||||
return R.layout.preference_widget_primary_checkbox;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final View widgetView = holder.findViewById(android.R.id.widget_frame);
|
||||
if (widgetView != null) {
|
||||
widgetView.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mCheckBox != null && !mCheckBox.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
setChecked(!mChecked);
|
||||
if (!callChangeListener(mChecked)) {
|
||||
setChecked(!mChecked);
|
||||
} else {
|
||||
persistBoolean(mChecked);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mCheckBox = (CheckBox) holder.findViewById(R.id.checkboxWidget);
|
||||
if (mCheckBox != null) {
|
||||
mCheckBox.setContentDescription(getTitle());
|
||||
mCheckBox.setChecked(mChecked);
|
||||
mCheckBox.setEnabled(mEnableCheckBox);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
setCheckBoxEnabled(enabled);
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mCheckBox != null && mChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the check status of checkbox
|
||||
* @param checked
|
||||
*/
|
||||
public void setChecked(boolean checked) {
|
||||
mChecked = checked;
|
||||
if (mCheckBox != null) {
|
||||
mCheckBox.setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the enabled status of CheckBox
|
||||
* @param enabled
|
||||
*/
|
||||
public void setCheckBoxEnabled(boolean enabled) {
|
||||
mEnableCheckBox = enabled;
|
||||
if (mCheckBox != null) {
|
||||
mCheckBox.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public CheckBox getCheckBox() {
|
||||
return mCheckBox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.annotation.AnyRes;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.core.PreferenceXmlParserUtils;
|
||||
import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
|
||||
import com.android.settingslib.widget.CandidateInfo;
|
||||
import com.android.settingslib.widget.IllustrationPreference;
|
||||
import com.android.settingslib.widget.SelectorWithWidgetPreference;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A fragment to handle general radio button picker
|
||||
*/
|
||||
public abstract class RadioButtonPickerFragment extends SettingsPreferenceFragment implements
|
||||
SelectorWithWidgetPreference.OnClickListener {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String EXTRA_FOR_WORK = "for_work";
|
||||
private static final String TAG = "RadioButtonPckrFrgmt";
|
||||
@VisibleForTesting
|
||||
boolean mAppendStaticPreferences = false;
|
||||
|
||||
private final Map<String, CandidateInfo> mCandidates = new ArrayMap<>();
|
||||
|
||||
protected UserManager mUserManager;
|
||||
protected int mUserId;
|
||||
private int mIllustrationId;
|
||||
private int mIllustrationPreviewId;
|
||||
private IllustrationType mIllustrationType;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
|
||||
final Bundle arguments = getArguments();
|
||||
|
||||
boolean mForWork = false;
|
||||
if (arguments != null) {
|
||||
mForWork = arguments.getBoolean(EXTRA_FOR_WORK);
|
||||
}
|
||||
final UserHandle managedProfile = Utils.getManagedProfile(mUserManager);
|
||||
mUserId = mForWork && managedProfile != null
|
||||
? managedProfile.getIdentifier()
|
||||
: UserHandle.myUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
try {
|
||||
// Check if the xml specifies if static preferences should go on the top or bottom
|
||||
final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(getContext(),
|
||||
getPreferenceScreenResId(),
|
||||
MetadataFlag.FLAG_INCLUDE_PREF_SCREEN |
|
||||
MetadataFlag.FLAG_NEED_PREF_APPEND);
|
||||
mAppendStaticPreferences = metadata.get(0)
|
||||
.getBoolean(PreferenceXmlParserUtils.METADATA_APPEND);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error trying to open xml file", e);
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.e(TAG, "Error parsing xml", e);
|
||||
}
|
||||
updateCandidates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
final View view = super.onCreateView(inflater, container, savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected abstract int getPreferenceScreenResId();
|
||||
|
||||
@Override
|
||||
public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
|
||||
final String selectedKey = selected.getKey();
|
||||
onRadioButtonConfirmed(selectedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the user tries to select an item.
|
||||
*/
|
||||
protected void onSelectionPerformed(boolean success) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the UI should show a "None" item selection.
|
||||
*/
|
||||
protected boolean shouldShowItemNone() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate any static preferences, independent of the radio buttons.
|
||||
* These might be used to provide extra information about the choices.
|
||||
**/
|
||||
protected void addStaticPreferences(PreferenceScreen screen) {
|
||||
}
|
||||
|
||||
protected CandidateInfo getCandidate(String key) {
|
||||
return mCandidates.get(key);
|
||||
}
|
||||
|
||||
protected void onRadioButtonConfirmed(String selectedKey) {
|
||||
final boolean success = setDefaultKey(selectedKey);
|
||||
if (success) {
|
||||
updateCheckedState(selectedKey);
|
||||
}
|
||||
onSelectionPerformed(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* A chance for subclasses to bind additional things to the preference.
|
||||
*/
|
||||
public void bindPreferenceExtra(SelectorWithWidgetPreference pref,
|
||||
String key, CandidateInfo info, String defaultKey, String systemDefaultKey) {
|
||||
}
|
||||
|
||||
public void updateCandidates() {
|
||||
mCandidates.clear();
|
||||
final List<? extends CandidateInfo> candidateList = getCandidates();
|
||||
if (candidateList != null) {
|
||||
for (CandidateInfo info : candidateList) {
|
||||
mCandidates.put(info.getKey(), info);
|
||||
}
|
||||
}
|
||||
final String defaultKey = getDefaultKey();
|
||||
final String systemDefaultKey = getSystemDefaultKey();
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
screen.removeAll();
|
||||
if (mIllustrationId != 0) {
|
||||
addIllustration(screen);
|
||||
}
|
||||
if (!mAppendStaticPreferences) {
|
||||
addStaticPreferences(screen);
|
||||
}
|
||||
|
||||
final int customLayoutResId = getRadioButtonPreferenceCustomLayoutResId();
|
||||
if (shouldShowItemNone()) {
|
||||
final SelectorWithWidgetPreference nonePref =
|
||||
new SelectorWithWidgetPreference(getPrefContext());
|
||||
if (customLayoutResId > 0) {
|
||||
nonePref.setLayoutResource(customLayoutResId);
|
||||
}
|
||||
nonePref.setIcon(R.drawable.ic_remove_circle);
|
||||
nonePref.setTitle(R.string.app_list_preference_none);
|
||||
nonePref.setChecked(TextUtils.isEmpty(defaultKey));
|
||||
nonePref.setOnClickListener(this);
|
||||
screen.addPreference(nonePref);
|
||||
}
|
||||
if (candidateList != null) {
|
||||
for (CandidateInfo info : candidateList) {
|
||||
SelectorWithWidgetPreference pref =
|
||||
new SelectorWithWidgetPreference(getPrefContext());
|
||||
if (customLayoutResId > 0) {
|
||||
pref.setLayoutResource(customLayoutResId);
|
||||
}
|
||||
bindPreference(pref, info.getKey(), info, defaultKey);
|
||||
bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey);
|
||||
screen.addPreference(pref);
|
||||
}
|
||||
}
|
||||
mayCheckOnlyRadioButton();
|
||||
if (mAppendStaticPreferences) {
|
||||
addStaticPreferences(screen);
|
||||
}
|
||||
}
|
||||
|
||||
public SelectorWithWidgetPreference bindPreference(SelectorWithWidgetPreference pref,
|
||||
String key, CandidateInfo info, String defaultKey) {
|
||||
pref.setTitle(info.loadLabel());
|
||||
pref.setIcon(Utils.getSafeIcon(info.loadIcon()));
|
||||
pref.setKey(key);
|
||||
if (TextUtils.equals(defaultKey, key)) {
|
||||
pref.setChecked(true);
|
||||
}
|
||||
pref.setEnabled(info.enabled);
|
||||
pref.setOnClickListener(this);
|
||||
return pref;
|
||||
}
|
||||
|
||||
public void updateCheckedState(String selectedKey) {
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
if (screen != null) {
|
||||
final int count = screen.getPreferenceCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final Preference pref = screen.getPreference(i);
|
||||
if (pref instanceof SelectorWithWidgetPreference) {
|
||||
final SelectorWithWidgetPreference radioPref =
|
||||
(SelectorWithWidgetPreference) pref;
|
||||
final boolean newCheckedState = TextUtils.equals(pref.getKey(), selectedKey);
|
||||
if (radioPref.isChecked() != newCheckedState) {
|
||||
radioPref.setChecked(TextUtils.equals(pref.getKey(), selectedKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void mayCheckOnlyRadioButton() {
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
// If there is only 1 thing on screen, select it.
|
||||
if (screen != null && screen.getPreferenceCount() == 1) {
|
||||
final Preference onlyPref = screen.getPreference(0);
|
||||
if (onlyPref instanceof SelectorWithWidgetPreference) {
|
||||
((SelectorWithWidgetPreference) onlyPref).setChecked(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to set an illustration at the top of this screen. Set the illustration id to 0
|
||||
* if you want to remove the illustration.
|
||||
*
|
||||
* @param illustrationId The res id for the raw of the illustration.
|
||||
* @param previewId The res id for the drawable of the illustration.
|
||||
* @param illustrationType The illustration type for the raw of the illustration.
|
||||
*/
|
||||
protected void setIllustration(@AnyRes int illustrationId, @AnyRes int previewId,
|
||||
IllustrationType illustrationType) {
|
||||
mIllustrationId = illustrationId;
|
||||
mIllustrationPreviewId = previewId;
|
||||
mIllustrationType = illustrationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to set an illustration at the top of this screen. Set the illustration id to 0
|
||||
* if you want to remove the illustration.
|
||||
*
|
||||
* @param illustrationId The res id for the raw of the illustration.
|
||||
* @param illustrationType The illustration type for the raw of the illustration.
|
||||
*/
|
||||
protected void setIllustration(@AnyRes int illustrationId, IllustrationType illustrationType) {
|
||||
setIllustration(illustrationId, 0, illustrationType);
|
||||
}
|
||||
|
||||
private void addIllustration(PreferenceScreen screen) {
|
||||
switch (mIllustrationType) {
|
||||
case LOTTIE_ANIMATION:
|
||||
IllustrationPreference illustrationPreference = new IllustrationPreference(
|
||||
getContext());
|
||||
illustrationPreference.setLottieAnimationResId(mIllustrationId);
|
||||
screen.addPreference(illustrationPreference);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid illustration type: " + mIllustrationType);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract List<? extends CandidateInfo> getCandidates();
|
||||
|
||||
protected abstract String getDefaultKey();
|
||||
|
||||
protected abstract boolean setDefaultKey(String key);
|
||||
|
||||
protected String getSystemDefaultKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a custom layout for each candidate row.
|
||||
*/
|
||||
@LayoutRes
|
||||
protected int getRadioButtonPreferenceCustomLayoutResId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected enum IllustrationType {
|
||||
LOTTIE_ANIMATION
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.UserHandle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedPreferenceHelper;
|
||||
import com.android.settingslib.widget.AppPreference;
|
||||
|
||||
/**
|
||||
* {@link AppPreference} that implements user restriction utilities using
|
||||
* {@link com.android.settingslib.RestrictedPreferenceHelper}.
|
||||
* Used to show policy transparency on {@link AppPreference}.
|
||||
*/
|
||||
public class RestrictedAppPreference extends AppPreference {
|
||||
private RestrictedPreferenceHelper mHelper;
|
||||
private String userRestriction;
|
||||
|
||||
public RestrictedAppPreference(Context context) {
|
||||
super(context);
|
||||
initialize(null, null);
|
||||
}
|
||||
|
||||
public RestrictedAppPreference(Context context, String userRestriction) {
|
||||
super(context);
|
||||
initialize(null, userRestriction);
|
||||
}
|
||||
|
||||
public RestrictedAppPreference(Context context, AttributeSet attrs, String userRestriction) {
|
||||
super(context, attrs);
|
||||
initialize(attrs, userRestriction);
|
||||
}
|
||||
|
||||
private void initialize(AttributeSet attrs, String userRestriction) {
|
||||
mHelper = new RestrictedPreferenceHelper(getContext(), this, attrs);
|
||||
this.userRestriction = userRestriction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mHelper.onBindViewHolder(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performClick() {
|
||||
if (!mHelper.performClick()) {
|
||||
super.performClick();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
boolean changed = false;
|
||||
if (enabled && isDisabledByAdmin()) {
|
||||
mHelper.setDisabledByAdmin(null);
|
||||
changed = true;
|
||||
}
|
||||
if (enabled && isDisabledByEcm()) {
|
||||
mHelper.setDisabledByEcm(null);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDisabledByAdmin(RestrictedLockUtils.EnforcedAdmin admin) {
|
||||
if (mHelper.setDisabledByAdmin(admin)) {
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDisabledByAdmin() {
|
||||
return mHelper.isDisabledByAdmin();
|
||||
}
|
||||
|
||||
public boolean isDisabledByEcm() {
|
||||
return mHelper.isDisabledByEcm();
|
||||
}
|
||||
|
||||
public void useAdminDisabledSummary(boolean useSummary) {
|
||||
mHelper.useAdminDisabledSummary(useSummary);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
|
||||
mHelper.onAttachedToHierarchy();
|
||||
super.onAttachedToHierarchy(preferenceManager);
|
||||
}
|
||||
|
||||
public void checkRestrictionAndSetDisabled() {
|
||||
if (TextUtils.isEmpty(userRestriction)) {
|
||||
return;
|
||||
}
|
||||
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
|
||||
}
|
||||
|
||||
public void checkRestrictionAndSetDisabled(String userRestriction) {
|
||||
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
|
||||
}
|
||||
|
||||
public void checkRestrictionAndSetDisabled(String userRestriction, int userId) {
|
||||
mHelper.checkRestrictionAndSetDisabled(userRestriction, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
|
||||
* package. Marks the preference as disabled if so.
|
||||
* @param settingIdentifier The key identifying the setting
|
||||
* @param packageName the package to check the settingIdentifier for
|
||||
*/
|
||||
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
|
||||
@NonNull String packageName) {
|
||||
mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.UserHandle;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
|
||||
/**
|
||||
* A preference with a plus button on the side representing an "add" action. The plus button will
|
||||
* only be visible when a non-null click listener is registered.
|
||||
*/
|
||||
public class RestrictedButton extends Button {
|
||||
|
||||
private UserHandle mUserHandle;
|
||||
private String mUserRestriction;
|
||||
|
||||
public RestrictedButton(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public RestrictedButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public RestrictedButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public RestrictedButton(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performClick() {
|
||||
EnforcedAdmin admin = getEnforcedAdmin();
|
||||
if (admin != null) {
|
||||
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, admin);
|
||||
return false;
|
||||
}
|
||||
return super.performClick();
|
||||
}
|
||||
|
||||
/** Initialize the button with {@link UserHandle} and a restriction */
|
||||
public void init(UserHandle userHandle, String restriction) {
|
||||
setAllowClickWhenDisabled(true);
|
||||
mUserHandle = userHandle;
|
||||
mUserRestriction = restriction;
|
||||
}
|
||||
|
||||
/** Update the restriction state */
|
||||
public void updateState() {
|
||||
setEnabled(getEnforcedAdmin() == null);
|
||||
}
|
||||
|
||||
private EnforcedAdmin getEnforcedAdmin() {
|
||||
if (mUserHandle != null) {
|
||||
EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
|
||||
mContext, mUserRestriction, mUserHandle.getIdentifier());
|
||||
if (admin != null) {
|
||||
return admin;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settingslib.RestrictedTopLevelPreference;
|
||||
|
||||
/** Homepage preference that can be disabled by a device admin using a user restriction. */
|
||||
public class RestrictedHomepagePreference extends RestrictedTopLevelPreference implements
|
||||
HomepagePreferenceLayoutHelper.HomepagePreferenceLayout {
|
||||
|
||||
private final HomepagePreferenceLayoutHelper mHelper;
|
||||
|
||||
public RestrictedHomepagePreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
public RestrictedHomepagePreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
public RestrictedHomepagePreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
public RestrictedHomepagePreference(Context context) {
|
||||
super(context);
|
||||
mHelper = new HomepagePreferenceLayoutHelper(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mHelper.onBindViewHolder(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomepagePreferenceLayoutHelper getHelper() {
|
||||
return mHelper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* A (determinate) progress bar in the form of a ring. The progress bar goes clockwise starting
|
||||
* from the 12 o'clock position. This view maintain equal width and height using a strategy similar
|
||||
* to "centerInside" for ImageView.
|
||||
*/
|
||||
public class RingProgressBar extends ProgressBar {
|
||||
|
||||
public RingProgressBar(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public RingProgressBar(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public RingProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, R.style.RingProgressBarStyle);
|
||||
}
|
||||
|
||||
public RingProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
final int measuredHeight = getMeasuredHeight();
|
||||
final int measuredWidth = getMeasuredWidth();
|
||||
final int shortSide = Math.min(measuredHeight, measuredWidth);
|
||||
setMeasuredDimension(shortSide, shortSide);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A {@link ViewPager} that's aware of RTL changes when used with FragmentPagerAdapter.
|
||||
*/
|
||||
public final class RtlCompatibleViewPager extends ViewPager {
|
||||
|
||||
/**
|
||||
* Callback interface for responding to changing state of the selected page.
|
||||
* Positions supplied will always be the logical position in the adapter -
|
||||
* that is, the 0 index corresponds to the left-most page in LTR and the
|
||||
* right-most page in RTL.
|
||||
*/
|
||||
|
||||
public RtlCompatibleViewPager(Context context) {
|
||||
this(context, null /* attrs */);
|
||||
}
|
||||
|
||||
public RtlCompatibleViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentItem() {
|
||||
return getRtlAwareIndex(super.getCurrentItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentItem(int item) {
|
||||
super.setCurrentItem(getRtlAwareIndex(item));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
Parcelable parcelable = super.onSaveInstanceState();
|
||||
|
||||
RtlSavedState rtlSavedState = new RtlSavedState(parcelable);
|
||||
rtlSavedState.position = getCurrentItem();
|
||||
return rtlSavedState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
RtlSavedState rtlSavedState = (RtlSavedState) state;
|
||||
super.onRestoreInstanceState(rtlSavedState.getSuperState());
|
||||
|
||||
setCurrentItem(rtlSavedState.position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a "RTL friendly" index. If the locale is LTR, the index is returned as is.
|
||||
* Otherwise it's transformed so view pager can render views using the new index for RTL. For
|
||||
* example, the second view will be rendered to the left of first view.
|
||||
*
|
||||
* @param index The logical index.
|
||||
*/
|
||||
public int getRtlAwareIndex(int index) {
|
||||
// Using TextUtils rather than View.getLayoutDirection() because LayoutDirection is not
|
||||
// defined until onMeasure, and this is called before then.
|
||||
if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
|
||||
== View.LAYOUT_DIRECTION_RTL) {
|
||||
return getAdapter().getCount() - index - 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
static class RtlSavedState extends BaseSavedState {
|
||||
int position;
|
||||
|
||||
public RtlSavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
private RtlSavedState(Parcel in, ClassLoader loader) {
|
||||
super(in, loader);
|
||||
position = in.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(position);
|
||||
}
|
||||
|
||||
public static final Parcelable.ClassLoaderCreator<RtlSavedState> CREATOR
|
||||
= new Parcelable.ClassLoaderCreator<RtlSavedState>() {
|
||||
@Override
|
||||
public RtlSavedState createFromParcel(Parcel source,
|
||||
ClassLoader loader) {
|
||||
return new RtlSavedState(source, loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RtlSavedState createFromParcel(Parcel in) {
|
||||
return new RtlSavedState(in, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RtlSavedState[] newArray(int size) {
|
||||
return new RtlSavedState[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ImeAwareEditText;
|
||||
|
||||
/**
|
||||
* An EditText that, instead of scrolling to itself when focused, will request scrolling to its
|
||||
* parent. This is used in ChooseLockPassword to do make a best effort for not hiding the error
|
||||
* messages for why the password is invalid under the keyboard.
|
||||
*/
|
||||
public class ScrollToParentEditText extends ImeAwareEditText {
|
||||
|
||||
private Rect mRect = new Rect();
|
||||
|
||||
public ScrollToParentEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
|
||||
ViewParent parent = getParent();
|
||||
if (parent instanceof View) {
|
||||
// Request the entire parent view to be shown, which in ChooseLockPassword's case,
|
||||
// will include messages for why the password is invalid (if any).
|
||||
((View) parent).getDrawingRect(mRect);
|
||||
return ((View) parent).requestRectangleOnScreen(mRect, immediate);
|
||||
} else {
|
||||
return super.requestRectangleOnScreen(rectangle, immediate);
|
||||
}
|
||||
}
|
||||
}
|
||||
478
Settings/src/com/android/settings/widget/SeekBarPreference.java
Normal file
478
Settings/src/com/android/settings/widget/SeekBarPreference.java
Normal file
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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 com.android.settings.widget;
|
||||
|
||||
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
|
||||
|
||||
import static com.android.internal.jank.InteractionJankMonitor.CUJ_SETTINGS_SLIDER;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener;
|
||||
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.internal.jank.InteractionJankMonitor;
|
||||
import com.android.settingslib.RestrictedPreference;
|
||||
|
||||
/**
|
||||
* Based on android.preference.SeekBarPreference, but uses support preference as base.
|
||||
*/
|
||||
public class SeekBarPreference extends RestrictedPreference
|
||||
implements OnSeekBarChangeListener, View.OnKeyListener, View.OnHoverListener {
|
||||
|
||||
public static final int HAPTIC_FEEDBACK_MODE_NONE = 0;
|
||||
public static final int HAPTIC_FEEDBACK_MODE_ON_TICKS = 1;
|
||||
public static final int HAPTIC_FEEDBACK_MODE_ON_ENDS = 2;
|
||||
|
||||
private final InteractionJankMonitor mJankMonitor = InteractionJankMonitor.getInstance();
|
||||
private int mProgress;
|
||||
private int mMax;
|
||||
private int mMin;
|
||||
private boolean mTrackingTouch;
|
||||
|
||||
private boolean mContinuousUpdates;
|
||||
private int mHapticFeedbackMode = HAPTIC_FEEDBACK_MODE_NONE;
|
||||
private int mDefaultProgress = -1;
|
||||
|
||||
private SeekBar mSeekBar;
|
||||
private boolean mShouldBlink;
|
||||
private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT;
|
||||
private CharSequence mOverrideSeekBarStateDescription;
|
||||
private CharSequence mSeekBarContentDescription;
|
||||
private CharSequence mSeekBarStateDescription;
|
||||
private OnSeekBarChangeListener mOnSeekBarChangeListener;
|
||||
|
||||
public SeekBarPreference(
|
||||
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(
|
||||
attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes);
|
||||
setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax));
|
||||
setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin));
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs,
|
||||
com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
|
||||
final int layoutResId = a.getResourceId(
|
||||
com.android.internal.R.styleable.SeekBarPreference_layout,
|
||||
com.android.internal.R.layout.preference_widget_seekbar);
|
||||
a.recycle();
|
||||
|
||||
setSelectable(false);
|
||||
|
||||
setLayoutResource(layoutResId);
|
||||
}
|
||||
|
||||
public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, TypedArrayUtils.getAttr(context,
|
||||
androidx.preference.R.attr.seekBarPreferenceStyle,
|
||||
com.android.internal.R.attr.seekBarPreferenceStyle));
|
||||
}
|
||||
|
||||
public SeekBarPreference(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback that notifies clients when the seekbar progress level has been
|
||||
* changed. See {@link OnSeekBarChangeListener} for more info.
|
||||
*/
|
||||
public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) {
|
||||
mOnSeekBarChangeListener = listener;
|
||||
}
|
||||
|
||||
public void setShouldBlink(boolean shouldBlink) {
|
||||
mShouldBlink = shouldBlink;
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelectable() {
|
||||
if(isDisabledByAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
return super.isSelectable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
||||
super.onBindViewHolder(view);
|
||||
view.itemView.setOnKeyListener(this);
|
||||
view.itemView.setOnHoverListener(this);
|
||||
mSeekBar = (SeekBar) view.findViewById(
|
||||
com.android.internal.R.id.seekbar);
|
||||
mSeekBar.setOnSeekBarChangeListener(this);
|
||||
mSeekBar.setMax(mMax);
|
||||
mSeekBar.setMin(mMin);
|
||||
mSeekBar.setProgress(mProgress);
|
||||
mSeekBar.setEnabled(isEnabled());
|
||||
final CharSequence title = getTitle();
|
||||
if (!TextUtils.isEmpty(mSeekBarContentDescription)) {
|
||||
mSeekBar.setContentDescription(mSeekBarContentDescription);
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mSeekBar.setContentDescription(title);
|
||||
}
|
||||
if (!TextUtils.isEmpty(mSeekBarStateDescription)) {
|
||||
mSeekBar.setStateDescription(mSeekBarStateDescription);
|
||||
}
|
||||
if (mSeekBar instanceof DefaultIndicatorSeekBar) {
|
||||
((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
|
||||
}
|
||||
if (mShouldBlink) {
|
||||
View v = view.itemView;
|
||||
v.post(() -> {
|
||||
if (v.getBackground() != null) {
|
||||
final int centerX = v.getWidth() / 2;
|
||||
final int centerY = v.getHeight() / 2;
|
||||
v.getBackground().setHotspot(centerX, centerY);
|
||||
}
|
||||
v.setPressed(true);
|
||||
v.setPressed(false);
|
||||
mShouldBlink = false;
|
||||
});
|
||||
}
|
||||
mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(view, info);
|
||||
// Update the range info with the correct type
|
||||
AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo();
|
||||
if (rangeInfo != null) {
|
||||
info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain(
|
||||
mAccessibilityRangeInfoType, rangeInfo.getMin(),
|
||||
rangeInfo.getMax(), rangeInfo.getCurrent()));
|
||||
}
|
||||
if (mOverrideSeekBarStateDescription != null) {
|
||||
info.setStateDescription(mOverrideSeekBarStateDescription);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
|
||||
setProgress(restoreValue ? getPersistedInt(mProgress)
|
||||
: (Integer) defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object onGetDefaultValue(TypedArray a, int index) {
|
||||
return a.getInt(index, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() != KeyEvent.ACTION_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SeekBar seekBar = (SeekBar) v.findViewById(com.android.internal.R.id.seekbar);
|
||||
if (seekBar == null) {
|
||||
return false;
|
||||
}
|
||||
return seekBar.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
if (max != mMax) {
|
||||
mMax = max;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMin(int min) {
|
||||
if (min != mMin) {
|
||||
mMin = min;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return mMax;
|
||||
}
|
||||
|
||||
public int getMin() {
|
||||
return mMin;
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
setProgress(progress, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the progress point to draw a single tick mark representing a default value.
|
||||
*/
|
||||
public void setDefaultProgress(int defaultProgress) {
|
||||
if (mDefaultProgress != defaultProgress) {
|
||||
mDefaultProgress = defaultProgress;
|
||||
if (mSeekBar instanceof DefaultIndicatorSeekBar) {
|
||||
((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb
|
||||
* is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb
|
||||
* is dropped.
|
||||
*/
|
||||
public void setContinuousUpdates(boolean continuousUpdates) {
|
||||
mContinuousUpdates = continuousUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the haptic feedback mode. HAPTIC_FEEDBACK_MODE_ON_TICKS means to perform haptic feedback
|
||||
* as the SeekBar's progress is updated; HAPTIC_FEEDBACK_MODE_ON_ENDS means to perform haptic
|
||||
* feedback as the SeekBar's progress value is equal to the min/max value.
|
||||
*
|
||||
* @param hapticFeedbackMode the haptic feedback mode.
|
||||
*/
|
||||
public void setHapticFeedbackMode(int hapticFeedbackMode) {
|
||||
mHapticFeedbackMode = hapticFeedbackMode;
|
||||
}
|
||||
|
||||
private void setProgress(int progress, boolean notifyChanged) {
|
||||
if (progress > mMax) {
|
||||
progress = mMax;
|
||||
}
|
||||
if (progress < mMin) {
|
||||
progress = mMin;
|
||||
}
|
||||
if (progress != mProgress) {
|
||||
mProgress = progress;
|
||||
persistInt(progress);
|
||||
if (notifyChanged) {
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return mProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the seekBar's progress value if callChangeListener
|
||||
* returns true, otherwise set the seekBar's progress to the stored value
|
||||
*/
|
||||
void syncProgress(SeekBar seekBar) {
|
||||
int progress = seekBar.getProgress();
|
||||
if (progress != mProgress) {
|
||||
if (callChangeListener(progress)) {
|
||||
setProgress(progress, false);
|
||||
switch (mHapticFeedbackMode) {
|
||||
case HAPTIC_FEEDBACK_MODE_ON_TICKS:
|
||||
seekBar.performHapticFeedback(CLOCK_TICK);
|
||||
break;
|
||||
case HAPTIC_FEEDBACK_MODE_ON_ENDS:
|
||||
if (progress == mMax || progress == mMin) {
|
||||
seekBar.performHapticFeedback(CLOCK_TICK);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
seekBar.setProgress(mProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser && (mContinuousUpdates || !mTrackingTouch)) {
|
||||
syncProgress(seekBar);
|
||||
}
|
||||
if (mOnSeekBarChangeListener != null) {
|
||||
mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
mTrackingTouch = true;
|
||||
mJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
|
||||
.withView(CUJ_SETTINGS_SLIDER, seekBar)
|
||||
.setTag(getKey()));
|
||||
if (mOnSeekBarChangeListener != null) {
|
||||
mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
mTrackingTouch = false;
|
||||
if (seekBar.getProgress() != mProgress) {
|
||||
syncProgress(seekBar);
|
||||
}
|
||||
if (mOnSeekBarChangeListener != null) {
|
||||
mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
|
||||
}
|
||||
mJankMonitor.end(CUJ_SETTINGS_SLIDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the type of range this seek bar represents.
|
||||
*
|
||||
* @param rangeInfoType The type of range to be shared with accessibility
|
||||
*
|
||||
* @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo
|
||||
*/
|
||||
public void setAccessibilityRangeInfoType(int rangeInfoType) {
|
||||
mAccessibilityRangeInfoType = rangeInfoType;
|
||||
}
|
||||
|
||||
public void setSeekBarContentDescription(CharSequence contentDescription) {
|
||||
mSeekBarContentDescription = contentDescription;
|
||||
if (mSeekBar != null) {
|
||||
mSeekBar.setContentDescription(contentDescription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the state description for this seek bar represents.
|
||||
*
|
||||
* @param stateDescription the state description of seek bar
|
||||
*/
|
||||
public void setSeekBarStateDescription(CharSequence stateDescription) {
|
||||
mSeekBarStateDescription = stateDescription;
|
||||
if (mSeekBar != null) {
|
||||
mSeekBar.setStateDescription(stateDescription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the state description of {@link SeekBar} with given content.
|
||||
*/
|
||||
public void overrideSeekBarStateDescription(CharSequence stateDescription) {
|
||||
mOverrideSeekBarStateDescription = stateDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
/*
|
||||
* Suppose a client uses this preference type without persisting. We
|
||||
* must save the instance state so it is able to, for example, survive
|
||||
* orientation changes.
|
||||
*/
|
||||
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
if (isPersistent()) {
|
||||
// No need to save instance state since it's persistent
|
||||
return superState;
|
||||
}
|
||||
|
||||
// Save the instance state
|
||||
final SavedState myState = new SavedState(superState);
|
||||
myState.progress = mProgress;
|
||||
myState.max = mMax;
|
||||
myState.min = mMin;
|
||||
return myState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (!state.getClass().equals(SavedState.class)) {
|
||||
// Didn't save state for us in onSaveInstanceState
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the instance state
|
||||
SavedState myState = (SavedState) state;
|
||||
super.onRestoreInstanceState(myState.getSuperState());
|
||||
mProgress = myState.progress;
|
||||
mMax = myState.max;
|
||||
mMin = myState.min;
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onHover(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
v.setHovered(true);
|
||||
break;
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
v.setHovered(false);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* SavedState, a subclass of {@link BaseSavedState}, will store the state
|
||||
* of MyPreference, a subclass of Preference.
|
||||
* <p>
|
||||
* It is important to always call through to super methods.
|
||||
*/
|
||||
private static class SavedState extends BaseSavedState {
|
||||
int progress;
|
||||
int max;
|
||||
int min;
|
||||
|
||||
public SavedState(Parcel source) {
|
||||
super(source);
|
||||
|
||||
// Restore the click counter
|
||||
progress = source.readInt();
|
||||
max = source.readInt();
|
||||
min = source.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
|
||||
// Save the click counter
|
||||
dest.writeInt(progress);
|
||||
dest.writeInt(max);
|
||||
dest.writeInt(min);
|
||||
}
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final Parcelable.Creator<SavedState> CREATOR =
|
||||
new Parcelable.Creator<SavedState>() {
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.settings.widget;
|
||||
|
||||
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||
import com.android.settingslib.widget.MainSwitchBar;
|
||||
|
||||
/**
|
||||
* A {@link MainSwitchBar} with a customized Switch and provides the metrics feature.
|
||||
*/
|
||||
public class SettingsMainSwitchBar extends MainSwitchBar {
|
||||
|
||||
/**
|
||||
* Called before the checked state of the Switch has changed.
|
||||
*/
|
||||
public interface OnBeforeCheckedChangeListener {
|
||||
|
||||
/**
|
||||
* @param isChecked The new checked state of switchView.
|
||||
*/
|
||||
boolean onBeforeCheckedChanged(boolean isChecked);
|
||||
}
|
||||
|
||||
private EnforcedAdmin mEnforcedAdmin;
|
||||
private boolean mDisabledByAdmin;
|
||||
|
||||
private final MetricsFeatureProvider mMetricsFeatureProvider;
|
||||
private OnBeforeCheckedChangeListener mOnBeforeListener;
|
||||
|
||||
private int mMetricsCategory;
|
||||
|
||||
public SettingsMainSwitchBar(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SettingsMainSwitchBar(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SettingsMainSwitchBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public SettingsMainSwitchBar(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
|
||||
|
||||
addOnSwitchChangeListener((switchView, isChecked) -> logMetrics(isChecked));
|
||||
}
|
||||
|
||||
/**
|
||||
* If admin is not null, disables the text and switch but keeps the view clickable (unless the
|
||||
* switch is disabled for other reasons). Otherwise, calls setEnabled.
|
||||
*/
|
||||
public void setDisabledByAdmin(EnforcedAdmin admin) {
|
||||
mEnforcedAdmin = admin;
|
||||
if (admin != null) {
|
||||
super.setEnabled(true);
|
||||
mDisabledByAdmin = true;
|
||||
mTextView.setEnabled(false);
|
||||
mSwitch.setEnabled(false);
|
||||
} else {
|
||||
mDisabledByAdmin = false;
|
||||
mSwitch.setVisibility(View.VISIBLE);
|
||||
setEnabled(isEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
if (enabled && mDisabledByAdmin) {
|
||||
setDisabledByAdmin(null);
|
||||
return;
|
||||
}
|
||||
super.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the restricted icon clicked.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public boolean performClick() {
|
||||
if (mDisabledByAdmin) {
|
||||
performRestrictedClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
return mSwitch.performClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
if (mOnBeforeListener != null
|
||||
&& mOnBeforeListener.onBeforeCheckedChanged(checked)) {
|
||||
return;
|
||||
}
|
||||
super.setChecked(checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of switch but doesn't notify the mOnBeforeListener.
|
||||
*/
|
||||
public void setCheckedInternal(boolean checked) {
|
||||
super.setChecked(checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the OnBeforeCheckedChangeListener.
|
||||
*/
|
||||
public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) {
|
||||
mOnBeforeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metrics tag.
|
||||
*/
|
||||
public void setMetricsCategory(int category) {
|
||||
mMetricsCategory = category;
|
||||
}
|
||||
|
||||
private void logMetrics(boolean isChecked) {
|
||||
mMetricsFeatureProvider.changed(mMetricsCategory, "switch_bar", isChecked ? 1 : 0);
|
||||
}
|
||||
|
||||
private void performRestrictedClick() {
|
||||
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), mEnforcedAdmin);
|
||||
mMetricsFeatureProvider.clicked(mMetricsCategory, "switch_bar|restricted");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.preference.TwoStatePreference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.widget.SettingsMainSwitchBar.OnBeforeCheckedChangeListener;
|
||||
import com.android.settingslib.RestrictedPreferenceHelper;
|
||||
import com.android.settingslib.core.instrumentation.SettingsJankMonitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SettingsMainSwitchPreference is a Preference with a customized Switch.
|
||||
* This component is used as the main switch of the page
|
||||
* to enable or disable the preferences on the page.
|
||||
*/
|
||||
public class SettingsMainSwitchPreference extends TwoStatePreference implements
|
||||
OnCheckedChangeListener {
|
||||
|
||||
private final List<OnBeforeCheckedChangeListener> mBeforeCheckedChangeListeners =
|
||||
new ArrayList<>();
|
||||
private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
|
||||
|
||||
private SettingsMainSwitchBar mMainSwitchBar;
|
||||
private EnforcedAdmin mEnforcedAdmin;
|
||||
private RestrictedPreferenceHelper mRestrictedHelper;
|
||||
|
||||
public SettingsMainSwitchPreference(Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public SettingsMainSwitchPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public SettingsMainSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public SettingsMainSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
holder.setDividerAllowedAbove(false);
|
||||
holder.setDividerAllowedBelow(false);
|
||||
|
||||
if (mEnforcedAdmin == null && mRestrictedHelper != null) {
|
||||
mEnforcedAdmin = mRestrictedHelper.checkRestrictionEnforced();
|
||||
}
|
||||
mMainSwitchBar = (SettingsMainSwitchBar) holder.findViewById(R.id.main_switch_bar);
|
||||
initMainSwitchBar();
|
||||
if (isVisible()) {
|
||||
mMainSwitchBar.show();
|
||||
if (mMainSwitchBar.isChecked() != isChecked()) {
|
||||
setChecked(isChecked());
|
||||
}
|
||||
registerListenerToSwitchBar();
|
||||
} else {
|
||||
mMainSwitchBar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
setLayoutResource(R.layout.preference_widget_main_switch);
|
||||
mSwitchChangeListeners.add(this);
|
||||
|
||||
if (attrs != null) {
|
||||
mRestrictedHelper = new RestrictedPreferenceHelper(context, this, attrs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
super.setChecked(checked);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SettingsMainSwitchBar
|
||||
*/
|
||||
public final SettingsMainSwitchBar getSwitchBar() {
|
||||
return mMainSwitchBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(CharSequence title) {
|
||||
super.setTitle(title);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
super.setChecked(isChecked);
|
||||
SettingsJankMonitor.detectToggleJank(getKey(), buttonView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the MainSwitchBar
|
||||
*/
|
||||
public void show() {
|
||||
setVisible(true);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the MainSwitchBar
|
||||
*/
|
||||
public void hide() {
|
||||
setVisible(false);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the MainSwitchBar is visible.
|
||||
*/
|
||||
public boolean isShowing() {
|
||||
if (mMainSwitchBar != null) {
|
||||
return mMainSwitchBar.isShowing();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of switch but doesn't notify the mOnBeforeListener.
|
||||
*/
|
||||
public void setCheckedInternal(boolean checked) {
|
||||
super.setChecked(checked);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setCheckedInternal(checked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the text and switch.
|
||||
*/
|
||||
public void setSwitchBarEnabled(boolean enabled) {
|
||||
setEnabled(enabled);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the OnBeforeCheckedChangeListener.
|
||||
*/
|
||||
public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) {
|
||||
if (!mBeforeCheckedChangeListeners.contains(listener)) {
|
||||
mBeforeCheckedChangeListeners.add(listener);
|
||||
}
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setOnBeforeCheckedChangeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for switch changes
|
||||
*/
|
||||
public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
|
||||
if (!mSwitchChangeListeners.contains(listener)) {
|
||||
mSwitchChangeListeners.add(listener);
|
||||
}
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.addOnSwitchChangeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener for switch changes
|
||||
*/
|
||||
public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
|
||||
mSwitchChangeListeners.remove(listener);
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.removeOnSwitchChangeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If admin is not null, disables the text and switch but keeps the view clickable.
|
||||
* Otherwise, calls setEnabled which will enables the entire view including
|
||||
* the text and switch.
|
||||
*/
|
||||
public void setDisabledByAdmin(EnforcedAdmin admin) {
|
||||
mEnforcedAdmin = admin;
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setDisabledByAdmin(mEnforcedAdmin);
|
||||
}
|
||||
}
|
||||
|
||||
private void initMainSwitchBar() {
|
||||
if (mMainSwitchBar != null) {
|
||||
mMainSwitchBar.setTitle(getTitle());
|
||||
mMainSwitchBar.setDisabledByAdmin(mEnforcedAdmin);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerListenerToSwitchBar() {
|
||||
for (OnBeforeCheckedChangeListener listener : mBeforeCheckedChangeListeners) {
|
||||
mMainSwitchBar.setOnBeforeCheckedChangeListener(listener);
|
||||
}
|
||||
for (OnCheckedChangeListener listener : mSwitchChangeListeners) {
|
||||
mMainSwitchBar.addOnSwitchChangeListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.core.TogglePreferenceController;
|
||||
import com.android.settingslib.widget.MainSwitchPreference;
|
||||
|
||||
/**
|
||||
* Preference controller for MainSwitchPreference.
|
||||
*/
|
||||
public abstract class SettingsMainSwitchPreferenceController extends
|
||||
TogglePreferenceController implements OnCheckedChangeListener {
|
||||
|
||||
protected MainSwitchPreference mSwitchPreference;
|
||||
|
||||
public SettingsMainSwitchPreferenceController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen) {
|
||||
super.displayPreference(screen);
|
||||
final Preference pref = screen.findPreference(getPreferenceKey());
|
||||
if (pref != null && pref instanceof MainSwitchPreference) {
|
||||
mSwitchPreference = (MainSwitchPreference) pref;
|
||||
mSwitchPreference.addOnSwitchChangeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
mSwitchPreference.setChecked(isChecked);
|
||||
setChecked(isChecked);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* A preference with single target and a gear icon on the side.
|
||||
*/
|
||||
public class SingleTargetGearPreference extends Preference {
|
||||
public SingleTargetGearPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public SingleTargetGearPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public SingleTargetGearPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public SingleTargetGearPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setLayoutResource(R.layout.preference_single_target);
|
||||
setWidgetLayoutResource(R.layout.preference_widget_gear_optional_background);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final View divider = holder.findViewById(
|
||||
com.android.settingslib.widget.preference.twotarget.R.id.two_target_divider);
|
||||
if (divider != null) {
|
||||
divider.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
187
Settings/src/com/android/settings/widget/SlidingTabLayout.java
Normal file
187
Settings/src/com/android/settings/widget/SlidingTabLayout.java
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* To be used with ViewPager to provide a tab indicator component which give constant feedback as
|
||||
* to the user's scroll progress.
|
||||
*/
|
||||
public final class SlidingTabLayout extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
private final LinearLayout mTitleView;
|
||||
private final View mIndicatorView;
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
|
||||
private RtlCompatibleViewPager mViewPager;
|
||||
private int mSelectedPosition;
|
||||
private float mSelectionOffset;
|
||||
|
||||
public SlidingTabLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mLayoutInflater = LayoutInflater.from(context);
|
||||
mTitleView = new LinearLayout(context);
|
||||
mTitleView.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
mIndicatorView = mLayoutInflater.inflate(R.layout.sliding_tab_indicator_view, this, false);
|
||||
|
||||
addView(mTitleView, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||
addView(mIndicatorView, mIndicatorView.getLayoutParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the associated view pager. Note that the assumption here is that the pager content
|
||||
* (number of tabs and tab titles) does not change after this call has been made.
|
||||
*/
|
||||
public void setViewPager(RtlCompatibleViewPager viewPager) {
|
||||
mTitleView.removeAllViews();
|
||||
|
||||
mViewPager = viewPager;
|
||||
if (viewPager != null) {
|
||||
viewPager.addOnPageChangeListener(new InternalViewPagerListener());
|
||||
populateTabStrip();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
final int titleCount = mTitleView.getChildCount();
|
||||
if (titleCount > 0) {
|
||||
final int width = MeasureSpec.makeMeasureSpec(
|
||||
mTitleView.getMeasuredWidth() / titleCount, MeasureSpec.EXACTLY);
|
||||
final int height = MeasureSpec.makeMeasureSpec(
|
||||
mIndicatorView.getMeasuredHeight(), MeasureSpec.EXACTLY);
|
||||
mIndicatorView.measure(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
if (mTitleView.getChildCount() > 0) {
|
||||
final int indicatorBottom = getMeasuredHeight();
|
||||
final int indicatorHeight = mIndicatorView.getMeasuredHeight();
|
||||
final int indicatorWidth = mIndicatorView.getMeasuredWidth();
|
||||
final int totalWidth = getMeasuredWidth();
|
||||
final int leftPadding = getPaddingLeft();
|
||||
final int rightPadding = getPaddingRight();
|
||||
|
||||
mTitleView.layout(leftPadding, 0, mTitleView.getMeasuredWidth() + rightPadding,
|
||||
mTitleView.getMeasuredHeight());
|
||||
// IndicatorView should start on the right when RTL mode is enabled
|
||||
if (isRtlMode()) {
|
||||
mIndicatorView.layout(totalWidth - indicatorWidth,
|
||||
indicatorBottom - indicatorHeight, totalWidth,
|
||||
indicatorBottom);
|
||||
} else {
|
||||
mIndicatorView.layout(0, indicatorBottom - indicatorHeight,
|
||||
indicatorWidth, indicatorBottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final int titleCount = mTitleView.getChildCount();
|
||||
for (int i = 0; i < titleCount; i++) {
|
||||
if (v == mTitleView.getChildAt(i)) {
|
||||
mViewPager.setCurrentItem(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onViewPagerPageChanged(int position, float positionOffset) {
|
||||
mSelectedPosition = position;
|
||||
mSelectionOffset = positionOffset;
|
||||
// Translation should be reversed in RTL mode
|
||||
final int leftIndicator = isRtlMode() ? -getIndicatorLeft() : getIndicatorLeft();
|
||||
mIndicatorView.setTranslationX(leftIndicator);
|
||||
}
|
||||
|
||||
private void populateTabStrip() {
|
||||
final PagerAdapter adapter = mViewPager.getAdapter();
|
||||
|
||||
for (int i = 0; i < adapter.getCount(); i++) {
|
||||
final TextView tabTitleView = (TextView) mLayoutInflater.inflate(
|
||||
R.layout.sliding_tab_title_view, mTitleView, false);
|
||||
|
||||
tabTitleView.setText(adapter.getPageTitle(i));
|
||||
tabTitleView.setOnClickListener(this);
|
||||
|
||||
mTitleView.addView(tabTitleView);
|
||||
tabTitleView.setSelected(i == mViewPager.getCurrentItem());
|
||||
}
|
||||
}
|
||||
|
||||
private int getIndicatorLeft() {
|
||||
View selectedTitle = mTitleView.getChildAt(mSelectedPosition);
|
||||
int left = selectedTitle.getLeft();
|
||||
if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) {
|
||||
View nextTitle = mTitleView.getChildAt(mSelectedPosition + 1);
|
||||
left = (int) (mSelectionOffset * nextTitle.getLeft()
|
||||
+ (1.0f - mSelectionOffset) * left);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private boolean isRtlMode() {
|
||||
return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
|
||||
private final class InternalViewPagerListener implements
|
||||
RtlCompatibleViewPager.OnPageChangeListener {
|
||||
private int mScrollState;
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
final int titleCount = mTitleView.getChildCount();
|
||||
if ((titleCount == 0) || (position < 0) || (position >= titleCount)) {
|
||||
return;
|
||||
}
|
||||
onViewPagerPageChanged(position, positionOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
mScrollState = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
position = mViewPager.getRtlAwareIndex(position);
|
||||
if (mScrollState == RtlCompatibleViewPager.SCROLL_STATE_IDLE) {
|
||||
onViewPagerPageChanged(position, 0f);
|
||||
}
|
||||
final int titleCount = mTitleView.getChildCount();
|
||||
for (int i = 0; i < titleCount; i++) {
|
||||
mTitleView.getChildAt(i).setSelected(position == i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
Settings/src/com/android/settings/widget/SummaryUpdater.java
Normal file
86
Settings/src/com/android/settings/widget/SummaryUpdater.java
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/**
|
||||
* Helper class that listens to settings changes and notifies client when there is update in
|
||||
* corresponding summary info.
|
||||
*/
|
||||
public abstract class SummaryUpdater {
|
||||
|
||||
protected final Context mContext;
|
||||
|
||||
private final OnSummaryChangeListener mListener;
|
||||
private String mSummary;
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when the summary has been changed.
|
||||
*/
|
||||
public interface OnSummaryChangeListener {
|
||||
/**
|
||||
* Called when summary has changed.
|
||||
*
|
||||
* @param summary The new summary .
|
||||
*/
|
||||
void onSummaryChanged(String summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The Context the updater is running in, through which it can register broadcast
|
||||
* receiver etc.
|
||||
* @param listener The listener that would like to receive summary change notification.
|
||||
*
|
||||
*/
|
||||
public SummaryUpdater(Context context, OnSummaryChangeListener listener) {
|
||||
mContext = context;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the listener when there is update in summary
|
||||
*/
|
||||
protected void notifyChangeIfNeeded() {
|
||||
String summary = getSummary();
|
||||
if (!TextUtils.equals(mSummary, summary)) {
|
||||
mSummary = summary;
|
||||
if (mListener != null) {
|
||||
mListener.onSummaryChanged(summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts/stops receiving updates on the summary.
|
||||
*
|
||||
* @param register true if we want to receive updates, false otherwise
|
||||
*/
|
||||
public abstract void register(boolean register);
|
||||
|
||||
/**
|
||||
* Gets the summary. Subclass should checks latest conditions and update the summary
|
||||
* accordingly.
|
||||
*
|
||||
* @return the latest summary text
|
||||
*/
|
||||
protected abstract String getSummary();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
|
||||
/*
|
||||
* A controller class for general switch widget handling. We have different containers that provide
|
||||
* different forms of switch layout. Provide a centralized control for updating the switch widget.
|
||||
*/
|
||||
public abstract class SwitchWidgetController {
|
||||
|
||||
protected OnSwitchChangeListener mListener;
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when the switch has been toggled.
|
||||
*/
|
||||
public interface OnSwitchChangeListener {
|
||||
/**
|
||||
* Called when the checked state of the Switch has changed.
|
||||
*
|
||||
* @param isChecked The new checked state of switchView.
|
||||
*
|
||||
* @return true to update the state of the switch with the new value.
|
||||
*/
|
||||
boolean onSwitchToggled(boolean isChecked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any view setup.
|
||||
*/
|
||||
public void setupView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any view teardown.
|
||||
*/
|
||||
public void teardownView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback to be invoked when the switch is toggled by the user (but before the
|
||||
* internal state has been updated).
|
||||
*
|
||||
* @param listener the callback to be invoked
|
||||
*/
|
||||
public void setListener(OnSwitchChangeListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the preference title associated with the switch.
|
||||
*
|
||||
* @param title the preference title
|
||||
*/
|
||||
public abstract void setTitle(String title);
|
||||
|
||||
/**
|
||||
* Start listening to switch toggling.
|
||||
*/
|
||||
public abstract void startListening();
|
||||
|
||||
/**
|
||||
* Stop listening to switch toggling.
|
||||
*/
|
||||
public abstract void stopListening();
|
||||
|
||||
/**
|
||||
* Set the checked state for the switch.
|
||||
*
|
||||
* @param checked whether the switch should be checked or not.
|
||||
*/
|
||||
public abstract void setChecked(boolean checked);
|
||||
|
||||
/**
|
||||
* Get the checked state for the switch.
|
||||
*
|
||||
* @return true if the switch is currently checked, false otherwise.
|
||||
*/
|
||||
public abstract boolean isChecked();
|
||||
|
||||
/**
|
||||
* Set the enabled state for the switch.
|
||||
*
|
||||
* @param enabled whether the switch should be enabled or not.
|
||||
*/
|
||||
public abstract void setEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Disable the switch based on the enforce admin.
|
||||
*
|
||||
* @param admin Details of the admin who enforced the restriction. If it
|
||||
* is {@code null}, then this preference will be enabled. Otherwise, it will be disabled.
|
||||
*/
|
||||
public abstract void setDisabledByAdmin(EnforcedAdmin admin);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/** A preference with tick icon. */
|
||||
public class TickButtonPreference extends Preference {
|
||||
private ImageView mCheckIcon;
|
||||
private boolean mIsSelected = false;
|
||||
|
||||
public TickButtonPreference(Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public TickButtonPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
setWidgetLayoutResource(R.layout.preference_check_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
mCheckIcon = (ImageView) holder.findViewById(R.id.check_icon);
|
||||
setSelected(mIsSelected);
|
||||
}
|
||||
|
||||
/** Set icon state.*/
|
||||
public void setSelected(boolean isSelected) {
|
||||
if (mCheckIcon != null) {
|
||||
mCheckIcon.setVisibility(isSelected ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
mIsSelected = isSelected;
|
||||
}
|
||||
|
||||
/** Return state of presenting icon. */
|
||||
public boolean isSelected() {
|
||||
return mIsSelected;
|
||||
}
|
||||
}
|
||||
102
Settings/src/com/android/settings/widget/TintDrawable.java
Normal file
102
Settings/src/com/android/settings/widget/TintDrawable.java
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.settings.widget;
|
||||
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.DrawableWrapper;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A Drawable that tints a contained Drawable, overriding the existing tint specified in the
|
||||
* underlying drawable. This class should only be used in XML.
|
||||
*
|
||||
* @attr ref android.R.styleable#DrawableWrapper_drawable
|
||||
* @attr ref R.styleable#TintDrawable_tint
|
||||
*/
|
||||
public class TintDrawable extends DrawableWrapper {
|
||||
private ColorStateList mTint;
|
||||
private int[] mThemeAttrs;
|
||||
|
||||
/** No-arg constructor used by drawable inflation. */
|
||||
public TintDrawable() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
|
||||
@NonNull AttributeSet attrs, @Nullable Theme theme)
|
||||
throws XmlPullParserException, IOException {
|
||||
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.TintDrawable);
|
||||
|
||||
super.inflate(r, parser, attrs, theme);
|
||||
|
||||
mThemeAttrs = a.extractThemeAttrs();
|
||||
updateStateFromTypedArray(a);
|
||||
a.recycle();
|
||||
|
||||
applyTint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTheme(Theme t) {
|
||||
super.applyTheme(t);
|
||||
|
||||
if (mThemeAttrs != null) {
|
||||
final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.TintDrawable);
|
||||
updateStateFromTypedArray(a);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
// Ensure tint is reapplied after applying the theme to ensure this drawables'
|
||||
// tint overrides the underlying drawables' tint.
|
||||
applyTint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canApplyTheme() {
|
||||
return (mThemeAttrs != null && mThemeAttrs.length > 0) || super.canApplyTheme();
|
||||
}
|
||||
|
||||
private void updateStateFromTypedArray(@NonNull TypedArray a) {
|
||||
if (a.hasValue(R.styleable.TintDrawable_android_drawable)) {
|
||||
setDrawable(a.getDrawable(R.styleable.TintDrawable_android_drawable));
|
||||
}
|
||||
if (a.hasValue(R.styleable.TintDrawable_android_tint)) {
|
||||
mTint = a.getColorStateList(R.styleable.TintDrawable_android_tint);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTint() {
|
||||
if (getDrawable() != null && mTint != null) {
|
||||
getDrawable().mutate().setTintList(mTint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
/**
|
||||
* Preference that presents a button with two states(On vs Off)
|
||||
*/
|
||||
public class TwoStateButtonPreference extends LayoutPreference implements
|
||||
View.OnClickListener {
|
||||
|
||||
private boolean mIsChecked;
|
||||
private final Button mButtonOn;
|
||||
private final Button mButtonOff;
|
||||
|
||||
public TwoStateButtonPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs, TypedArrayUtils.getAttr(
|
||||
context, R.attr.twoStateButtonPreferenceStyle, android.R.attr.preferenceStyle));
|
||||
|
||||
if (attrs == null) {
|
||||
mButtonOn = null;
|
||||
mButtonOff = null;
|
||||
} else {
|
||||
final TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.TwoStateButtonPreference);
|
||||
final int textOnId = styledAttrs.getResourceId(
|
||||
R.styleable.TwoStateButtonPreference_textOn,
|
||||
R.string.summary_placeholder);
|
||||
final int textOffId = styledAttrs.getResourceId(
|
||||
R.styleable.TwoStateButtonPreference_textOff,
|
||||
R.string.summary_placeholder);
|
||||
styledAttrs.recycle();
|
||||
|
||||
mButtonOn = findViewById(R.id.state_on_button);
|
||||
mButtonOn.setText(textOnId);
|
||||
mButtonOn.setOnClickListener(this);
|
||||
mButtonOff = findViewById(R.id.state_off_button);
|
||||
mButtonOff.setText(textOffId);
|
||||
mButtonOff.setOnClickListener(this);
|
||||
setChecked(isChecked());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final boolean stateOn = v.getId() == R.id.state_on_button;
|
||||
setChecked(stateOn);
|
||||
callChangeListener(stateOn);
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked) {
|
||||
// Update state
|
||||
mIsChecked = checked;
|
||||
// And update UI
|
||||
if (checked) {
|
||||
mButtonOn.setVisibility(View.GONE);
|
||||
mButtonOff.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mButtonOn.setVisibility(View.VISIBLE);
|
||||
mButtonOff.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mIsChecked;
|
||||
}
|
||||
|
||||
public void setButtonEnabled(boolean enabled) {
|
||||
mButtonOn.setEnabled(enabled);
|
||||
mButtonOff.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Button getStateOnButton() {
|
||||
return mButtonOn;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Button getStateOffButton() {
|
||||
return mButtonOff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 com.android.settings.widget;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog.Builder;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import com.android.settingslib.core.instrumentation.Instrumentable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* {@link PreferenceDialogFragmentCompat} that updates the available options
|
||||
* when {@code onListPreferenceUpdated} is called."
|
||||
*/
|
||||
public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragmentCompat implements
|
||||
Instrumentable {
|
||||
|
||||
private static final String SAVE_STATE_INDEX = "UpdatableListPreferenceDialogFragment.index";
|
||||
private static final String SAVE_STATE_ENTRIES =
|
||||
"UpdatableListPreferenceDialogFragment.entries";
|
||||
private static final String SAVE_STATE_ENTRY_VALUES =
|
||||
"UpdatableListPreferenceDialogFragment.entryValues";
|
||||
private static final String METRICS_CATEGORY_KEY = "metrics_category_key";
|
||||
private ArrayAdapter mAdapter;
|
||||
private int mClickedDialogEntryIndex;
|
||||
private ArrayList<CharSequence> mEntries;
|
||||
private CharSequence[] mEntryValues;
|
||||
private int mMetricsCategory = METRICS_CATEGORY_UNKNOWN;
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link UpdatableListPreferenceDialogFragment}.
|
||||
*/
|
||||
public static UpdatableListPreferenceDialogFragment newInstance(
|
||||
String key, int metricsCategory) {
|
||||
UpdatableListPreferenceDialogFragment fragment =
|
||||
new UpdatableListPreferenceDialogFragment();
|
||||
Bundle args = new Bundle(1);
|
||||
args.putString(ARG_KEY, key);
|
||||
args.putInt(METRICS_CATEGORY_KEY, metricsCategory);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Bundle bundle = getArguments();
|
||||
mMetricsCategory =
|
||||
bundle.getInt(METRICS_CATEGORY_KEY, METRICS_CATEGORY_UNKNOWN);
|
||||
if (savedInstanceState == null) {
|
||||
mEntries = new ArrayList<>();
|
||||
setPreferenceData(getListPreference());
|
||||
} else {
|
||||
mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0);
|
||||
mEntries = savedInstanceState.getCharSequenceArrayList(SAVE_STATE_ENTRIES);
|
||||
mEntryValues =
|
||||
savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex);
|
||||
outState.putCharSequenceArrayList(SAVE_STATE_ENTRIES, mEntries);
|
||||
outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult && mClickedDialogEntryIndex >= 0) {
|
||||
final ListPreference preference = getListPreference();
|
||||
final String value = mEntryValues[mClickedDialogEntryIndex].toString();
|
||||
if (preference.callChangeListener(value)) {
|
||||
preference.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setAdapter(ArrayAdapter adapter) {
|
||||
mAdapter = adapter;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setEntries(ArrayList<CharSequence> entries) {
|
||||
mEntries = entries;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ArrayAdapter getAdapter() {
|
||||
return mAdapter;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setMetricsCategory(Bundle bundle) {
|
||||
mMetricsCategory =
|
||||
bundle.getInt(METRICS_CATEGORY_KEY, METRICS_CATEGORY_UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
final TypedArray a = getContext().obtainStyledAttributes(
|
||||
null,
|
||||
com.android.internal.R.styleable.AlertDialog,
|
||||
com.android.internal.R.attr.alertDialogStyle, 0);
|
||||
|
||||
mAdapter = new ArrayAdapter<>(
|
||||
getContext(),
|
||||
a.getResourceId(
|
||||
com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout,
|
||||
com.android.internal.R.layout.select_dialog_singlechoice),
|
||||
mEntries);
|
||||
|
||||
builder.setSingleChoiceItems(mAdapter, mClickedDialogEntryIndex,
|
||||
(dialog, which) -> {
|
||||
mClickedDialogEntryIndex = which;
|
||||
onClick(dialog, -1);
|
||||
dialog.dismiss();
|
||||
});
|
||||
builder.setPositiveButton(null, null);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
return mMetricsCategory;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ListPreference getListPreference() {
|
||||
return (ListPreference) getPreference();
|
||||
}
|
||||
|
||||
private void setPreferenceData(ListPreference preference) {
|
||||
mEntries.clear();
|
||||
mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue());
|
||||
for (CharSequence entry : preference.getEntries()) {
|
||||
mEntries.add(entry);
|
||||
}
|
||||
mEntryValues = preference.getEntryValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update new data set for list preference.
|
||||
*/
|
||||
public void onListPreferenceUpdated(ListPreference preference) {
|
||||
if (mAdapter != null) {
|
||||
setPreferenceData(preference);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
331
Settings/src/com/android/settings/widget/UsageGraph.java
Normal file
331
Settings/src/com/android/settings/widget/UsageGraph.java
Normal file
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.CornerPathEffect;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Cap;
|
||||
import android.graphics.Paint.Join;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Shader.TileMode;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.SparseIntArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.fuelgauge.BatteryUtils;
|
||||
|
||||
public class UsageGraph extends View {
|
||||
|
||||
private static final int PATH_DELIM = -1;
|
||||
public static final String LOG_TAG = "UsageGraph";
|
||||
|
||||
private final Paint mLinePaint;
|
||||
private final Paint mFillPaint;
|
||||
private final Paint mDottedPaint;
|
||||
|
||||
private final Drawable mDivider;
|
||||
private final Drawable mTintedDivider;
|
||||
private final int mDividerSize;
|
||||
|
||||
private final Path mPath = new Path();
|
||||
|
||||
// Paths in coordinates they are passed in.
|
||||
private final SparseIntArray mPaths = new SparseIntArray();
|
||||
// Paths in local coordinates for drawing.
|
||||
private final SparseIntArray mLocalPaths = new SparseIntArray();
|
||||
|
||||
// Paths for projection in coordinates they are passed in.
|
||||
private final SparseIntArray mProjectedPaths = new SparseIntArray();
|
||||
// Paths for projection in local coordinates for drawing.
|
||||
private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
|
||||
|
||||
private final int mCornerRadius;
|
||||
private int mAccentColor;
|
||||
|
||||
private float mMaxX = 100;
|
||||
private float mMaxY = 100;
|
||||
|
||||
private float mMiddleDividerLoc = .5f;
|
||||
private int mMiddleDividerTint = -1;
|
||||
private int mTopDividerTint = -1;
|
||||
|
||||
public UsageGraph(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
final Resources resources = context.getResources();
|
||||
|
||||
mLinePaint = new Paint();
|
||||
mLinePaint.setStyle(Style.STROKE);
|
||||
mLinePaint.setStrokeCap(Cap.ROUND);
|
||||
mLinePaint.setStrokeJoin(Join.ROUND);
|
||||
mLinePaint.setAntiAlias(true);
|
||||
mCornerRadius = resources.getDimensionPixelSize(
|
||||
com.android.settingslib.R.dimen.usage_graph_line_corner_radius);
|
||||
mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
|
||||
mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(
|
||||
com.android.settingslib.R.dimen.usage_graph_line_width));
|
||||
|
||||
mFillPaint = new Paint(mLinePaint);
|
||||
mFillPaint.setStyle(Style.FILL);
|
||||
|
||||
mDottedPaint = new Paint(mLinePaint);
|
||||
mDottedPaint.setStyle(Style.STROKE);
|
||||
float dots = resources.getDimensionPixelSize(
|
||||
com.android.settingslib.R.dimen.usage_graph_dot_size);
|
||||
float interval = resources.getDimensionPixelSize(
|
||||
com.android.settingslib.R.dimen.usage_graph_dot_interval);
|
||||
mDottedPaint.setStrokeWidth(dots * 3);
|
||||
mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
|
||||
mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
|
||||
|
||||
TypedValue v = new TypedValue();
|
||||
context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
|
||||
mDivider = context.getDrawable(v.resourceId);
|
||||
mTintedDivider = context.getDrawable(v.resourceId);
|
||||
mDividerSize = resources.getDimensionPixelSize(
|
||||
com.android.settingslib.R.dimen.usage_graph_divider_size);
|
||||
}
|
||||
|
||||
void clearPaths() {
|
||||
mPaths.clear();
|
||||
mLocalPaths.clear();
|
||||
mProjectedPaths.clear();
|
||||
mLocalProjectedPaths.clear();
|
||||
}
|
||||
|
||||
void setMax(int maxX, int maxY) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
mMaxX = maxX;
|
||||
mMaxY = maxY;
|
||||
calculateLocalPaths();
|
||||
postInvalidate();
|
||||
BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
|
||||
}
|
||||
|
||||
void setDividerLoc(int height) {
|
||||
mMiddleDividerLoc = 1 - height / mMaxY;
|
||||
}
|
||||
|
||||
void setDividerColors(int middleColor, int topColor) {
|
||||
mMiddleDividerTint = middleColor;
|
||||
mTopDividerTint = topColor;
|
||||
}
|
||||
|
||||
public void addPath(SparseIntArray points) {
|
||||
addPathAndUpdate(points, mPaths, mLocalPaths);
|
||||
}
|
||||
|
||||
public void addProjectedPath(SparseIntArray points) {
|
||||
addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
|
||||
}
|
||||
|
||||
private void addPathAndUpdate(
|
||||
SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
for (int i = 0, size = points.size(); i < size; i++) {
|
||||
paths.put(points.keyAt(i), points.valueAt(i));
|
||||
}
|
||||
// Add a delimiting value immediately after the last point.
|
||||
paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
|
||||
calculateLocalPaths(paths, localPaths);
|
||||
postInvalidate();
|
||||
BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
|
||||
}
|
||||
|
||||
void setAccentColor(int color) {
|
||||
mAccentColor = color;
|
||||
mLinePaint.setColor(mAccentColor);
|
||||
updateGradient();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
updateGradient();
|
||||
calculateLocalPaths();
|
||||
BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
|
||||
}
|
||||
|
||||
private void calculateLocalPaths() {
|
||||
calculateLocalPaths(mPaths, mLocalPaths);
|
||||
calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
if (getWidth() == 0) {
|
||||
return;
|
||||
}
|
||||
localPaths.clear();
|
||||
// Store the local coordinates of the most recent point.
|
||||
int lx = 0;
|
||||
int ly = PATH_DELIM;
|
||||
boolean skippedLastPoint = false;
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
int x = paths.keyAt(i);
|
||||
int y = paths.valueAt(i);
|
||||
if (y == PATH_DELIM) {
|
||||
if (i == 1) {
|
||||
localPaths.put(getX(x+1) - 1, getY(0));
|
||||
continue;
|
||||
}
|
||||
if (i == paths.size() - 1 && skippedLastPoint) {
|
||||
// Add back skipped point to complete the path.
|
||||
localPaths.put(lx, ly);
|
||||
}
|
||||
skippedLastPoint = false;
|
||||
localPaths.put(lx + 1, PATH_DELIM);
|
||||
} else {
|
||||
lx = getX(x);
|
||||
ly = getY(y);
|
||||
// Skip this point if it is not far enough from the last one added.
|
||||
if (localPaths.size() > 0) {
|
||||
int lastX = localPaths.keyAt(localPaths.size() - 1);
|
||||
int lastY = localPaths.valueAt(localPaths.size() - 1);
|
||||
if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
|
||||
skippedLastPoint = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
skippedLastPoint = false;
|
||||
localPaths.put(lx, ly);
|
||||
}
|
||||
}
|
||||
BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
|
||||
}
|
||||
|
||||
private boolean hasDiff(int x1, int x2) {
|
||||
return Math.abs(x2 - x1) >= mCornerRadius;
|
||||
}
|
||||
|
||||
private int getX(float x) {
|
||||
return (int) (x / mMaxX * getWidth());
|
||||
}
|
||||
|
||||
private int getY(float y) {
|
||||
return (int) (getHeight() * (1 - (y / mMaxY)));
|
||||
}
|
||||
|
||||
private void updateGradient() {
|
||||
mFillPaint.setShader(
|
||||
new LinearGradient(
|
||||
0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
|
||||
}
|
||||
|
||||
private int getColor(int color, float alphaScale) {
|
||||
return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
// Draw lines across the top, middle, and bottom.
|
||||
if (mMiddleDividerLoc != 0) {
|
||||
drawDivider(0, canvas, mTopDividerTint);
|
||||
}
|
||||
drawDivider(
|
||||
(int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
|
||||
canvas,
|
||||
mMiddleDividerTint);
|
||||
drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
|
||||
|
||||
if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
|
||||
// Flip the canvas along the y-axis of the center of itself before drawing paths.
|
||||
canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0);
|
||||
}
|
||||
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
|
||||
drawFilledPath(canvas, mLocalPaths, mFillPaint);
|
||||
drawLinePath(canvas, mLocalPaths, mLinePaint);
|
||||
canvas.restore();
|
||||
BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
|
||||
}
|
||||
|
||||
private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
|
||||
if (localPaths.size() == 0) {
|
||||
return;
|
||||
}
|
||||
mPath.reset();
|
||||
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
|
||||
for (int i = 1; i < localPaths.size(); i++) {
|
||||
int x = localPaths.keyAt(i);
|
||||
int y = localPaths.valueAt(i);
|
||||
if (y == PATH_DELIM) {
|
||||
if (++i < localPaths.size()) {
|
||||
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
|
||||
}
|
||||
} else {
|
||||
mPath.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(mPath, paint);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
|
||||
if (localPaths.size() == 0) {
|
||||
return;
|
||||
}
|
||||
mPath.reset();
|
||||
float lastStartX = localPaths.keyAt(0);
|
||||
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
|
||||
for (int i = 1; i < localPaths.size(); i++) {
|
||||
int x = localPaths.keyAt(i);
|
||||
int y = localPaths.valueAt(i);
|
||||
if (y == PATH_DELIM) {
|
||||
mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
|
||||
mPath.lineTo(lastStartX, getHeight());
|
||||
mPath.close();
|
||||
if (++i < localPaths.size()) {
|
||||
lastStartX = localPaths.keyAt(i);
|
||||
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
|
||||
}
|
||||
} else {
|
||||
mPath.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(mPath, paint);
|
||||
}
|
||||
|
||||
private void drawDivider(int y, Canvas canvas, int tintColor) {
|
||||
Drawable d = mDivider;
|
||||
if (tintColor != -1) {
|
||||
mTintedDivider.setTint(tintColor);
|
||||
d = mTintedDivider;
|
||||
}
|
||||
d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
|
||||
d.draw(canvas);
|
||||
}
|
||||
}
|
||||
159
Settings/src/com/android/settings/widget/UsageView.java
Normal file
159
Settings/src/com/android/settings/widget/UsageView.java
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class UsageView extends FrameLayout {
|
||||
|
||||
private final UsageGraph mUsageGraph;
|
||||
private final TextView[] mLabels;
|
||||
private final TextView[] mBottomLabels;
|
||||
|
||||
public UsageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LayoutInflater.from(context).inflate(R.layout.usage_view, this);
|
||||
mUsageGraph = findViewById(R.id.usage_graph);
|
||||
mLabels = new TextView[] {
|
||||
findViewById(R.id.label_bottom),
|
||||
findViewById(R.id.label_middle),
|
||||
findViewById(R.id.label_top),
|
||||
};
|
||||
mBottomLabels = new TextView[] {
|
||||
findViewById(R.id.label_start),
|
||||
findViewById(R.id.label_end),
|
||||
};
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UsageView, 0, 0);
|
||||
if (a.hasValue(R.styleable.UsageView_sideLabels)) {
|
||||
setSideLabels(a.getTextArray(R.styleable.UsageView_sideLabels));
|
||||
}
|
||||
if (a.hasValue(R.styleable.UsageView_bottomLabels)) {
|
||||
setBottomLabels(a.getTextArray(R.styleable.UsageView_bottomLabels));
|
||||
}
|
||||
if (a.hasValue(R.styleable.UsageView_textColor)) {
|
||||
int color = a.getColor(R.styleable.UsageView_textColor, 0);
|
||||
for (TextView v : mLabels) {
|
||||
v.setTextColor(color);
|
||||
}
|
||||
for (TextView v : mBottomLabels) {
|
||||
v.setTextColor(color);
|
||||
}
|
||||
}
|
||||
if (a.hasValue(R.styleable.UsageView_android_gravity)) {
|
||||
int gravity = a.getInt(R.styleable.UsageView_android_gravity, 0);
|
||||
if (gravity == Gravity.END) {
|
||||
LinearLayout layout = findViewById(R.id.graph_label_group);
|
||||
LinearLayout labels = findViewById(R.id.label_group);
|
||||
// Swap the children order.
|
||||
layout.removeView(labels);
|
||||
layout.addView(labels);
|
||||
// Set gravity.
|
||||
labels.setGravity(Gravity.END);
|
||||
// Swap the bottom space order.
|
||||
LinearLayout bottomLabels = findViewById(R.id.bottom_label_group);
|
||||
View bottomSpace = bottomLabels.findViewById(R.id.bottom_label_space);
|
||||
bottomLabels.removeView(bottomSpace);
|
||||
bottomLabels.addView(bottomSpace);
|
||||
} else if (gravity != Gravity.START) {
|
||||
throw new IllegalArgumentException("Unsupported gravity " + gravity);
|
||||
}
|
||||
}
|
||||
mUsageGraph.setAccentColor(a.getColor(R.styleable.UsageView_android_colorAccent, 0));
|
||||
a.recycle();
|
||||
|
||||
// Locale Persian & Urdu are RTL languages but request LTR graph direction layout.
|
||||
final String defaultLanguageCode = Locale.getDefault().getLanguage();
|
||||
if (TextUtils.equals(defaultLanguageCode, new Locale("fa").getLanguage())
|
||||
|| TextUtils.equals(defaultLanguageCode, new Locale("ur").getLanguage())) {
|
||||
findViewById(R.id.graph_label_group).setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
|
||||
findViewById(R.id.bottom_label_group).setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPaths() {
|
||||
mUsageGraph.clearPaths();
|
||||
}
|
||||
|
||||
public void addPath(SparseIntArray points) {
|
||||
mUsageGraph.addPath(points);
|
||||
}
|
||||
|
||||
public void addProjectedPath(SparseIntArray points) {
|
||||
mUsageGraph.addProjectedPath(points);
|
||||
}
|
||||
|
||||
public void configureGraph(int maxX, int maxY) {
|
||||
mUsageGraph.setMax(maxX, maxY);
|
||||
}
|
||||
|
||||
public void setAccentColor(int color) {
|
||||
mUsageGraph.setAccentColor(color);
|
||||
}
|
||||
|
||||
public void setDividerLoc(int dividerLoc) {
|
||||
mUsageGraph.setDividerLoc(dividerLoc);
|
||||
}
|
||||
|
||||
public void setDividerColors(int middleColor, int topColor) {
|
||||
mUsageGraph.setDividerColors(middleColor, topColor);
|
||||
}
|
||||
|
||||
public void setSideLabelWeights(float before, float after) {
|
||||
setWeight(R.id.space1, before);
|
||||
setWeight(R.id.space2, after);
|
||||
}
|
||||
|
||||
private void setWeight(int id, float weight) {
|
||||
View v = findViewById(id);
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) v.getLayoutParams();
|
||||
params.weight = weight;
|
||||
v.setLayoutParams(params);
|
||||
}
|
||||
|
||||
public void setSideLabels(CharSequence[] labels) {
|
||||
if (labels.length != mLabels.length) {
|
||||
throw new IllegalArgumentException("Invalid number of labels");
|
||||
}
|
||||
for (int i = 0; i < mLabels.length; i++) {
|
||||
mLabels[i].setText(labels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void setBottomLabels(CharSequence[] labels) {
|
||||
if (labels.length != mBottomLabels.length) {
|
||||
throw new IllegalArgumentException("Invalid number of labels");
|
||||
}
|
||||
for (int i = 0; i < mBottomLabels.length; i++) {
|
||||
mBottomLabels[i].setText(labels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settingslib.CustomEditTextPreferenceCompat;
|
||||
|
||||
/**
|
||||
* {@code EditTextPreference} that supports input validation.
|
||||
*/
|
||||
public class ValidatedEditTextPreference extends CustomEditTextPreferenceCompat {
|
||||
|
||||
public interface Validator {
|
||||
boolean isTextValid(String value);
|
||||
}
|
||||
|
||||
private final EditTextWatcher mTextWatcher = new EditTextWatcher();
|
||||
private Validator mValidator;
|
||||
private boolean mIsPassword;
|
||||
private boolean mIsSummaryPassword;
|
||||
|
||||
public ValidatedEditTextPreference(Context context, AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public ValidatedEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public ValidatedEditTextPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ValidatedEditTextPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(View view) {
|
||||
super.onBindDialogView(view);
|
||||
final EditText editText = view.findViewById(android.R.id.edit);
|
||||
if (editText != null && !TextUtils.isEmpty(editText.getText())) {
|
||||
editText.setSelection(editText.getText().length());
|
||||
}
|
||||
if (mValidator != null && editText != null) {
|
||||
editText.removeTextChangedListener(mTextWatcher);
|
||||
if (mIsPassword) {
|
||||
editText.setInputType(
|
||||
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
|
||||
editText.setMaxLines(1);
|
||||
}
|
||||
editText.addTextChangedListener(mTextWatcher);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
final TextView textView = (TextView) holder.findViewById(android.R.id.summary);
|
||||
if (textView == null) {
|
||||
return;
|
||||
}
|
||||
if (mIsSummaryPassword) {
|
||||
textView.setInputType(
|
||||
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
} else {
|
||||
textView.setInputType(
|
||||
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsPassword(boolean isPassword) {
|
||||
mIsPassword = isPassword;
|
||||
}
|
||||
|
||||
public void setIsSummaryPassword(boolean isPassword) {
|
||||
mIsSummaryPassword = isPassword;
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
public boolean isPassword() {
|
||||
return mIsPassword;
|
||||
}
|
||||
|
||||
public void setValidator(Validator validator) {
|
||||
mValidator = validator;
|
||||
}
|
||||
|
||||
private class EditTextWatcher implements TextWatcher {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
final EditText editText = getEditText();
|
||||
if (mValidator != null && editText != null) {
|
||||
final AlertDialog dialog = (AlertDialog) getDialog();
|
||||
final boolean valid = mValidator.isTextValid(editText.getText().toString());
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
|
||||
/**
|
||||
* A {@link VideoPreference.AnimationController} containing a {@link
|
||||
* AnimatedVectorDrawableCompat}. The controller is used by {@link VideoPreference}
|
||||
* to display AnimatedVectorDrawable content.
|
||||
*/
|
||||
class VectorAnimationController implements VideoPreference.AnimationController {
|
||||
private AnimatedVectorDrawableCompat mAnimatedVectorDrawableCompat;
|
||||
private Drawable mPreviewDrawable;
|
||||
private Animatable2Compat.AnimationCallback mAnimationCallback;
|
||||
|
||||
/**
|
||||
* Called by a preference panel fragment to finish itself.
|
||||
*
|
||||
* @param context Application Context
|
||||
* @param animationId An {@link android.graphics.drawable.AnimationDrawable} resource id
|
||||
*/
|
||||
VectorAnimationController(Context context, int animationId) {
|
||||
mAnimatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(context, animationId);
|
||||
mAnimationCallback = new Animatable2Compat.AnimationCallback() {
|
||||
@Override
|
||||
public void onAnimationEnd(Drawable drawable) {
|
||||
mAnimatedVectorDrawableCompat.start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVideoWidth() {
|
||||
return mAnimatedVectorDrawableCompat.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVideoHeight() {
|
||||
return mAnimatedVectorDrawableCompat.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
mAnimatedVectorDrawableCompat.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mAnimatedVectorDrawableCompat.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mAnimatedVectorDrawableCompat.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() {
|
||||
// We can't get duration from AnimatedVectorDrawable, just return a non zero value.
|
||||
return 5000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachView(TextureView video, View preview, View playButton) {
|
||||
mPreviewDrawable = preview.getForeground();
|
||||
video.setVisibility(View.GONE);
|
||||
updateViewStates(preview, playButton);
|
||||
preview.setOnClickListener(v -> updateViewStates(preview, playButton));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
mAnimatedVectorDrawableCompat.stop();
|
||||
mAnimatedVectorDrawableCompat.clearAnimationCallbacks();
|
||||
}
|
||||
|
||||
private void updateViewStates(View imageView, View playButton) {
|
||||
if (mAnimatedVectorDrawableCompat.isRunning()) {
|
||||
mAnimatedVectorDrawableCompat.stop();
|
||||
mAnimatedVectorDrawableCompat.clearAnimationCallbacks();
|
||||
playButton.setVisibility(View.VISIBLE);
|
||||
imageView.setForeground(mPreviewDrawable);
|
||||
} else {
|
||||
playButton.setVisibility(View.GONE);
|
||||
imageView.setForeground(mAnimatedVectorDrawableCompat);
|
||||
mAnimatedVectorDrawableCompat.start();
|
||||
mAnimatedVectorDrawableCompat.registerAnimationCallback(mAnimationCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
250
Settings/src/com/android/settings/widget/VideoPreference.java
Normal file
250
Settings/src/com/android/settings/widget/VideoPreference.java
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* A full width preference that hosts a MP4 video or a {@link AnimatedVectorDrawableCompat}.
|
||||
*/
|
||||
public class VideoPreference extends Preference {
|
||||
|
||||
private static final String TAG = "VideoPreference";
|
||||
private final Context mContext;
|
||||
|
||||
@VisibleForTesting
|
||||
AnimationController mAnimationController;
|
||||
@VisibleForTesting
|
||||
boolean mAnimationAvailable;
|
||||
|
||||
private float mAspectRatio = 1.0f;
|
||||
private int mPreviewId;
|
||||
private int mAnimationId;
|
||||
private int mVectorAnimationId;
|
||||
private int mHeight = LinearLayout.LayoutParams.MATCH_PARENT - 1; // video height in pixels
|
||||
private TextureView mVideo;
|
||||
private ImageView mPreviewImage;
|
||||
private ImageView mPlayButton;
|
||||
|
||||
public VideoPreference(Context context) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
initialize(context, null);
|
||||
}
|
||||
|
||||
public VideoPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
initialize(context, attrs);
|
||||
}
|
||||
|
||||
private void initialize(Context context, AttributeSet attrs) {
|
||||
TypedArray attributes = context.getTheme().obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.VideoPreference,
|
||||
0, 0);
|
||||
try {
|
||||
// if these are already set that means they were set dynamically and don't need
|
||||
// to be loaded from xml
|
||||
mAnimationAvailable = false;
|
||||
mAnimationId = mAnimationId == 0
|
||||
? attributes.getResourceId(R.styleable.VideoPreference_animation, 0)
|
||||
: mAnimationId;
|
||||
mPreviewId = mPreviewId == 0
|
||||
? attributes.getResourceId(R.styleable.VideoPreference_preview, 0)
|
||||
: mPreviewId;
|
||||
mVectorAnimationId = attributes.getResourceId(
|
||||
R.styleable.VideoPreference_vectorAnimation, 0);
|
||||
if (mPreviewId == 0 && mAnimationId == 0 && mVectorAnimationId == 0) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
initAnimationController();
|
||||
if (mAnimationController != null && mAnimationController.getDuration() > 0) {
|
||||
setVisible(true);
|
||||
setLayoutResource(R.layout.video_preference);
|
||||
mAnimationAvailable = true;
|
||||
updateAspectRatio();
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Animation resource not found. Will not show animation.");
|
||||
} finally {
|
||||
attributes.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
if (!mAnimationAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
mVideo = (TextureView) holder.findViewById(R.id.video_texture_view);
|
||||
mPreviewImage = (ImageView) holder.findViewById(R.id.video_preview_image);
|
||||
mPlayButton = (ImageView) holder.findViewById(R.id.video_play_button);
|
||||
final AspectRatioFrameLayout layout = (AspectRatioFrameLayout) holder.findViewById(
|
||||
R.id.video_container);
|
||||
|
||||
mPreviewImage.setImageResource(mPreviewId);
|
||||
layout.setAspectRatio(mAspectRatio);
|
||||
if (mHeight >= LinearLayout.LayoutParams.MATCH_PARENT) {
|
||||
layout.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, mHeight));
|
||||
}
|
||||
if (mAnimationController != null) {
|
||||
mAnimationController.attachView(mVideo, mPreviewImage, mPlayButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetached() {
|
||||
releaseAnimationController();
|
||||
super.onDetached();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link VideoPreferenceController} when the view is onResume
|
||||
*/
|
||||
public void onViewVisible() {
|
||||
initAnimationController();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link VideoPreferenceController} when the view is onPause
|
||||
*/
|
||||
public void onViewInvisible() {
|
||||
releaseAnimationController();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the video for this preference. If a previous video was set this one will override it
|
||||
* and properly release any resources and re-initialize the preference to play the new video.
|
||||
*
|
||||
* @param videoId The raw res id of the video
|
||||
* @param previewId The drawable res id of the preview image to use if the video fails to load.
|
||||
*/
|
||||
public void setVideo(int videoId, int previewId) {
|
||||
mAnimationId = videoId;
|
||||
mPreviewId = previewId;
|
||||
releaseAnimationController();
|
||||
initialize(mContext, null);
|
||||
}
|
||||
|
||||
private void initAnimationController() {
|
||||
if (mVectorAnimationId != 0) {
|
||||
mAnimationController = new VectorAnimationController(mContext, mVectorAnimationId);
|
||||
return;
|
||||
}
|
||||
if (mAnimationId != 0) {
|
||||
mAnimationController = new MediaAnimationController(mContext, mAnimationId);
|
||||
if (mVideo != null) {
|
||||
mAnimationController.attachView(mVideo, mPreviewImage, mPlayButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseAnimationController() {
|
||||
if (mAnimationController != null) {
|
||||
mAnimationController.release();
|
||||
mAnimationController = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAnimationAvailable() {
|
||||
return mAnimationAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the height of the video preference
|
||||
*
|
||||
* @param height in dp
|
||||
*/
|
||||
public void setHeight(float height) {
|
||||
mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height,
|
||||
mContext.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateAspectRatio() {
|
||||
mAspectRatio = mAnimationController.getVideoWidth()
|
||||
/ (float) mAnimationController.getVideoHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle animation operations.
|
||||
*/
|
||||
interface AnimationController {
|
||||
/**
|
||||
* Pauses the animation.
|
||||
*/
|
||||
void pause();
|
||||
|
||||
/**
|
||||
* Starts the animation.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Releases the animation object.
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* Attaches the animation to UI view.
|
||||
*/
|
||||
void attachView(TextureView video, View preview, View playButton);
|
||||
|
||||
/**
|
||||
* Returns the animation Width.
|
||||
*/
|
||||
int getVideoWidth();
|
||||
|
||||
/**
|
||||
* Returns the animation Height.
|
||||
*/
|
||||
int getVideoHeight();
|
||||
|
||||
/**
|
||||
* Returns the animation duration.
|
||||
*/
|
||||
int getDuration();
|
||||
|
||||
/**
|
||||
* Returns if the animation is playing.
|
||||
*/
|
||||
boolean isPlaying();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
||||
import com.android.settingslib.core.lifecycle.events.OnStart;
|
||||
import com.android.settingslib.core.lifecycle.events.OnStop;
|
||||
|
||||
public class VideoPreferenceController extends BasePreferenceController implements
|
||||
LifecycleObserver, OnStart, OnStop {
|
||||
|
||||
private VideoPreference mVideoPreference;
|
||||
|
||||
public VideoPreferenceController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return mVideoPreference != null && mVideoPreference.isAnimationAvailable() ?
|
||||
AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen) {
|
||||
mVideoPreference = screen.findPreference(getPreferenceKey());
|
||||
super.displayPreference(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (mVideoPreference != null) {
|
||||
mVideoPreference.onViewVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (mVideoPreference != null) {
|
||||
mVideoPreference.onViewInvisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.android.settings.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.UserManager;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceCategory;
|
||||
|
||||
import com.android.settings.SelfAvailablePreference;
|
||||
import com.android.settings.Utils;
|
||||
|
||||
/**
|
||||
* A PreferenceCategory that is only visible when the device has a work profile.
|
||||
*/
|
||||
public class WorkOnlyCategory extends PreferenceCategory implements SelfAvailablePreference {
|
||||
|
||||
public WorkOnlyCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(Context context) {
|
||||
return Utils.getManagedProfile(UserManager.get(context)) != null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user