1 /*
2  * Copyright (C) 2010 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.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.content.res.TypedArray;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.LinearLayout;
30 import android.widget.TextView;
31 
32 import androidx.annotation.RestrictTo;
33 import androidx.appcompat.R;
34 import androidx.appcompat.view.ActionMode;
35 import androidx.appcompat.view.menu.MenuBuilder;
36 import androidx.core.view.ViewCompat;
37 
38 import org.jspecify.annotations.NonNull;
39 import org.jspecify.annotations.Nullable;
40 
41 /**
42  */
43 @RestrictTo(LIBRARY_GROUP_PREFIX)
44 public class ActionBarContextView extends AbsActionBarView {
45     private CharSequence mTitle;
46     private CharSequence mSubtitle;
47 
48     private View mClose;
49     private View mCloseButton;
50     private View mCustomView;
51     private LinearLayout mTitleLayout;
52     private TextView mTitleView;
53     private TextView mSubtitleView;
54     private int mTitleStyleRes;
55     private int mSubtitleStyleRes;
56     private boolean mTitleOptional;
57     private int mCloseItemLayout;
58     private final int mInternalVerticalPadding;
59 
ActionBarContextView(@onNull Context context)60     public ActionBarContextView(@NonNull Context context) {
61         this(context, null);
62     }
63 
ActionBarContextView(@onNull Context context, @Nullable AttributeSet attrs)64     public ActionBarContextView(@NonNull Context context, @Nullable AttributeSet attrs) {
65         this(context, attrs, R.attr.actionModeStyle);
66     }
67 
ActionBarContextView( @onNull Context context, @Nullable AttributeSet attrs, int defStyle)68     public ActionBarContextView(
69             @NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
70         super(context, attrs, defStyle);
71 
72         final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
73                 R.styleable.ActionMode, defStyle, 0);
74         setBackground(a.getDrawable(R.styleable.ActionMode_background));
75         mTitleStyleRes = a.getResourceId(
76                 R.styleable.ActionMode_titleTextStyle, 0);
77         mSubtitleStyleRes = a.getResourceId(
78                 R.styleable.ActionMode_subtitleTextStyle, 0);
79 
80         mContentHeight = a.getLayoutDimension(
81                 R.styleable.ActionMode_height, 0);
82 
83         mCloseItemLayout = a.getResourceId(
84                 R.styleable.ActionMode_closeItemLayout,
85                 R.layout.abc_action_mode_close_item_material);
86 
87         a.recycle();
88 
89         mInternalVerticalPadding = getPaddingTop() + getPaddingBottom();
90     }
91 
92     @Override
onDetachedFromWindow()93     public void onDetachedFromWindow() {
94         super.onDetachedFromWindow();
95         if (mActionMenuPresenter != null) {
96             mActionMenuPresenter.hideOverflowMenu();
97             mActionMenuPresenter.hideSubMenus();
98         }
99     }
100 
101     @Override
onConfigurationChanged(Configuration newConfig)102     protected void onConfigurationChanged(Configuration newConfig) {
103         super.onConfigurationChanged(newConfig);
104 
105         // Action bar can change size on configuration changes.
106         // Reread the desired height from the theme-specified style.
107         final TypedArray a = getContext().obtainStyledAttributes(null, R.styleable.ActionMode,
108                 R.attr.actionModeStyle, 0);
109         setContentHeight(a.getLayoutDimension(R.styleable.ActionMode_height, 0));
110         a.recycle();
111     }
112 
113     @Override
setContentHeight(int height)114     public void setContentHeight(int height) {
115         mContentHeight = height;
116     }
117 
setCustomView(View view)118     public void setCustomView(View view) {
119         if (mCustomView != null) {
120             removeView(mCustomView);
121         }
122         mCustomView = view;
123         if (view != null && mTitleLayout != null) {
124             removeView(mTitleLayout);
125             mTitleLayout = null;
126         }
127         if (view != null) {
128             addView(view);
129         }
130         requestLayout();
131     }
132 
setTitle(CharSequence title)133     public void setTitle(CharSequence title) {
134         mTitle = title;
135         initTitle();
136         ViewCompat.setAccessibilityPaneTitle(this, title);
137     }
138 
setSubtitle(CharSequence subtitle)139     public void setSubtitle(CharSequence subtitle) {
140         mSubtitle = subtitle;
141         initTitle();
142     }
143 
getTitle()144     public CharSequence getTitle() {
145         return mTitle;
146     }
147 
getSubtitle()148     public CharSequence getSubtitle() {
149         return mSubtitle;
150     }
151 
initTitle()152     private void initTitle() {
153         if (mTitleLayout == null) {
154             LayoutInflater inflater = LayoutInflater.from(getContext());
155             inflater.inflate(R.layout.abc_action_bar_title_item, this);
156             mTitleLayout = (LinearLayout) getChildAt(getChildCount() - 1);
157             mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
158             mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
159             if (mTitleStyleRes != 0) {
160                 mTitleView.setTextAppearance(getContext(), mTitleStyleRes);
161             }
162             if (mSubtitleStyleRes != 0) {
163                 mSubtitleView.setTextAppearance(getContext(), mSubtitleStyleRes);
164             }
165         }
166 
167         mTitleView.setText(mTitle);
168         mSubtitleView.setText(mSubtitle);
169 
170         final boolean hasTitle = !TextUtils.isEmpty(mTitle);
171         final boolean hasSubtitle = !TextUtils.isEmpty(mSubtitle);
172         mSubtitleView.setVisibility(hasSubtitle ? VISIBLE : GONE);
173         mTitleLayout.setVisibility(hasTitle || hasSubtitle ? VISIBLE : GONE);
174         if (mTitleLayout.getParent() == null) {
175             addView(mTitleLayout);
176         }
177     }
178 
initForMode(final ActionMode mode)179     public void initForMode(final ActionMode mode) {
180         if (mClose == null) {
181             LayoutInflater inflater = LayoutInflater.from(getContext());
182             mClose = inflater.inflate(mCloseItemLayout, this, false);
183             addView(mClose);
184         } else if (mClose.getParent() == null) {
185             addView(mClose);
186         }
187 
188         mCloseButton = mClose.findViewById(R.id.action_mode_close_button);
189         mCloseButton.setOnClickListener(new OnClickListener() {
190             @Override
191             public void onClick(View v) {
192                 mode.finish();
193             }
194         });
195 
196         final MenuBuilder menu = (MenuBuilder) mode.getMenu();
197         if (mActionMenuPresenter != null) {
198             mActionMenuPresenter.dismissPopupMenus();
199         }
200         mActionMenuPresenter = new ActionMenuPresenter(getContext());
201         mActionMenuPresenter.setReserveOverflow(true);
202 
203         final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
204                 LayoutParams.MATCH_PARENT);
205         menu.addMenuPresenter(mActionMenuPresenter, mPopupContext);
206         mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
207         mMenuView.setBackground(null);
208         addView(mMenuView, layoutParams);
209     }
210 
closeMode()211     public void closeMode() {
212         if (mClose == null) {
213             killMode();
214             return;
215         }
216     }
217 
killMode()218     public void killMode() {
219         removeAllViews();
220         mCustomView = null;
221         mMenuView = null;
222         mActionMenuPresenter = null;
223         if (mCloseButton != null) {
224             mCloseButton.setOnClickListener(null);
225         }
226     }
227 
228     @Override
showOverflowMenu()229     public boolean showOverflowMenu() {
230         if (mActionMenuPresenter != null) {
231             return mActionMenuPresenter.showOverflowMenu();
232         }
233         return false;
234     }
235 
236     @Override
hideOverflowMenu()237     public boolean hideOverflowMenu() {
238         if (mActionMenuPresenter != null) {
239             return mActionMenuPresenter.hideOverflowMenu();
240         }
241         return false;
242     }
243 
244     @Override
isOverflowMenuShowing()245     public boolean isOverflowMenuShowing() {
246         if (mActionMenuPresenter != null) {
247             return mActionMenuPresenter.isOverflowMenuShowing();
248         }
249         return false;
250     }
251 
252     @Override
generateDefaultLayoutParams()253     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
254         // Used by custom views if they don't supply layout params. Everything else
255         // added to an ActionBarContextView should have them already.
256         return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
257     }
258 
259     @Override
generateLayoutParams(AttributeSet attrs)260     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
261         return new MarginLayoutParams(getContext(), attrs);
262     }
263 
264     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)265     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
266         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
267         if (widthMode != MeasureSpec.EXACTLY) {
268             throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
269                     "with android:layout_width=\"match_parent\" (or fill_parent)");
270         }
271 
272         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
273         if (heightMode == MeasureSpec.UNSPECIFIED) {
274             throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
275                     "with android:layout_height=\"wrap_content\"");
276         }
277 
278         final int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
279         int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
280 
281         final int verticalPadding = getPaddingTop() + getPaddingBottom();
282         final int externalVerticalPadding = Math.max(0, verticalPadding - mInternalVerticalPadding);
283         final int maxHeight = mContentHeight > 0
284                 ? mContentHeight + externalVerticalPadding
285                 : MeasureSpec.getSize(heightMeasureSpec);
286         final int height = maxHeight - verticalPadding;
287         final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
288 
289         if (mClose != null) {
290             availableWidth = measureChildView(mClose, availableWidth, childSpecHeight, 0);
291             MarginLayoutParams lp = (MarginLayoutParams) mClose.getLayoutParams();
292             availableWidth -= lp.leftMargin + lp.rightMargin;
293         }
294 
295         if (mMenuView != null && mMenuView.getParent() == this) {
296             availableWidth = measureChildView(mMenuView, availableWidth,
297                     childSpecHeight, 0);
298         }
299 
300         if (mTitleLayout != null && mCustomView == null) {
301             if (mTitleOptional) {
302                 final int titleWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
303                 mTitleLayout.measure(titleWidthSpec, childSpecHeight);
304                 final int titleWidth = mTitleLayout.getMeasuredWidth();
305                 final boolean titleFits = titleWidth <= availableWidth;
306                 if (titleFits) {
307                     availableWidth -= titleWidth;
308                 }
309                 mTitleLayout.setVisibility(titleFits ? VISIBLE : GONE);
310             } else {
311                 availableWidth = measureChildView(mTitleLayout, availableWidth, childSpecHeight, 0);
312             }
313         }
314 
315         if (mCustomView != null) {
316             ViewGroup.LayoutParams lp = mCustomView.getLayoutParams();
317             final int customWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
318                     MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
319             final int customWidth = lp.width >= 0 ?
320                     Math.min(lp.width, availableWidth) : availableWidth;
321             final int customHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
322                     MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
323             final int customHeight = lp.height >= 0 ?
324                     Math.min(lp.height, height) : height;
325             mCustomView.measure(MeasureSpec.makeMeasureSpec(customWidth, customWidthMode),
326                     MeasureSpec.makeMeasureSpec(customHeight, customHeightMode));
327         }
328 
329         if (mContentHeight <= 0) {
330             int measuredHeight = 0;
331             final int count = getChildCount();
332             for (int i = 0; i < count; i++) {
333                 View v = getChildAt(i);
334                 int paddedViewHeight = v.getMeasuredHeight() + verticalPadding;
335                 if (paddedViewHeight > measuredHeight) {
336                     measuredHeight = paddedViewHeight;
337                 }
338             }
339             setMeasuredDimension(contentWidth, measuredHeight);
340         } else {
341             setMeasuredDimension(contentWidth, maxHeight);
342         }
343     }
344 
345     @Override
onLayout(boolean changed, int l, int t, int r, int b)346     protected void onLayout(boolean changed, int l, int t, int r, int b) {
347         final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this);
348         int x = isLayoutRtl ? r - l - getPaddingRight() : getPaddingLeft();
349         final int y = getPaddingTop();
350         final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
351 
352         if (mClose != null && mClose.getVisibility() != GONE) {
353             MarginLayoutParams lp = (MarginLayoutParams) mClose.getLayoutParams();
354             final int startMargin = (isLayoutRtl ? lp.rightMargin : lp.leftMargin);
355             final int endMargin = (isLayoutRtl ? lp.leftMargin : lp.rightMargin);
356             x = next(x, startMargin, isLayoutRtl);
357             x += positionChild(mClose, x, y, contentHeight, isLayoutRtl);
358             x = next(x, endMargin, isLayoutRtl);
359         }
360 
361         if (mTitleLayout != null && mCustomView == null && mTitleLayout.getVisibility() != GONE) {
362             x += positionChild(mTitleLayout, x, y, contentHeight, isLayoutRtl);
363         }
364 
365         if (mCustomView != null) {
366             x += positionChild(mCustomView, x, y, contentHeight, isLayoutRtl);
367         }
368 
369         x = isLayoutRtl ? getPaddingLeft() : r - l - getPaddingRight();
370 
371         if (mMenuView != null) {
372             x += positionChild(mMenuView, x, y, contentHeight, !isLayoutRtl);
373         }
374     }
375 
376     @Override
shouldDelayChildPressedState()377     public boolean shouldDelayChildPressedState() {
378         return false;
379     }
380 
setTitleOptional(boolean titleOptional)381     public void setTitleOptional(boolean titleOptional) {
382         if (titleOptional != mTitleOptional) {
383             requestLayout();
384         }
385         mTitleOptional = titleOptional;
386     }
387 
isTitleOptional()388     public boolean isTitleOptional() {
389         return mTitleOptional;
390     }
391 }
392