1 /* 2 * Copyright (C) 2019 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 com.android.car.dialer.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.util.AttributeSet; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.widget.FrameLayout; 25 import android.widget.ImageView; 26 import android.widget.TextView; 27 28 import androidx.annotation.DrawableRes; 29 import androidx.annotation.IdRes; 30 import androidx.annotation.IntDef; 31 import androidx.annotation.LayoutRes; 32 import androidx.annotation.MainThread; 33 import androidx.annotation.Nullable; 34 import androidx.annotation.StringRes; 35 36 import com.android.car.apps.common.util.ViewUtils; 37 import com.android.car.dialer.Constants; 38 import com.android.car.dialer.R; 39 import com.android.car.dialer.log.L; 40 41 /** 42 * A widget that supports different {@link State}s: NEW, LOADING, CONTENT, EMPTY OR ERROR. 43 */ 44 public class LoadingFrameLayout extends FrameLayout { 45 private static final String TAG = "CD.LoadingFrameLayout"; 46 47 /** 48 * Possible states of a service request display. 49 */ 50 @IntDef({State.NEW, State.LOADING, State.CONTENT, State.ERROR, State.EMPTY}) 51 public @interface State { 52 int NEW = 0; 53 int LOADING = 1; 54 int CONTENT = 2; 55 int ERROR = 3; 56 int EMPTY = 4; 57 } 58 59 private final Context mContext; 60 61 private ViewContainer mEmptyView; 62 private ViewContainer mLoadingView; 63 private ViewContainer mErrorView; 64 65 @State 66 private int mState = State.NEW; 67 LoadingFrameLayout(Context context, AttributeSet attrs)68 public LoadingFrameLayout(Context context, AttributeSet attrs) { 69 this(context, attrs, 0); 70 } 71 LoadingFrameLayout(Context context, AttributeSet attrs, int defStyle)72 public LoadingFrameLayout(Context context, AttributeSet attrs, int defStyle) { 73 super(context, attrs, defStyle); 74 mContext = context; 75 TypedArray values = 76 context.obtainStyledAttributes(attrs, R.styleable.LoadingFrameLayout, defStyle, 0); 77 setLoadingView( 78 values.getResourceId( 79 R.styleable.LoadingFrameLayout_progressViewLayout, 80 R.layout.loading_progress_view)); 81 setEmptyView( 82 values.getResourceId( 83 R.styleable.LoadingFrameLayout_emptyViewLayout, 84 R.layout.loading_info_view)); 85 setErrorView( 86 values.getResourceId( 87 R.styleable.LoadingFrameLayout_errorViewLayout, 88 R.layout.loading_info_view)); 89 values.recycle(); 90 } 91 92 @Override onFinishInflate()93 public void onFinishInflate() { 94 super.onFinishInflate(); 95 // Start with a loading view when inflated from XML. 96 showLoading(); 97 } 98 setLoadingView(int loadingLayoutId)99 private void setLoadingView(int loadingLayoutId) { 100 mLoadingView = new ViewContainer(State.LOADING, loadingLayoutId, 0, 0, 0, 0); 101 } 102 setEmptyView(int emptyLayoutId)103 private void setEmptyView(int emptyLayoutId) { 104 mEmptyView = new ViewContainer(State.EMPTY, emptyLayoutId, R.id.loading_info_icon, 105 R.id.loading_info_message, R.id.loading_info_secondary_message, 106 R.id.loading_info_action_button); 107 } 108 setErrorView(int errorLayoutId)109 private void setErrorView(int errorLayoutId) { 110 mErrorView = new ViewContainer(State.ERROR, errorLayoutId, R.id.loading_info_icon, 111 R.id.loading_info_message, R.id.loading_info_secondary_message, 112 R.id.loading_info_action_button); 113 } 114 115 /** 116 * Shows the loading view, hides other views. 117 */ 118 @MainThread showLoading()119 public void showLoading() { 120 switchTo(State.LOADING); 121 } 122 123 /** 124 * Shows the error view where the action button is not available and hides other views. 125 * 126 * @param iconResId drawable resource id used for the top icon. When it is invalid, 127 * hide the icon view. 128 * @param messageResId string resource id used for the error message. When it is 129 * invalid, hide the message view. 130 * @param secondaryMessageResId string resource id for the secondary error message. When it is 131 * invalid, hide the secondary message view. 132 */ showError(@rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId)133 public void showError(@DrawableRes int iconResId, @StringRes int messageResId, 134 @StringRes int secondaryMessageResId) { 135 showError(iconResId, messageResId, secondaryMessageResId, Constants.INVALID_RES_ID, null, 136 false); 137 } 138 139 /** 140 * Shows the error view, hides other views. 141 * 142 * @param iconResId drawable resource id used for the top icon.When it is 143 * invalid, hide the icon view. 144 * @param messageResId string resource id used for the error message. When it is 145 * invalid, hide the message view. 146 * @param secondaryMessageResId string resource id for the secondary error message. When 147 * it is invalid, hide the secondary message view. 148 * @param actionButtonTextResId string resource id for the action button. 149 * @param actionButtonOnClickListener click listener set on the action button. 150 * @param showActionButton boolean flag if the action button will show. 151 */ showError( @rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId, @StringRes int actionButtonTextResId, View.OnClickListener actionButtonOnClickListener, boolean showActionButton)152 public void showError( 153 @DrawableRes int iconResId, 154 @StringRes int messageResId, 155 @StringRes int secondaryMessageResId, 156 @StringRes int actionButtonTextResId, 157 View.OnClickListener actionButtonOnClickListener, 158 boolean showActionButton) { 159 mErrorView.setIcon(iconResId); 160 mErrorView.setMessage(messageResId); 161 mErrorView.setSecondaryMessage(secondaryMessageResId); 162 mErrorView.setActionButtonText(actionButtonTextResId); 163 mErrorView.setActionButtonClickListener(actionButtonOnClickListener); 164 mErrorView.setActionButtonVisible(showActionButton); 165 switchTo(State.ERROR); 166 } 167 168 /** 169 * Shows the empty view where the action button is not available and hides other views. 170 * 171 * @param iconResId drawable resource id used for the top icon. When it is invalid, 172 * hide the icon view. 173 * @param messageResId string resource id used for the empty message. When it is 174 * invalid, hide the message view. 175 * @param secondaryMessageResId string resource id for the secondary empty message. When it is 176 * invalid, hide the secondary message view. 177 */ showEmpty(@rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId)178 public void showEmpty(@DrawableRes int iconResId, @StringRes int messageResId, 179 @StringRes int secondaryMessageResId) { 180 showEmpty(iconResId, messageResId, secondaryMessageResId, Constants.INVALID_RES_ID, null, 181 false); 182 } 183 184 /** 185 * Shows the empty view and hides other views. 186 * 187 * @param iconResId drawable resource id used for the top icon.When it is 188 * invalid, hide the icon view. 189 * @param messageResId string resource id used for the empty message. When it is 190 * invalid, hide the message view. 191 * @param secondaryMessageResId string resource id for the secondary empty message. When 192 * it is invalid, hide the secondary message view. 193 * @param actionButtonTextResId string resource id for the action button. 194 * @param actionButtonOnClickListener click listener set on the action button. 195 * @param showActionButton boolean flag if the action button will show. 196 */ showEmpty( @rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId, @StringRes int actionButtonTextResId, @Nullable View.OnClickListener actionButtonOnClickListener, boolean showActionButton)197 public void showEmpty( 198 @DrawableRes int iconResId, 199 @StringRes int messageResId, 200 @StringRes int secondaryMessageResId, 201 @StringRes int actionButtonTextResId, 202 @Nullable View.OnClickListener actionButtonOnClickListener, 203 boolean showActionButton) { 204 mEmptyView.setIcon(iconResId); 205 mEmptyView.setMessage(messageResId); 206 mEmptyView.setSecondaryMessage(secondaryMessageResId); 207 mEmptyView.setActionButtonText(actionButtonTextResId); 208 mEmptyView.setActionButtonClickListener(actionButtonOnClickListener); 209 mEmptyView.setActionButtonVisible(showActionButton); 210 switchTo(State.EMPTY); 211 } 212 213 /** 214 * Shows the content view, hides other views. 215 */ showContent()216 public void showContent() { 217 switchTo(State.CONTENT); 218 } 219 220 /** 221 * Hide all views. 222 */ reset()223 public void reset() { 224 switchTo(State.NEW); 225 } 226 switchTo(@tate int state)227 private void switchTo(@State int state) { 228 if (mState != state) { 229 L.d(TAG, "Switch to state: %d", state); 230 // Hides, or shows, all the children, including the loading and error views. 231 ViewUtils.setVisible((View) findViewById(R.id.list_view), state == State.CONTENT); 232 233 // Corrects the visibility setting for error and loading views since they are 234 // shown independently of the views content. 235 mLoadingView.setVisibilityFromState(state); 236 mErrorView.setVisibilityFromState(state); 237 mEmptyView.setVisibilityFromState(state); 238 239 mState = state; 240 } 241 } 242 243 /** 244 * Container for views held by this LoadingFrameLayout. Used for deferring view inflation until 245 * the view is about to be shown. 246 */ 247 private class ViewContainer { 248 249 @State 250 private final int mViewState; 251 private final int mLayoutId; 252 private final int mIconViewId; 253 private final int mMessageViewId; 254 private final int mSecondaryMessageViewId; 255 private final int mActionButtonId; 256 257 private View mView; 258 259 private ImageView mIconView; 260 // Cache image view resource id until imageView is inflated. 261 @DrawableRes 262 private int mIconResId; 263 264 private TextView mActionButton; 265 // Cache action button visibility until action button is inflated. 266 private boolean mIsActionButtonVisible; 267 // Cache action button onClickListener until action button is inflated. 268 private View.OnClickListener mActionButtonOnClickListener; 269 // Cache action button text until action button is inflated. 270 private int mActionButtonTextResId; 271 272 private TextView mMessageView; 273 // Cache message view string until message view is inflated. 274 private int mMessageResId; 275 private TextView mSecondaryMessageView; 276 // Cache the secondary message view string until the secondary message view is inflated. 277 private int mSecondaryMessageResId; 278 ViewContainer(@tate int state, @LayoutRes int layoutId, @IdRes int iconViewId, @IdRes int messageViewId, @IdRes int secondaryMessageViewId, @IdRes int actionButtonId)279 private ViewContainer(@State int state, @LayoutRes int layoutId, @IdRes int iconViewId, 280 @IdRes int messageViewId, @IdRes int secondaryMessageViewId, 281 @IdRes int actionButtonId) { 282 mViewState = state; 283 mLayoutId = layoutId; 284 mIconViewId = iconViewId; 285 mMessageViewId = messageViewId; 286 mSecondaryMessageViewId = secondaryMessageViewId; 287 mActionButtonId = actionButtonId; 288 } 289 inflateView()290 private View inflateView() { 291 View view = LayoutInflater.from(mContext).inflate(mLayoutId, LoadingFrameLayout.this, 292 false); 293 294 if (mMessageViewId > Constants.INVALID_RES_ID) { 295 mMessageView = view.findViewById(mMessageViewId); 296 setMessage(mMessageResId); 297 } 298 299 if (mSecondaryMessageViewId > Constants.INVALID_RES_ID) { 300 mSecondaryMessageView = view.findViewById(mSecondaryMessageViewId); 301 setSecondaryMessage(mSecondaryMessageResId); 302 } 303 304 if (mIconViewId > Constants.INVALID_RES_ID) { 305 mIconView = view.findViewById(mIconViewId); 306 setIcon(mIconResId); 307 } 308 309 if (mActionButtonId > Constants.INVALID_RES_ID) { 310 mActionButton = view.findViewById(mActionButtonId); 311 setActionButtonClickListener(mActionButtonOnClickListener); 312 setActionButtonVisible(mIsActionButtonVisible); 313 setActionButtonText(mActionButtonTextResId); 314 } 315 316 return view; 317 } 318 setVisibilityFromState(@tate int newState)319 public void setVisibilityFromState(@State int newState) { 320 if (mViewState == newState) { 321 show(); 322 } else { 323 hide(); 324 } 325 } 326 show()327 private void show() { 328 if (mView == null) { 329 mView = inflateView(); 330 LoadingFrameLayout.this.addView(mView); 331 } 332 mView.setVisibility(View.VISIBLE); 333 } 334 hide()335 private void hide() { 336 if (mView != null) { 337 mView.setVisibility(View.GONE); 338 mView.clearFocus(); 339 } 340 } 341 setMessage(@tringRes int messageResId)342 private void setMessage(@StringRes int messageResId) { 343 if (messageResId > Constants.INVALID_RES_ID) { 344 ViewUtils.setText(mMessageView, messageResId); 345 } else { 346 ViewUtils.setVisible(mMessageView, false); 347 } 348 mMessageResId = messageResId; 349 } 350 setSecondaryMessage(@tringRes int secondaryMessageResId)351 private void setSecondaryMessage(@StringRes int secondaryMessageResId) { 352 if (secondaryMessageResId > Constants.INVALID_RES_ID) { 353 ViewUtils.setText(mSecondaryMessageView, secondaryMessageResId); 354 } else { 355 ViewUtils.setVisible(mSecondaryMessageView, false); 356 } 357 mSecondaryMessageResId = secondaryMessageResId; 358 } 359 setActionButtonClickListener( View.OnClickListener actionButtonOnClickListener)360 private void setActionButtonClickListener( 361 View.OnClickListener actionButtonOnClickListener) { 362 ViewUtils.setOnClickListener(mActionButton, actionButtonOnClickListener); 363 mActionButtonOnClickListener = actionButtonOnClickListener; 364 } 365 setActionButtonText(@tringRes int actionButtonTextResId)366 private void setActionButtonText(@StringRes int actionButtonTextResId) { 367 if (actionButtonTextResId > Constants.INVALID_RES_ID) { 368 ViewUtils.setText(mActionButton, actionButtonTextResId); 369 } 370 mActionButtonTextResId = actionButtonTextResId; 371 } 372 setActionButtonVisible(boolean visible)373 private void setActionButtonVisible(boolean visible) { 374 ViewUtils.setVisible(mActionButton, visible); 375 mIsActionButtonVisible = visible; 376 } 377 setIcon(@rawableRes int iconResId)378 private void setIcon(@DrawableRes int iconResId) { 379 if (iconResId > Constants.INVALID_RES_ID) { 380 if (mIconView != null) { 381 mIconView.setImageResource(iconResId); 382 } 383 } else { 384 ViewUtils.setVisible(mIconView, false); 385 } 386 mIconResId = iconResId; 387 } 388 } 389 } 390