blob: 0f21a478855fc7546f46febac6f881aea7d38f0d [file] [log] [blame]
/*
* 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;
}
}
}