1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.slice.widget; 18 19 import static android.app.slice.Slice.HINT_NO_TINT; 20 import static android.app.slice.SliceItem.FORMAT_IMAGE; 21 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT; 22 23 import static androidx.slice.core.SliceHints.ICON_IMAGE; 24 25 import android.app.PendingIntent.CanceledException; 26 import android.app.RemoteInput; 27 import android.app.slice.Slice; 28 import android.content.Context; 29 import android.content.res.ColorStateList; 30 import android.graphics.Color; 31 import android.os.Build; 32 import android.util.Log; 33 import android.util.TypedValue; 34 import android.view.View; 35 import android.view.ViewParent; 36 import android.widget.FrameLayout; 37 import android.widget.ImageView; 38 import android.widget.ImageView.ScaleType; 39 import android.widget.LinearLayout; 40 import android.widget.TextView; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.RequiresApi; 44 import androidx.annotation.RestrictTo; 45 import androidx.core.graphics.drawable.IconCompat; 46 import androidx.core.widget.ImageViewCompat; 47 import androidx.slice.SliceItem; 48 import androidx.slice.core.SliceActionImpl; 49 import androidx.slice.core.SliceQuery; 50 51 import java.util.List; 52 53 /** 54 * @hide 55 */ 56 @RestrictTo(RestrictTo.Scope.LIBRARY) 57 public class ActionRow extends FrameLayout { 58 59 private static final int MAX_ACTIONS = 5; 60 private static final String TAG = "ActionRow"; 61 62 private final int mSize; 63 private final int mIconPadding; 64 private final LinearLayout mActionsGroup; 65 private final boolean mFullActions; 66 private int mColor = Color.BLACK; 67 ActionRow(Context context, boolean fullActions)68 public ActionRow(Context context, boolean fullActions) { 69 super(context); 70 mFullActions = fullActions; 71 mSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, 72 context.getResources().getDisplayMetrics()); 73 mIconPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, 74 context.getResources().getDisplayMetrics()); 75 mActionsGroup = new LinearLayout(context); 76 mActionsGroup.setOrientation(LinearLayout.HORIZONTAL); 77 mActionsGroup.setLayoutParams( 78 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 79 addView(mActionsGroup); 80 } 81 setColor(int color)82 private void setColor(int color) { 83 mColor = color; 84 for (int i = 0; i < mActionsGroup.getChildCount(); i++) { 85 View view = mActionsGroup.getChildAt(i); 86 int mode = (Integer) view.getTag(); 87 boolean tint = mode == ICON_IMAGE; 88 if (tint) { 89 ImageViewCompat.setImageTintList((ImageView) view, ColorStateList.valueOf(mColor)); 90 } 91 } 92 } 93 addAction(IconCompat icon, boolean allowTint)94 private ImageView addAction(IconCompat icon, boolean allowTint) { 95 ImageView imageView = new ImageView(getContext()); 96 imageView.setPadding(mIconPadding, mIconPadding, mIconPadding, mIconPadding); 97 imageView.setScaleType(ScaleType.FIT_CENTER); 98 imageView.setImageDrawable(icon.loadDrawable(getContext())); 99 if (allowTint) { 100 ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(mColor)); 101 } 102 imageView.setBackground(SliceViewUtil.getDrawable(getContext(), 103 android.R.attr.selectableItemBackground)); 104 imageView.setTag(allowTint); 105 addAction(imageView); 106 return imageView; 107 } 108 109 /** 110 * Set the actions and color for this action row. 111 */ setActions(@onNull List<SliceItem> actions, int color)112 public void setActions(@NonNull List<SliceItem> actions, int color) { 113 removeAllViews(); 114 mActionsGroup.removeAllViews(); 115 addView(mActionsGroup); 116 if (color != -1) { 117 setColor(color); 118 } 119 for (final SliceItem action : actions) { 120 if (mActionsGroup.getChildCount() >= MAX_ACTIONS) { 121 return; 122 } 123 final SliceItem input = SliceQuery.find(action, FORMAT_REMOTE_INPUT); 124 final SliceItem image = SliceQuery.find(action, FORMAT_IMAGE); 125 if (input != null && image != null) { 126 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 127 handleSetRemoteInputActions(input, image, action); 128 } else { 129 Log.w(TAG, "Received RemoteInput on API <20 " + input); 130 } 131 } else if (action.hasHint(Slice.HINT_SHORTCUT)) { 132 final SliceActionImpl ac = new SliceActionImpl(action); 133 IconCompat iconItem = ac.getIcon(); 134 if (iconItem != null && ac.getActionItem() != null) { 135 boolean tint = ac.getImageMode() == ICON_IMAGE; 136 addAction(iconItem, tint).setOnClickListener( 137 new OnClickListener() { 138 @Override 139 public void onClick(View v) { 140 try { 141 // TODO - should log events here 142 ac.getActionItem().fireAction(null, null); 143 } catch (CanceledException e) { 144 e.printStackTrace(); 145 } 146 } 147 }); 148 } 149 } 150 } 151 setVisibility(getChildCount() != 0 ? View.VISIBLE : View.GONE); 152 } 153 addAction(View child)154 private void addAction(View child) { 155 mActionsGroup.addView(child, new LinearLayout.LayoutParams(mSize, mSize, 1)); 156 } 157 158 @RequiresApi(21) handleSetRemoteInputActions(final SliceItem input, SliceItem image, final SliceItem action)159 private void handleSetRemoteInputActions(final SliceItem input, SliceItem image, 160 final SliceItem action) { 161 if (input.getRemoteInput().getAllowFreeFormInput()) { 162 boolean tint = !image.hasHint(HINT_NO_TINT); 163 addAction(image.getIcon(), tint).setOnClickListener( 164 new OnClickListener() { 165 @Override 166 public void onClick(View v) { 167 handleRemoteInputClick(v, action, 168 input.getRemoteInput()); 169 } 170 }); 171 createRemoteInputView(mColor, getContext()); 172 } 173 } 174 175 @RequiresApi(21) createRemoteInputView(int color, Context context)176 private void createRemoteInputView(int color, Context context) { 177 View riv = RemoteInputView.inflate(context, this); 178 riv.setVisibility(View.INVISIBLE); 179 addView(riv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 180 riv.setBackgroundColor(color); 181 } 182 183 @RequiresApi(21) handleRemoteInputClick(View view, SliceItem action, RemoteInput input)184 private boolean handleRemoteInputClick(View view, SliceItem action, 185 RemoteInput input) { 186 if (input == null) { 187 return false; 188 } 189 190 ViewParent p = view.getParent().getParent(); 191 RemoteInputView riv = null; 192 while (p != null) { 193 if (p instanceof View) { 194 View pv = (View) p; 195 riv = findRemoteInputView(pv); 196 if (riv != null) { 197 break; 198 } 199 } 200 p = p.getParent(); 201 } 202 if (riv == null) { 203 return false; 204 } 205 206 int width = view.getWidth(); 207 if (view instanceof TextView) { 208 // Center the reveal on the text which might be off-center from the TextView 209 TextView tv = (TextView) view; 210 if (tv.getLayout() != null) { 211 int innerWidth = (int) tv.getLayout().getLineWidth(0); 212 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); 213 width = Math.min(width, innerWidth); 214 } 215 } 216 int cx = view.getLeft() + width / 2; 217 int cy = view.getTop() + view.getHeight() / 2; 218 int w = riv.getWidth(); 219 int h = riv.getHeight(); 220 int r = Math.max( 221 Math.max(cx + cy, cx + (h - cy)), 222 Math.max((w - cx) + cy, (w - cx) + (h - cy))); 223 224 riv.setRevealParameters(cx, cy, r); 225 riv.setAction(action); 226 riv.setRemoteInput(new RemoteInput[] { 227 input 228 }, input); 229 riv.focusAnimated(); 230 return true; 231 } 232 233 @RequiresApi(21) findRemoteInputView(View v)234 private RemoteInputView findRemoteInputView(View v) { 235 if (v == null) { 236 return null; 237 } 238 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); 239 } 240 } 241