1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14package com.android.systemui.qs.tileimpl; 15 16import android.content.Context; 17import android.content.res.TypedArray; 18import android.graphics.drawable.Drawable; 19import android.graphics.drawable.RippleDrawable; 20import android.os.Handler; 21import android.os.Looper; 22import android.os.Message; 23import android.service.quicksettings.Tile; 24import android.text.TextUtils; 25import android.view.Gravity; 26import android.view.View; 27import android.view.ViewGroup; 28import android.view.accessibility.AccessibilityEvent; 29import android.view.accessibility.AccessibilityNodeInfo; 30import android.widget.FrameLayout; 31import android.widget.Switch; 32 33import com.android.systemui.R; 34import com.android.systemui.plugins.qs.*; 35import com.android.systemui.plugins.qs.QSTile.BooleanState; 36 37public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { 38 39 private static final String TAG = "QSTileBaseView"; 40 private final H mHandler = new H(); 41 private final FrameLayout mIconFrame; 42 protected QSIconView mIcon; 43 protected RippleDrawable mRipple; 44 private Drawable mTileBackground; 45 private String mAccessibilityClass; 46 private boolean mTileState; 47 private boolean mCollapsedView; 48 private boolean mClicked; 49 50 public QSTileBaseView(Context context, QSIconView icon) { 51 this(context, icon, false); 52 } 53 54 public QSTileBaseView(Context context, QSIconView icon, boolean collapsedView) { 55 super(context); 56 // Default to Quick Tile padding, and QSTileView will specify its own padding. 57 int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding); 58 59 mIconFrame = new FrameLayout(context); 60 mIconFrame.setForegroundGravity(Gravity.CENTER); 61 int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size); 62 addView(mIconFrame, new LayoutParams(size, size)); 63 mIcon = icon; 64 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 65 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 66 params.setMargins(0, padding, 0, padding); 67 mIconFrame.addView(mIcon, params); 68 69 mTileBackground = newTileBackground(); 70 if (mTileBackground instanceof RippleDrawable) { 71 setRipple((RippleDrawable) mTileBackground); 72 } 73 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 74 setBackground(mTileBackground); 75 76 setPadding(0, 0, 0, 0); 77 setClipChildren(false); 78 setClipToPadding(false); 79 mCollapsedView = collapsedView; 80 setFocusable(true); 81 } 82 83 protected Drawable newTileBackground() { 84 final int[] attrs = new int[]{android.R.attr.selectableItemBackgroundBorderless}; 85 final TypedArray ta = getContext().obtainStyledAttributes(attrs); 86 final Drawable d = ta.getDrawable(0); 87 ta.recycle(); 88 return d; 89 } 90 91 private void setRipple(RippleDrawable tileBackground) { 92 mRipple = tileBackground; 93 if (getWidth() != 0) { 94 updateRippleSize(getWidth(), getHeight()); 95 } 96 } 97 98 private void updateRippleSize(int width, int height) { 99 // center the touch feedback on the center of the icon, and dial it down a bit 100 final int cx = width / 2; 101 final int cy = mIconFrame.getMeasuredHeight() / 2; 102 final int rad = (int) (mIcon.getHeight() * .85f); 103 mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad); 104 } 105 106 @Override 107 public void init(QSTile tile) { 108 init(v -> tile.click(), v -> tile.secondaryClick(), view -> { 109 tile.longClick(); 110 return true; 111 }); 112 } 113 114 public void init(OnClickListener click, OnClickListener secondaryClick, 115 OnLongClickListener longClick) { 116 setOnClickListener(click); 117 setOnLongClickListener(longClick); 118 } 119 120 @Override 121 protected void onLayout(boolean changed, int l, int t, int r, int b) { 122 super.onLayout(changed, l, t, r, b); 123 final int w = getMeasuredWidth(); 124 final int h = getMeasuredHeight(); 125 126 if (mRipple != null) { 127 updateRippleSize(w, h); 128 } 129 } 130 131 @Override 132 public boolean hasOverlappingRendering() { 133 // Avoid layers for this layout - we don't need them. 134 return false; 135 } 136 137 /** 138 * Update the accessibility order for this view. 139 * 140 * @param previousView the view which should be before this one 141 * @return the last view in this view which is accessible 142 */ 143 public View updateAccessibilityOrder(View previousView) { 144 setAccessibilityTraversalAfter(previousView.getId()); 145 return this; 146 } 147 148 public void onStateChanged(QSTile.State state) { 149 mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget(); 150 } 151 152 protected void handleStateChanged(QSTile.State state) { 153 setClickable(state.state != Tile.STATE_UNAVAILABLE); 154 mIcon.setIcon(state); 155 setContentDescription(state.contentDescription); 156 mAccessibilityClass = state.expandedAccessibilityClassName; 157 if (state instanceof QSTile.BooleanState) { 158 boolean newState = ((BooleanState) state).value; 159 if (mTileState != newState) { 160 mClicked = false; 161 mTileState = newState; 162 } 163 } 164 } 165 166 @Override 167 public void setClickable(boolean clickable) { 168 super.setClickable(clickable); 169 setBackground(clickable ? mRipple : null); 170 } 171 172 @Override 173 public int getDetailY() { 174 return getTop() + getHeight() / 2; 175 } 176 177 public QSIconView getIcon() { 178 return mIcon; 179 } 180 181 @Override 182 public boolean performClick() { 183 mClicked = true; 184 return super.performClick(); 185 } 186 187 @Override 188 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 189 super.onInitializeAccessibilityEvent(event); 190 if (!TextUtils.isEmpty(mAccessibilityClass)) { 191 event.setClassName(mAccessibilityClass); 192 if (Switch.class.getName().equals(mAccessibilityClass)) { 193 boolean b = mClicked ? !mTileState : mTileState; 194 String label = getResources() 195 .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off); 196 event.setContentDescription(label); 197 event.setChecked(b); 198 } 199 } 200 } 201 202 @Override 203 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 204 super.onInitializeAccessibilityNodeInfo(info); 205 if (!TextUtils.isEmpty(mAccessibilityClass)) { 206 info.setClassName(mAccessibilityClass); 207 if (Switch.class.getName().equals(mAccessibilityClass)) { 208 boolean b = mClicked ? !mTileState : mTileState; 209 String label = getResources() 210 .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off); 211 info.setText(label); 212 info.setChecked(b); 213 info.setCheckable(true); 214 } 215 } 216 } 217 218 private class H extends Handler { 219 private static final int STATE_CHANGED = 1; 220 public H() { 221 super(Looper.getMainLooper()); 222 } 223 224 @Override 225 public void handleMessage(Message msg) { 226 if (msg.what == STATE_CHANGED) { 227 handleStateChanged((QSTile.State) msg.obj); 228 } 229 } 230 } 231} 232