| /* |
| * Copyright (C) 2019 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.car.dialer.widget; |
| |
| import android.annotation.StringRes; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.util.AttributeSet; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.IdRes; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.LayoutRes; |
| import androidx.annotation.MainThread; |
| import androidx.annotation.Nullable; |
| |
| import com.android.car.apps.common.util.ViewUtils; |
| import com.android.car.dialer.Constants; |
| import com.android.car.dialer.R; |
| import com.android.car.dialer.log.L; |
| |
| /** |
| * A widget that supports different {@link State}s: NEW, LOADING, CONTENT, EMPTY OR ERROR. |
| */ |
| public class LoadingFrameLayout extends FrameLayout { |
| private static final String TAG = "CD.LoadingFrameLayout"; |
| |
| /** |
| * Possible states of a service request display. |
| */ |
| @IntDef({State.NEW, State.LOADING, State.CONTENT, State.ERROR, State.EMPTY}) |
| public @interface State { |
| int NEW = 0; |
| int LOADING = 1; |
| int CONTENT = 2; |
| int ERROR = 3; |
| int EMPTY = 4; |
| } |
| |
| private final Context mContext; |
| |
| private ViewContainer mEmptyView; |
| private ViewContainer mLoadingView; |
| private ViewContainer mErrorView; |
| |
| @State |
| private int mState = State.NEW; |
| |
| public LoadingFrameLayout(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public LoadingFrameLayout(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| mContext = context; |
| TypedArray values = |
| context.obtainStyledAttributes(attrs, R.styleable.LoadingFrameLayout, defStyle, 0); |
| setLoadingView( |
| values.getResourceId( |
| R.styleable.LoadingFrameLayout_progressViewLayout, |
| R.layout.loading_progress_view)); |
| setEmptyView( |
| values.getResourceId( |
| R.styleable.LoadingFrameLayout_emptyViewLayout, |
| R.layout.loading_info_view)); |
| setErrorView( |
| values.getResourceId( |
| R.styleable.LoadingFrameLayout_errorViewLayout, |
| R.layout.loading_info_view)); |
| values.recycle(); |
| } |
| |
| @Override |
| public void onFinishInflate() { |
| super.onFinishInflate(); |
| // Start with a loading view when inflated from XML. |
| showLoading(); |
| } |
| |
| private void setLoadingView(int loadingLayoutId) { |
| mLoadingView = new ViewContainer(State.LOADING, loadingLayoutId, 0, 0, 0, 0); |
| } |
| |
| private void setEmptyView(int emptyLayoutId) { |
| mEmptyView = new ViewContainer(State.EMPTY, emptyLayoutId, R.id.loading_info_icon, |
| R.id.loading_info_message, R.id.loading_info_secondary_message, |
| R.id.loading_info_action_button); |
| } |
| |
| private void setErrorView(int errorLayoutId) { |
| mErrorView = new ViewContainer(State.ERROR, errorLayoutId, R.id.loading_info_icon, |
| R.id.loading_info_message, R.id.loading_info_secondary_message, |
| R.id.loading_info_action_button); |
| } |
| |
| /** |
| * Shows the loading view, hides other views. |
| */ |
| @MainThread |
| public void showLoading() { |
| switchTo(State.LOADING); |
| } |
| |
| /** |
| * Shows the error view where the action button is not available and hides other views. |
| * |
| * @param iconResId drawable resource id used for the top icon. When it is invalid, |
| * hide the icon view. |
| * @param messageResId string resource id used for the error message. When it is |
| * invalid, hide the message view. |
| * @param secondaryMessageResId string resource id for the secondary error message. When it is |
| * invalid, hide the secondary message view. |
| */ |
| public void showError(@DrawableRes int iconResId, @StringRes int messageResId, |
| @StringRes int secondaryMessageResId) { |
| showError(iconResId, messageResId, secondaryMessageResId, Constants.INVALID_RES_ID, null, |
| false); |
| } |
| |
| /** |
| * Shows the error view, hides other views. |
| * |
| * @param iconResId drawable resource id used for the top icon.When it is |
| * invalid, hide the icon view. |
| * @param messageResId string resource id used for the error message. When it is |
| * invalid, hide the message view. |
| * @param secondaryMessageResId string resource id for the secondary error message. When |
| * it is invalid, hide the secondary message view. |
| * @param actionButtonTextResId string resource id for the action button. |
| * @param actionButtonOnClickListener click listener set on the action button. |
| * @param showActionButton boolean flag if the action button will show. |
| */ |
| public void showError( |
| @DrawableRes int iconResId, |
| @StringRes int messageResId, |
| @StringRes int secondaryMessageResId, |
| @StringRes int actionButtonTextResId, |
| View.OnClickListener actionButtonOnClickListener, |
| boolean showActionButton) { |
| mErrorView.setIcon(iconResId); |
| mErrorView.setMessage(messageResId); |
| mErrorView.setSecondaryMessage(secondaryMessageResId); |
| mErrorView.setActionButtonText(actionButtonTextResId); |
| mErrorView.setActionButtonClickListener(actionButtonOnClickListener); |
| mErrorView.setActionButtonVisible(showActionButton); |
| switchTo(State.ERROR); |
| } |
| |
| /** |
| * Shows the empty view where the action button is not available and hides other views. |
| * |
| * @param iconResId drawable resource id used for the top icon. When it is invalid, |
| * hide the icon view. |
| * @param messageResId string resource id used for the empty message. When it is |
| * invalid, hide the message view. |
| * @param secondaryMessageResId string resource id for the secondary empty message. When it is |
| * invalid, hide the secondary message view. |
| */ |
| public void showEmpty(@DrawableRes int iconResId, @StringRes int messageResId, |
| @StringRes int secondaryMessageResId) { |
| showEmpty(iconResId, messageResId, secondaryMessageResId, Constants.INVALID_RES_ID, null, |
| false); |
| } |
| |
| /** |
| * Shows the empty view and hides other views. |
| * |
| * @param iconResId drawable resource id used for the top icon.When it is |
| * invalid, hide the icon view. |
| * @param messageResId string resource id used for the empty message. When it is |
| * invalid, hide the message view. |
| * @param secondaryMessageResId string resource id for the secondary empty message. When |
| * it is invalid, hide the secondary message view. |
| * @param actionButtonTextResId string resource id for the action button. |
| * @param actionButtonOnClickListener click listener set on the action button. |
| * @param showActionButton boolean flag if the action button will show. |
| */ |
| public void showEmpty( |
| @DrawableRes int iconResId, |
| @StringRes int messageResId, |
| @StringRes int secondaryMessageResId, |
| @StringRes int actionButtonTextResId, |
| @Nullable View.OnClickListener actionButtonOnClickListener, |
| boolean showActionButton) { |
| mEmptyView.setIcon(iconResId); |
| mEmptyView.setMessage(messageResId); |
| mEmptyView.setSecondaryMessage(secondaryMessageResId); |
| mEmptyView.setActionButtonText(actionButtonTextResId); |
| mEmptyView.setActionButtonClickListener(actionButtonOnClickListener); |
| mEmptyView.setActionButtonVisible(showActionButton); |
| switchTo(State.EMPTY); |
| } |
| |
| /** |
| * Shows the content view, hides other views. |
| */ |
| public void showContent() { |
| switchTo(State.CONTENT); |
| } |
| |
| /** |
| * Hide all views. |
| */ |
| public void reset() { |
| switchTo(State.NEW); |
| } |
| |
| private void switchTo(@State int state) { |
| if (mState != state) { |
| L.d(TAG, "Switch to state: %d", state); |
| // Hides, or shows, all the children, including the loading and error views. |
| ViewUtils.setVisible((View) findViewById(R.id.list_view), state == State.CONTENT); |
| |
| // Corrects the visibility setting for error and loading views since they are |
| // shown independently of the views content. |
| mLoadingView.setVisibilityFromState(state); |
| mErrorView.setVisibilityFromState(state); |
| mEmptyView.setVisibilityFromState(state); |
| |
| mState = state; |
| } |
| } |
| |
| /** |
| * Container for views held by this LoadingFrameLayout. Used for deferring view inflation until |
| * the view is about to be shown. |
| */ |
| private class ViewContainer { |
| |
| @State |
| private final int mViewState; |
| private final int mLayoutId; |
| private final int mIconViewId; |
| private final int mMessageViewId; |
| private final int mSecondaryMessageViewId; |
| private final int mActionButtonId; |
| |
| private View mView; |
| |
| private ImageView mIconView; |
| // Cache image view resource id until imageView is inflated. |
| @DrawableRes |
| private int mIconResId; |
| |
| private TextView mActionButton; |
| // Cache action button visibility until action button is inflated. |
| private boolean mIsActionButtonVisible; |
| // Cache action button onClickListener until action button is inflated. |
| private View.OnClickListener mActionButtonOnClickListener; |
| // Cache action button text until action button is inflated. |
| private int mActionButtonTextResId; |
| |
| private TextView mMessageView; |
| // Cache message view string until message view is inflated. |
| private int mMessageResId; |
| private TextView mSecondaryMessageView; |
| // Cache the secondary message view string until the secondary message view is inflated. |
| private int mSecondaryMessageResId; |
| |
| private ViewContainer(@State int state, @LayoutRes int layoutId, @IdRes int iconViewId, |
| @IdRes int messageViewId, @IdRes int secondaryMessageViewId, |
| @IdRes int actionButtonId) { |
| mViewState = state; |
| mLayoutId = layoutId; |
| mIconViewId = iconViewId; |
| mMessageViewId = messageViewId; |
| mSecondaryMessageViewId = secondaryMessageViewId; |
| mActionButtonId = actionButtonId; |
| } |
| |
| private View inflateView() { |
| View view = LayoutInflater.from(mContext).inflate(mLayoutId, LoadingFrameLayout.this, |
| false); |
| |
| if (mMessageViewId > Constants.INVALID_RES_ID) { |
| mMessageView = view.findViewById(mMessageViewId); |
| setMessage(mMessageResId); |
| } |
| |
| if (mSecondaryMessageViewId > Constants.INVALID_RES_ID) { |
| mSecondaryMessageView = view.findViewById(mSecondaryMessageViewId); |
| setSecondaryMessage(mSecondaryMessageResId); |
| } |
| |
| if (mIconViewId > Constants.INVALID_RES_ID) { |
| mIconView = view.findViewById(mIconViewId); |
| setIcon(mIconResId); |
| } |
| |
| if (mActionButtonId > Constants.INVALID_RES_ID) { |
| mActionButton = view.findViewById(mActionButtonId); |
| setActionButtonClickListener(mActionButtonOnClickListener); |
| setActionButtonVisible(mIsActionButtonVisible); |
| setActionButtonText(mActionButtonTextResId); |
| } |
| |
| return view; |
| } |
| |
| public void setVisibilityFromState(@State int newState) { |
| if (mViewState == newState) { |
| show(); |
| } else { |
| hide(); |
| } |
| } |
| |
| private void show() { |
| if (mView == null) { |
| mView = inflateView(); |
| LoadingFrameLayout.this.addView(mView); |
| } |
| mView.setVisibility(View.VISIBLE); |
| } |
| |
| private void hide() { |
| if (mView != null) { |
| mView.setVisibility(View.GONE); |
| mView.clearFocus(); |
| } |
| } |
| |
| private void setMessage(@StringRes int messageResId) { |
| if (messageResId > Constants.INVALID_RES_ID) { |
| ViewUtils.setText(mMessageView, messageResId); |
| } else { |
| ViewUtils.setVisible(mMessageView, false); |
| } |
| mMessageResId = messageResId; |
| } |
| |
| private void setSecondaryMessage(@StringRes int secondaryMessageResId) { |
| if (secondaryMessageResId > Constants.INVALID_RES_ID) { |
| ViewUtils.setText(mSecondaryMessageView, secondaryMessageResId); |
| } else { |
| ViewUtils.setVisible(mSecondaryMessageView, false); |
| } |
| mSecondaryMessageResId = secondaryMessageResId; |
| } |
| |
| private void setActionButtonClickListener( |
| View.OnClickListener actionButtonOnClickListener) { |
| ViewUtils.setOnClickListener(mActionButton, actionButtonOnClickListener); |
| mActionButtonOnClickListener = actionButtonOnClickListener; |
| } |
| |
| private void setActionButtonText(@StringRes int actionButtonTextResId) { |
| if (actionButtonTextResId > Constants.INVALID_RES_ID) { |
| ViewUtils.setText(mActionButton, actionButtonTextResId); |
| } |
| mActionButtonTextResId = actionButtonTextResId; |
| } |
| |
| private void setActionButtonVisible(boolean visible) { |
| ViewUtils.setVisible(mActionButton, visible); |
| mIsActionButtonVisible = visible; |
| } |
| |
| private void setIcon(@DrawableRes int iconResId) { |
| if (iconResId > Constants.INVALID_RES_ID) { |
| if (mIconView != null) { |
| mIconView.setImageResource(iconResId); |
| } |
| } else { |
| ViewUtils.setVisible(mIconView, false); |
| } |
| mIconResId = iconResId; |
| } |
| } |
| } |