إنشاء معرض صور بسيط باستخدام Firebase Storage
إنشاء معرض صور خاص بك باستخدام Firebase Storage و Firebase Database
شرحنا في الدرس السابق أساسيات Firebase Storage وكيف قمنا برفع الصور وتحميلها باستخدام Firebase Storage وسنقوم بهذا الدرس بإنشاء تطبيق بسيط فكرته رفع صورك الخاصة الى Firebase Storage وعرضها في RecyclerView مع إمكانية تحميلها مرة أخرى ويمكنك عرضهم من أي جهاز تريد (بنفس فكرة Google Photos ولكن بشكل مبسط)
سنستخدم Firebase Realtime Database لتخزين مسار الصور وتاريخها ولهذا أنصحك بمتابعة درس Firebase Realtime Database
سنقوم بإنشاء مشروع جديد على أندرويد ستوديو ونقوم بإنشاء مشروع على Firebase ونقوم بربطهم ببعضهم البعض
ثم نبدأ بإضافة بعض المكتبات
Firebase Storage
implementation 'com.google.firebase:firebase-storage:11.0.4'
Firebase Database
implementation 'com.google.firebase:firebase-database:11.0.4'
RecyclerView
implementation 'com.android.support:design:26.1.0'
Picasso لعرض الصور
implementation 'com.squareup.picasso:picasso:2.5.2'
نبدأ بتعريف Firebase Storage و Firebase Database
FirebaseStorage firebaseStorage = FirebaseStorage.getInstance();
FirebaseDatabase database = FirebaseDatabase.getInstance();
نقوم بتعريف الmainRef لFirebase Storage
StorageReference mainRef = firebaseStorage.getReference();
الآن سنقوم بوضع المسار الذي سيتم حفظ صور Firebase Storage ومساراتها في Database
ملاحظة:من المستحسن استخدام Firebase Auth الذي يعطي UID لكل شخص يقوم بتسجيل الدخول ووضع الصور داخل هذا uid بدلاً من 'userUid',ولكن في سبيل التجربة لا مشكلة
userImagesRef = mainRef.child("userUid").child("images"); databaseReference = database.getReference().child("userUid").child("images");
أما الآن فسيكون xml بهذا الشكل
وعند الضغط على زر Fab سنقوم باستدعاء ميثود pickImage التي ستقوم بإنشاء Intent لفتح معرض الصور لاختيار صورة ما
private void pickImage() {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType("image/*");
startActivityForResult(photoPickerIntent, PICK_IMG_REQUEST);
}
ونستلم نتيجة الاختيار في onActivityResult كما فعلنا في الدرس السابق ونستدعي الميثود upload التي قمنا بإنشائها في الدرس السابق ولكن مع بعض التعديلات
@Override
protected void onActivityResult(int reqCode, int resultCode, Intent data) {
super.onActivityResult(reqCode, resultCode, data);
if (resultCode == RESULT_OK) {
final Uri imageUri = data.getData();
upload(imageUri);
} else {
Toast.makeText(this, "no image selected :/", Toast.LENGTH_SHORT).show();
}
}
private void upload(Uri uri) {
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.show();
final String imageName = UUID.randomUUID().toString() + ".jpg";
userImagesRef.child(imageName).putFile(uri)
.addOnProgressListener(new OnProgressListener<UploadTask.TaskSnapshot>() {
@Override
public void onProgress(UploadTask.TaskSnapshot taskSnapshot) {
int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
progressDialog.setMessage(progress + "");
}
})
.addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() {
@Override
public void onComplete(@NonNull Task<UploadTask.TaskSnapshot> task) {
progressDialog.dismiss();
if (task.isSuccessful()) {
String link = String.valueOf(task.getResult().getDownloadUrl());
String path = task.getResult().getStorage().getPath();
saveImagePathToDatabase(link, path);
Toast.makeText(MainActivity.this, "Uplaod Succeed", Toast.LENGTH_SHORT).show();
} else {
Log.d("3llomi", "upload Failed " + task.getException().getLocalizedMessage());
Toast.makeText(MainActivity.this, "Uplaod Failed :( " + task.getException().getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
}
});
}
الشيئ الجديد الذي قمنا بإضافته هو استخراج رابط الصورة في onComplete عند الإنتهاء والرابط يكون بهذا الشكل https://......image-name.jpg (سنستخدمه لاحقاً لعرض الصورة)
واستخرجنا مسار الصورة الذي يكون بهذا الشكل "/userUid/images/67d6e163-701a-41e0-b235-dcea428cefc4.jpg" (سنستخدمه لاحقاً عند تحميل الصورة)
ملاحظة: يمكنك تطبيق درس توليد الصور المصغرة عند رفع الصور | Firebase Cloud Functions لتوليد الصور المصغرة عند رفع أي صورة بدلاً من عرض الصورة كاملة واستهلاك الكثير من الموارد
الآن نقوم باستدعاء ميثود saveImagePathToDatabase ونعطي link و path كبارامترز
قبل إنشاء هذه الميثود يجب علينا إنشاء كلاس POJO او Model والذي سيكون بهذا الشكل ,وهو يحتوي على
- رابط االصورة imageLink
- مسار الصورة imagePath
- تاريخ رفع الصورة timestamp
ونقوم بإنشاء ميثود getFormattedTime التي تقوم بتحويل التاريخ من long الى تاريخ مقروء مثل 17/3/2018 وقمنا بإضافة Annotation @Exclude لمنع الFirebase من حفظ هذه الميثود عند الرفع الى قاعدة البيانات
public class Image {
private String imageLink;
private String imagePath;
private long timestamp;
public Image() {
}
public Image(String imageLink, String imagePath) {
this.imageLink = imageLink;
this.imagePath = imagePath;
timestamp = new Date().getTime();
}
public String getImageLink() {
return imageLink;
}
public void setImageLink(String imageLink) {
this.imageLink = imageLink;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
@Exclude
public String getFormattedTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
Date date = new Date();
date.setTime(timestamp);
return sdf.format(date);
}
}
نعود الآن وننشئ الميثود saveImagePathToDatabase
نقوم بإنشاء اوبجكت جديد من Image ونعطيه link و path
ثم نقوم بعمل push التي تقوم بتوليد key جديد ثم setValue لحفظ البيانات
private void saveImagePathToDatabase(String link, String path) {
Image image = new Image(link, path);
databaseReference.push().setValue(image);
}
نقوم الآن بتشغيل التطبيق ونقوم برفع صورة ما
عند نجاح عملية الرفع سنرى معلومات الصورة في قاعدة البيانات بهذا الشكل
ونذهب الى Storage لنرى الصورة
ممتاز! :D ولكن الى الآن لم نقم بعرض الصور
نبدأ بتجهيز RecyclerView Adapter (يمكنك مراجعة درس RecyclerView ) ونقوم بإنشاء ملف row_image.xml
سيكون الأدابتر بهذا الشكل
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ImageHolder> {
private List<Image> images;
private Context context;
private OnClickListener onClickListener;
public ImageAdapter(List<Image> images, Context context, OnClickListener onClickListener) {
this.images = images;
this.context = context;
this.onClickListener = onClickListener;
}
@Override
public ImageHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View row = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_image, parent, false);
return new ImageHolder(row);
}
@Override
public void onBindViewHolder(final ImageHolder holder, int position) {
Image image = images.get(position);
Picasso.with(context).load(image.getImageLink()).into(holder.imageThumbnail);
holder.tvTime.setText(image.getFormattedTime());
holder.imageThumbnail.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onClickListener != null)
onClickListener.onClick(/* do NOT PASS position from the Params because it will be final! ,use get adapter position instead*/holder.getAdapterPosition());
}
});
}
@Override
public int getItemCount() {
return images.size();
}
class ImageHolder extends RecyclerView.ViewHolder {
private ImageView imageThumbnail;
private TextView tvTime;
public ImageHolder(View itemView) {
super(itemView);
imageThumbnail = itemView.findViewById(R.id.image_thumbnail);
tvTime = itemView.findViewById(R.id.tv_time);
}
}
public interface OnClickListener {
void onClick(int index);
}
}
ملاحظة:قمنا باستخدام interface عند الضغط على الصورة سنقوم بتحميلها من الأكتفتي
وrow_image.xml بهذا الشكل
بما أننا نريد عرض الصور على شكل شبكي Grid فسنقوم بإنشاء كلاس الذي سيقوم بجعل المسافات بين العنصر والآخر متساوية
public class GridItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
public GridItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.bottom = spacing; // item bottom
}
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}
نعود الآن الى MainActivity لنجهز الأدابتر
سنقوم بإنشاء List<Image> لنضيف اليها العناصر ونجعلها global ونعطيها ل adapter
List<Image> images = new ArrayList<>();
عند الضغط على الصورة سنقوم بتحميلها عبر الميثود download التي سنقوم بإنشاءها لاحقاً
ووضعنا 3 عناصر في كل صف في RecyclerView عبر SPAN_COUNT بمسافة 4px SPACING عبر itemDceoration الذي أنشأناه
adapter = new ImageAdapter(images, this, new ImageAdapter.OnClickListener() {
@Override
public void onClick(int index) {
Image image = images.get(index);
download(image.getImagePath());
}
});
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, SPAN_COUNT);
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.addItemDecoration(new GridItemDecoration(SPAN_COUNT, SPACING, true));
recyclerView.setAdapter(adapter);
تم تجهيز الأدابتر ولكن اذا شغلت التطبيق فلن تجد أي بيانات لأنه لم نقم بإضافتها
ولهذا سنقوم بإضافة addChildEventListener والتي سيتم استدعاؤها عندما يتم إضافة اي عنصر الى Firebase Database الى المسار userUid/images
في onChildAdded قمنا بعمل Cast ل Snapshot الصورة القادمة من قاعدة البيانات وقمنا بإضافتها الى الليست Images
وأخيراً نجعل الأدابتر يقوم بتحديث البيانات عبر adapter.notifyDataSetChanged();
databaseReference.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Image image = dataSnapshot.getValue(Image.class);
images.add(image);
adapter.notifyDataSetChanged();
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
adapter.notifyDataSetChanged();
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
أخيراً نقوم بتشغيل التطبيق لنرى النتيجة ونقوم بإضافة بعض الصور
الآن عند الضغط على الصورة سنقوم بتحميل هذه الصورة,سنقوم بتحميلها الى مجلد التطبيق نفسه وبالتالي لا داعي لوجود الصلاحيات WRITE_EXTERNAL_STORAGE
ولهذا نقوم بإنشاء نفس الميثود التي قمنا بإنشاءها في الدرس السابق download
private void download(String imagePath) {
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Downloading...");
progressDialog.show();
String localFileName = UUID.randomUUID().toString() + ".jpg";
final File file = new File(getFilesDir(), localFileName);
mainRef.child(imagePath).getFile(file)
.addOnCompleteListener(new OnCompleteListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onComplete(@NonNull Task<FileDownloadTask.TaskSnapshot> task) {
progressDialog.dismiss();
if (task.isSuccessful()) {
Toast.makeText(MainActivity.this, "file Downloaded to " + file.getPath(), Toast.LENGTH_SHORT).show();
Log.d("3llomi", "File Downloaded to " + file.getPath());
} else {
Toast.makeText(MainActivity.this, "download Failed " + task.getException().getLocalizedMessage(), Toast.LENGTH_SHORT).show();
Log.d("3llomi", "Download Failed " + task.getException().getLocalizedMessage());
}
}
}).addOnProgressListener(new OnProgressListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onProgress(FileDownloadTask.TaskSnapshot taskSnapshot) {
int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
progressDialog.setMessage(progress + "%");
}
});
}
ونقوم باستدعاءها في adapter onClick
adapter = new ImageAdapter(images, this, new ImageAdapter.OnClickListener() {
@Override
public void onClick(int index) {
Image image = images.get(index);
download(image.getImagePath());
}
});
ليصبح كلاس MainActivity كاملاً كالتالي
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private static final int SPAN_COUNT = 3;
private static final int SPACING = 4;
private static final int PICK_IMG_REQUEST = 7588;
FirebaseStorage firebaseStorage;
StorageReference mainRef, userImagesRef;
FirebaseDatabase database;
DatabaseReference databaseReference;
ImageAdapter adapter;
List<Image> images = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
recyclerView = findViewById(R.id.recycler_view);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pickImage();
}
});
adapter = new ImageAdapter(images, this, new ImageAdapter.OnClickListener() {
@Override
public void onClick(int index) {
Image image = images.get(index);
download(image.getImagePath());
}
});
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, SPAN_COUNT);
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.addItemDecoration(new GridItemDecoration(SPAN_COUNT, SPACING, true));
recyclerView.setAdapter(adapter);
firebaseStorage = FirebaseStorage.getInstance();
database = FirebaseDatabase.getInstance();
mainRef = firebaseStorage.getReference();
userImagesRef = mainRef.child("userUid").child("images");
databaseReference = database.getReference().child("userUid").child("images");
databaseReference.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Image image = dataSnapshot.getValue(Image.class);
images.add(image);
adapter.notifyDataSetChanged();
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
adapter.notifyDataSetChanged();
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
private void pickImage() {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType("image/*");
startActivityForResult(photoPickerIntent, PICK_IMG_REQUEST);
}
@Override
protected void onActivityResult(int reqCode, int resultCode, Intent data) {
super.onActivityResult(reqCode, resultCode, data);
if (resultCode == RESULT_OK) {
final Uri imageUri = data.getData();
upload(imageUri);
} else {
Toast.makeText(this, "no image selected :/", Toast.LENGTH_SHORT).show();
}
}
private void upload(Uri uri) {
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.show();
final String imageName = UUID.randomUUID().toString() + ".jpg";
userImagesRef.child(imageName).putFile(uri)
.addOnProgressListener(new OnProgressListener<UploadTask.TaskSnapshot>() {
@Override
public void onProgress(UploadTask.TaskSnapshot taskSnapshot) {
int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
progressDialog.setMessage(progress + "");
}
})
.addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() {
@Override
public void onComplete(@NonNull Task<UploadTask.TaskSnapshot> task) {
progressDialog.dismiss();
if (task.isSuccessful()) {
String link = String.valueOf(task.getResult().getDownloadUrl());
String path = task.getResult().getStorage().getPath();
saveImagePathToDatabase(link, path);
Toast.makeText(MainActivity.this, "Uplaod Succeed", Toast.LENGTH_SHORT).show();
} else {
Log.d("3llomi", "upload Failed " + task.getException().getLocalizedMessage());
Toast.makeText(MainActivity.this, "Uplaod Failed :( " + task.getException().getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
}
});
}
private void saveImagePathToDatabase(String link, String path) {
Image image = new Image(link, path);
databaseReference.push().setValue(image);
}
private void download(String imagePath) {
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Downloading...");
progressDialog.show();
String localFileName = UUID.randomUUID().toString() + ".jpg";
final File file = new File(getFilesDir(), localFileName);
mainRef.child(imagePath).getFile(file)
.addOnCompleteListener(new OnCompleteListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onComplete(@NonNull Task<FileDownloadTask.TaskSnapshot> task) {
progressDialog.dismiss();
if (task.isSuccessful()) {
Toast.makeText(MainActivity.this, "file Downloaded to " + file.getPath(), Toast.LENGTH_SHORT).show();
Log.d("3llomi", "File Downloaded to " + file.getPath());
} else {
Toast.makeText(MainActivity.this, "download Failed " + task.getException().getLocalizedMessage(), Toast.LENGTH_SHORT).show();
Log.d("3llomi", "Download Failed " + task.getException().getLocalizedMessage());
}
}
}).addOnProgressListener(new OnProgressListener<FileDownloadTask.TaskSnapshot>() {
@Override
public void onProgress(FileDownloadTask.TaskSnapshot taskSnapshot) {
int progress = (int) ((100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount());
progressDialog.setMessage(progress + "%");
}
});
}
}
المشروع كاملاً على Github
ملاحظة:المشروع على Github للمعاينة فقط ولايمكنك تجربته على Android Studio لعدم وجود google-services.json الخاص بك
التعليقات (1)
لدي سؤالين ..
سؤالي الاول ...
في الfirebase الimage link تحفظ كـ com.google.android.gms.tasks....etc وليس ارتباط http وهذا يسبب فشل اثناء الretrive قرأت ان هذا بسبب التحديث الجديد للfirebase
بحثت كثير بخصوص هذي المشكله ولم اجد حل : كيف يمكن حل الشكله واسترجاع الصوره من storge .
سؤالي الثاني ..
هناك gridview جاهزه للتعامل معها لماذا فضلت انشاء gridview من جديد؟؟
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !