Android 如何创建使用Firebase图像的RecyclerView
我的Firebase存储中名为Icons的文件夹中有超过1.5K个图像,我想将这些图像加载到使用GridLayoutManager的RecyclerView中。显然,我只想请求加载这些图像,如果它们在Recyclerview中可见,否则这是对存储的读取的浪费,对吗 因此,我首先创建R.layout.card_icons_rcv_item xml布局,它在ConstraintLayout中包含一个ImageView,以表示我的RecyclerView中的每个单元格 然后,我创建了适配器类,您可以在下面和onBindViewHolder内部看到,我正在尝试加载与相应RecyclerView单元格中的每个单元格关联的图像,但效果不好 首先,我只得到大约2张图像,或者大部分时间没有加载任何其他图像,其次,我可以看到Log.dAdapter,从firestore加载卡+位置的错误卡图标;语句被调用用于最后3个单元格,最后我在logcat中看到,所有这些对firebase的请求都是同时发出的,即使图像不在recyclerView中 更新 Recycler视图似乎成功加载了图标,但速度非常慢。所以我在文档里翻了翻,偶然发现了位图。文档说,由于我的图像视图比Firebase存储上的图像要小,我可能应该对它们进行下采样。正如您在我更新的onBindViewHolder方法中所看到的,我就是这样做的。图像加载速度明显加快,但仍然不够快。我还遇到了一个奇怪的异常,我似乎无法修复。我正在更新下面的问题: [更新]以下是我的问题: 我如何仅请求从firebase加载可见单元格的图像,因为从我现在在Logcat中看到的情况来看,所做的请求等于getItemCount返回的数字。 在getItemCount方法中,我返回一个随机数50,因为我不知道如何返回数据库中的图像数。那里至少有1.5K个图像,数据库将慢慢变大。这是否会引起任何问题?如果是,我该如何克服? 如何使这些图像的下载速度更快? 下载onBindViewHolder方法中看到的临时文件中的图像后,我应该删除它们吗?还是填充recycler视图的图像视图需要它们? 在几次运行之后,没有对代码进行任何更改,我开始在Logcat中出现以下错误: E/AuthUI:发生登录错误。 com.firebase.ui.auth.FirebaseUiException:保存凭据时出错。 位于com.firebase.ui.auth.viewmodel.smartlock.SmartLockHandler$1.onCompleteSmartLockHandler.java:99 位于com.google.android.gms.tasks.zzj.run未知来源:4 位于android.os.Handler.handleCallbackHandler.java:883 在android.os.Handler.dispatchMessageHandler.java:100 在android.os.Looper.Looper.java:221 在android.app.ActivityThread.mainActivityThread.java:7520 在java.lang.reflect.Method.Invokenactive方法中 位于com.android.internal.os.RuntimeInit$MethodAndArgsCaller.RuntimeInit.java:539 位于com.android.internal.os.ZygoteInit.mainZygoteInit.java:950 原因:com.google.android.gms.common.api.ApiException:16:当前应用程序的保存提示被禁用。要恢复,请删除 此应用程序来自智能密码锁中的从不保存列表 此设备上所有帐户的设置。 在com.google.android.gms.common.internal.apieexceptionutil.fromStatuscom.google.android.gms:play services base@@17.1.0:4 在com.google.android.gms.common.internal.zai.zafcom.google.android.gms:play services base@@17.1.0:2 在com.google.android.gms.common.internal.zak.onCompletecom.google.android.gms:play services base@@17.1.0:6 位于com.google.android.gms.common.api.internal.basependingreult.zaacom.google.android.gms:play services base@@17.1.0:176 位于com.google.android.gms.common.api.internal.basependingreult.setResultcom.google.android.gms:play services base@@17.1.0:135 在com.google.android.gms.common.api.internal.BaseImplementation$ApiMethodImpl.setResultcom.google.android.gms:play services base@@17.1.0:36 位于com.google.android.gms.internal.auth-api.zzo.zzc已知来源:4 位于com.google.android.gms.internal.auth-api.zzv.dispatchTransactionUnknown 资料来源:9 位于com.google.android.gms.internal.auth-api.zzd.onTransactUnknown 资料来源:12 在android.os.Binder.execTransactionInternalBinder.java:1021 在android.os.Binder.execTransactionBinder.java:994 我试图以所有者身份登录的用户已根据我的Log.d语句成功通过身份验证,但由于某些奇怪的原因,它不允许我从中读取数据 火基。这个错误提到我应该在Google SmartLock中更改一个设置,但我向上帝发誓,我已经在手机上到处寻找该设置,但我找不到它。我也在谷歌上搜索过,但运气不好。你知道我在哪里可以找到那个场景吗 CardIconRCVAdapter.java:Android 如何创建使用Firebase图像的RecyclerView,android,firebase,android-recyclerview,firebase-storage,Android,Firebase,Android Recyclerview,Firebase Storage,我的Firebase存储中名为Icons的文件夹中有超过1.5K个图像,我想将这些图像加载到使用GridLayoutManager的RecyclerView中。显然,我只想请求加载这些图像,如果它们在Recyclerview中可见,否则这是对存储的读取的浪费,对吗 因此,我首先创建R.layout.card_icons_rcv_item xml布局,它在ConstraintLayout中包含一个ImageView,以表示我的RecyclerView中的每个单元格 然后,我创建了适配器类,您可以在
public class CardIconsRCVAdapter extends RecyclerView.Adapter<CardIconsRCVAdapter.ViewHolder> {
private LayoutInflater inflater;
private Context mContext;
private StorageReference storageReference;
public CardIconsRCVAdapter(Context context,StorageReference storageRef) {
mContext = context;
inflater = LayoutInflater.from(context);
storageReference = storageRef;
}
// stores and recycles views as they are scrolled off screen
class ViewHolder extends RecyclerView.ViewHolder {
ImageView iconImgView;
ViewHolder(@NonNull View itemView) {
super(itemView);
iconImgView = itemView.findViewById(R.id.cardIcon);
}
}
// Inflates the cell layout from xml when needed
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.card_icons_rcv_item,parent,false);
return new ViewHolder(view);
}
// Binds the data to the views in each cell
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
//Log.d("Adapter","OnBindViewHolder called for #" + position);
StorageReference iconRefJpeg = storageReference.child("Icons/icon" + position + ".jpeg");
final StorageReference iconRefPng = storageReference.child("Icons/icon" + position + ".png");
try {
final File localFile = File.createTempFile("icon" + position, ".jpeg");
iconRefJpeg.getFile(localFile).addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
holder.iconImgView.setImageBitmap(decodeSampledBitmapFromResource(localFile.getAbsolutePath(), 150, 150));
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.d("AdapterError", "Failed fetching .jpeg image. Trying to fetch .png");
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
private static Bitmap decodeSampledBitmapFromResource(String filePath, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath,options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filePath,options);
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d("CalcSampleSize","height = " + height + " width = " + width + " Image type : " + options.outMimeType);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
@Override
public int getItemCount() {
return 50;
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.Toolbar
android:id="@+id/mainToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:background="@color/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/toolbar_title"
android:text="@string/app_name"
android:textSize="18sp"
android:textColor="@android:color/white"
android:layout_gravity="center"/>
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cardIconsRCV"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mainToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
在onActivityResult中,我只需创建一个从当前活动到我的主活动的新意图,如果登录成功,则返回RecyclerView,或者重新启动当前活动,直到用户登录
问题1和2
我建议您将所有从FirebaseStorage下载的图像URL列表存储在一个列表中,例如。消防商店。然后,您可以使用Android架构组件中的分页库逐页请求图像URL列表,并将其提交给回收商的适配器。如果您将URL存储在FireStore中,您可以在查询中使用startFrom和endAt方法来帮助分页。这也有助于解决您不知道正在加载到RecyclerView的项目数量的问题。你可以阅读更多关于
问题3和4
关于将图像下载到ImageView,有一些很好的图像加载库,您可以使用各种选项将图像缓存到磁盘上。一些流行的例子是,等等。它们可以节省您的时间,并帮助您更加专注于实现应用程序逻辑
问题5
从FirebaseUI github自述文件
默认情况下,FirebaseUI使用智能密码锁存储用户凭据,并在后续尝试时自动将用户登录到您的应用程序中。建议使用智能锁以提供最佳用户体验,但在某些情况下,您可能希望禁用智能锁以进行测试或开发
在AuthUI生成器中,您可能希望将setIsSmartLockEnabledtrue更改为setIsSmartLockEnabled!BuildConfig.DEBUG/*凭据*/,true/*提示*/或使用setIsSmartLockEnabledfalse将其完全关闭。您可以在此处了解更多信息我建议您使用类似的图像加载库,或者只需将URL中的图像加载到您的图像视图中。非常感谢您的回复,我甚至不知道这个库存在。我会做些调查,然后再打给你。顺便问一下,你知道我第五个问题中的错误吗?对于第五个问题,你能用你如何登录一个用户来更新这个问题吗?刚刚在我的帖子上添加了相应的代码。在帖子的底部:答案已经更新了。我还建议不要在使用AuthUI登录失败时重新启动整个活动,您可以向用户显示Toast或Snackbar之类的内容,然后重新启动AuthUI实例。您可以将startActivityForResult块移动到方法startAuthUI中,并在onCreate中调用该方法,然后调用将启动AuthUI意图的方法当AuthUI返回失败的登录响应时,用户再次尝试登录,因为重新启动活动的目的是重新启动AuthUI。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.Toolbar
android:id="@+id/mainToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:background="@color/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/toolbar_title"
android:text="@string/app_name"
android:textSize="18sp"
android:textColor="@android:color/white"
android:layout_gravity="center"/>
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cardIconsRCV"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mainToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/cardIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="cardIcon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
class UserAuthenticationActivity : AppCompatActivity() {
private val TAG = UserAuthenticationActivity::class.java.simpleName
private val RC_SIGN_IN: Int = 101 // A request code used to authenticate users
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Choose authentication providers
val providers = arrayListOf(
AuthUI.IdpConfig.EmailBuilder().build(),
AuthUI.IdpConfig.GoogleBuilder().build()
)
// Create and launch sign-in intent
startActivityForResult(
AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.setIsSmartLockEnabled(true)
.setLogo(R.drawable.applogotmp) // Set logo drawable-temp
.build(),
RC_SIGN_IN)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_SIGN_IN) {
val response = IdpResponse.fromResultIntent(data)
if (resultCode == Activity.RESULT_OK) {
// Successfully signed in
// Consider passing the user object to the MainActivity using a bundle
val user = FirebaseAuth.getInstance().currentUser
user?.let {
// Name, email address, and profile photo Url
val name = user.displayName
val email = user.email
Log.d(TAG,"User $user just logged in. Name $name and email $email")
}
startActivity(Intent(this, MainActivity::class.java)) // User was authenticated, allow app access
finish()
} else { // Sign in failed
if(response == null) { // If response is null the user canceled the sign-in flow using the back button.
startActivity(Intent(this,
UserAuthenticationActivity::class.java))// restart the activity/prompt for auth again
finish()
} else {// Otherwise check response.getError().getErrorCode() and handle the error.
Log.d(TAG,"Error logging in user. Reason : ${response.error?.errorCode}")
}
}
}
}
}