/* * Copyright (C) 2014 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.systemui.qs; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.MathUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.TextView; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.qs.QSTile.State; import java.util.Objects; /** View that represents a standard quick settings tile. **/ public class QSTileView extends ViewGroup { private static final Typeface CONDENSED = Typeface.create("sans-serif-condensed", Typeface.NORMAL); protected final Context mContext; private final View mIcon; private final View mDivider; private final H mHandler = new H(); private final int mIconSizePx; private final int mTileSpacingPx; private int mTilePaddingTopPx; private final int mTilePaddingBelowIconPx; private final int mDualTileVerticalPaddingPx; private final View mTopBackgroundView; private TextView mLabel; private QSDualTileLabel mDualLabel; private boolean mDual; private OnClickListener mClickPrimary; private OnClickListener mClickSecondary; private OnLongClickListener mLongClick; private Drawable mTileBackground; private RippleDrawable mRipple; public QSTileView(Context context) { super(context); mContext = context; final Resources res = context.getResources(); mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_tile_icon_size); mTileSpacingPx = res.getDimensionPixelSize(R.dimen.qs_tile_spacing); mTilePaddingBelowIconPx = res.getDimensionPixelSize(R.dimen.qs_tile_padding_below_icon); mDualTileVerticalPaddingPx = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical); mTileBackground = newTileBackground(); recreateLabel(); setClipChildren(false); mTopBackgroundView = new View(context); addView(mTopBackgroundView); mIcon = createIcon(); addView(mIcon); mDivider = new View(mContext); mDivider.setBackgroundColor(res.getColor(R.color.qs_tile_divider)); final int dh = res.getDimensionPixelSize(R.dimen.qs_tile_divider_height); mDivider.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dh)); addView(mDivider); setClickable(true); updateTopPadding(); } private void updateTopPadding() { Resources res = getResources(); int padding = res.getDimensionPixelSize(R.dimen.qs_tile_padding_top); int largePadding = res.getDimensionPixelSize(R.dimen.qs_tile_padding_top_large_text); float largeFactor = (MathUtils.constrain(getResources().getConfiguration().fontScale, 1.0f, FontSizeUtils.LARGE_TEXT_SCALE) - 1f) / (FontSizeUtils.LARGE_TEXT_SCALE - 1f); mTilePaddingTopPx = Math.round((1 - largeFactor) * padding + largeFactor * largePadding); requestLayout(); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateTopPadding(); FontSizeUtils.updateFontSize(mLabel, R.dimen.qs_tile_text_size); if (mDualLabel != null) { mDualLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(R.dimen.qs_tile_text_size)); } } private void recreateLabel() { CharSequence labelText = null; CharSequence labelDescription = null; if (mLabel != null) { labelText = mLabel.getText(); removeView(mLabel); mLabel = null; } if (mDualLabel != null) { labelText = mDualLabel.getText(); labelDescription = mLabel.getContentDescription(); removeView(mDualLabel); mDualLabel = null; } final Resources res = mContext.getResources(); if (mDual) { mDualLabel = new QSDualTileLabel(mContext); mDualLabel.setId(android.R.id.title); mDualLabel.setBackgroundResource(R.drawable.btn_borderless_rect); mDualLabel.setFirstLineCaret(res.getDrawable(R.drawable.qs_dual_tile_caret)); mDualLabel.setTextColor(res.getColor(R.color.qs_tile_text)); mDualLabel.setPadding(0, mDualTileVerticalPaddingPx, 0, mDualTileVerticalPaddingPx); mDualLabel.setTypeface(CONDENSED); mDualLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); mDualLabel.setClickable(true); mDualLabel.setOnClickListener(mClickSecondary); mDualLabel.setFocusable(true); if (labelText != null) { mDualLabel.setText(labelText); } if (labelDescription != null) { mDualLabel.setContentDescription(labelDescription); } addView(mDualLabel); } else { mLabel = new TextView(mContext); mLabel.setId(android.R.id.title); mLabel.setTextColor(res.getColor(R.color.qs_tile_text)); mLabel.setGravity(Gravity.CENTER_HORIZONTAL); mLabel.setMinLines(2); mLabel.setPadding(0, 0, 0, 0); mLabel.setTypeface(CONDENSED); mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); mLabel.setClickable(false); if (labelText != null) { mLabel.setText(labelText); } addView(mLabel); } } public boolean setDual(boolean dual) { final boolean changed = dual != mDual; mDual = dual; if (changed) { recreateLabel(); } if (mTileBackground instanceof RippleDrawable) { setRipple((RippleDrawable) mTileBackground); } if (dual) { mTopBackgroundView.setOnClickListener(mClickPrimary); setOnClickListener(null); setClickable(false); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); mTopBackgroundView.setBackground(mTileBackground); } else { mTopBackgroundView.setOnClickListener(null); mTopBackgroundView.setClickable(false); setOnClickListener(mClickPrimary); setOnLongClickListener(mLongClick); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); setBackground(mTileBackground); } mTopBackgroundView.setFocusable(dual); setFocusable(!dual); mDivider.setVisibility(dual ? VISIBLE : GONE); postInvalidate(); return changed; } private void setRipple(RippleDrawable tileBackground) { mRipple = tileBackground; if (getWidth() != 0) { updateRippleSize(getWidth(), getHeight()); } } public void init(OnClickListener clickPrimary, OnClickListener clickSecondary, OnLongClickListener longClick) { mClickPrimary = clickPrimary; mClickSecondary = clickSecondary; mLongClick = longClick; } protected View createIcon() { final ImageView icon = new ImageView(mContext); icon.setId(android.R.id.icon); icon.setScaleType(ScaleType.CENTER_INSIDE); return icon; } private Drawable newTileBackground() { final int[] attrs = new int[] { android.R.attr.selectableItemBackgroundBorderless }; final TypedArray ta = mContext.obtainStyledAttributes(attrs); final Drawable d = ta.getDrawable(0); ta.recycle(); return d; } private View labelView() { return mDual ? mDualLabel : mLabel; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int w = MeasureSpec.getSize(widthMeasureSpec); final int h = MeasureSpec.getSize(heightMeasureSpec); final int iconSpec = exactly(mIconSizePx); mIcon.measure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST), iconSpec); labelView().measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.AT_MOST)); if (mDual) { mDivider.measure(widthMeasureSpec, exactly(mDivider.getLayoutParams().height)); } int heightSpec = exactly( mIconSizePx + mTilePaddingBelowIconPx + mTilePaddingTopPx); mTopBackgroundView.measure(widthMeasureSpec, heightSpec); setMeasuredDimension(w, h); } private static int exactly(int size) { return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int w = getMeasuredWidth(); final int h = getMeasuredHeight(); layout(mTopBackgroundView, 0, mTileSpacingPx); int top = 0; top += mTileSpacingPx; top += mTilePaddingTopPx; final int iconLeft = (w - mIcon.getMeasuredWidth()) / 2; layout(mIcon, iconLeft, top); if (mRipple != null) { updateRippleSize(w, h); } top = mIcon.getBottom(); top += mTilePaddingBelowIconPx; if (mDual) { layout(mDivider, 0, top); top = mDivider.getBottom(); } layout(labelView(), 0, top); } private void updateRippleSize(int width, int height) { // center the touch feedback on the center of the icon, and dial it down a bit final int cx = width / 2; final int cy = mDual ? mIcon.getTop() + mIcon.getHeight() / 2 : height / 2; final int rad = (int)(mIcon.getHeight() * 1.25f); mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad); } private static void layout(View child, int left, int top) { child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); } protected void handleStateChanged(QSTile.State state) { if (mIcon instanceof ImageView) { setIcon((ImageView) mIcon, state); } if (mDual) { mDualLabel.setText(state.label); mDualLabel.setContentDescription(state.dualLabelContentDescription); mTopBackgroundView.setContentDescription(state.contentDescription); } else { mLabel.setText(state.label); setContentDescription(state.contentDescription); } } protected void setIcon(ImageView iv, QSTile.State state) { if (!Objects.equals(state.icon, iv.getTag(R.id.qs_icon_tag))) { Drawable d = state.icon != null ? state.icon.getDrawable(mContext) : null; if (d != null && state.autoMirrorDrawable) { d.setAutoMirrored(true); } iv.setImageDrawable(d); iv.setTag(R.id.qs_icon_tag, state.icon); if (d instanceof Animatable) { if (!iv.isShown()) { ((Animatable) d).stop(); // skip directly to end state } } } } public void onStateChanged(QSTile.State state) { mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget(); } private class H extends Handler { private static final int STATE_CHANGED = 1; public H() { super(Looper.getMainLooper()); } @Override public void handleMessage(Message msg) { if (msg.what == STATE_CHANGED) { handleStateChanged((State) msg.obj); } } } }