fix media_selector

androidx
Ztiany 5 years ago
parent f95865d703
commit 35d7360f08
  1. 4
      lib_media_selector/README.md
  2. 24
      lib_media_selector/build.gradle
  3. 32
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/BoxingResHelper.java
  4. 89
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/WindowManagerHelper.java
  5. 158
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/adapter/BoxingAlbumAdapter.java
  6. 212
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/adapter/BoxingMediaAdapter.java
  7. 93
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingActivity.java
  8. 36
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingBaseFragment.java
  9. 142
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingBottomSheetActivity.java
  10. 249
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingBottomSheetFragment.java
  11. 191
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingRawImageFragment.java
  12. 333
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingViewActivity.java
  13. 526
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/ui/BoxingViewFragment.java
  14. 34
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/view/HackyGridLayoutManager.java
  15. 67
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/view/HackyViewPager.java
  16. 162
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/view/MediaItemLayout.java
  17. 103
      lib_media_selector/src/boxing-impl/java/com/bilibili/boxing_impl/view/SpacesItemDecoration.java
  18. 27
      lib_media_selector/src/boxing-impl/res/anim/boxing_fade_in.xml
  19. 27
      lib_media_selector/src/boxing-impl/res/anim/boxing_fade_out.xml
  20. BIN
      lib_media_selector/src/boxing-impl/res/drawable-hdpi/ic_boxing_broken_image.png
  21. BIN
      lib_media_selector/src/boxing-impl/res/drawable-hdpi/ic_boxing_check_black.png
  22. BIN
      lib_media_selector/src/boxing-impl/res/drawable-hdpi/ic_boxing_checked.png
  23. BIN
      lib_media_selector/src/boxing-impl/res/drawable-xhdpi/ic_boxing_broken_image.png
  24. BIN
      lib_media_selector/src/boxing-impl/res/drawable-xhdpi/ic_boxing_check_black.png
  25. BIN
      lib_media_selector/src/boxing-impl/res/drawable-xhdpi/ic_boxing_checked.png
  26. BIN
      lib_media_selector/src/boxing-impl/res/drawable-xxhdpi/ic_boxing_broken_image.png
  27. BIN
      lib_media_selector/src/boxing-impl/res/drawable-xxhdpi/ic_boxing_check_black.png
  28. BIN
      lib_media_selector/src/boxing-impl/res/drawable-xxhdpi/ic_boxing_checked.png
  29. 39
      lib_media_selector/src/boxing-impl/res/drawable/selector_boxing_btn_solid.xml
  30. 27
      lib_media_selector/src/boxing-impl/res/drawable/shape_boxing_popup_background.xml
  31. 33
      lib_media_selector/src/boxing-impl/res/drawable/shape_boxing_unchecked.xml
  32. 50
      lib_media_selector/src/boxing-impl/res/layout/activity_boxing.xml
  33. 50
      lib_media_selector/src/boxing-impl/res/layout/activity_boxing_bottom_sheet.xml
  34. 83
      lib_media_selector/src/boxing-impl/res/layout/activity_boxing_view.xml
  35. 78
      lib_media_selector/src/boxing-impl/res/layout/fragmant_boxing_view.xml
  36. 72
      lib_media_selector/src/boxing-impl/res/layout/fragment_boxing_bottom_sheet.xml
  37. 37
      lib_media_selector/src/boxing-impl/res/layout/fragment_boxing_raw_image.xml
  38. 38
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_album.xml
  39. 71
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_album_item.xml
  40. 26
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_app_bar.xml
  41. 32
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_empty_txt.xml
  42. 83
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_media_item.xml
  43. 35
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_recycleview_header.xml
  44. 23
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_recycleview_item.xml
  45. 25
      lib_media_selector/src/boxing-impl/res/layout/layout_boxing_simple_media_item.xml
  46. 27
      lib_media_selector/src/boxing-impl/res/menu/activity_boxing_image_viewer.xml
  47. 48
      lib_media_selector/src/boxing-impl/res/values-en/strings.xml
  48. 37
      lib_media_selector/src/boxing-impl/res/values/colors.xml
  49. 32
      lib_media_selector/src/boxing-impl/res/values/dimens.xml
  50. 52
      lib_media_selector/src/boxing-impl/res/values/strings.xml
  51. 50
      lib_media_selector/src/boxing-impl/res/values/styles.xml
  52. 67
      lib_media_selector/src/boxing/java/com/bilibili/boxing/AbsBoxingActivity.java
  53. 241
      lib_media_selector/src/boxing/java/com/bilibili/boxing/AbsBoxingViewActivity.java
  54. 485
      lib_media_selector/src/boxing/java/com/bilibili/boxing/AbsBoxingViewFragment.java
  55. 255
      lib_media_selector/src/boxing/java/com/bilibili/boxing/Boxing.java
  56. 74
      lib_media_selector/src/boxing/java/com/bilibili/boxing/BoxingCrop.java
  57. 67
      lib_media_selector/src/boxing/java/com/bilibili/boxing/BoxingMediaLoader.java
  58. 36
      lib_media_selector/src/boxing/java/com/bilibili/boxing/loader/IBoxingCallback.java
  59. 55
      lib_media_selector/src/boxing/java/com/bilibili/boxing/loader/IBoxingCrop.java
  60. 50
      lib_media_selector/src/boxing/java/com/bilibili/boxing/loader/IBoxingMediaLoader.java
  61. 35
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/BoxingBuilderConfig.java
  62. 80
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/BoxingManager.java
  63. 41
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/callback/IAlbumTaskCallback.java
  64. 48
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/callback/IMediaTaskCallback.java
  65. 319
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/config/BoxingConfig.java
  66. 118
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/config/BoxingCropOption.java
  67. 102
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/entity/AlbumEntity.java
  68. 94
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/entity/BaseMedia.java
  69. 340
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/entity/impl/ImageMedia.java
  70. 196
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/entity/impl/VideoMedia.java
  71. 36
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/task/IMediaTask.java
  72. 187
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/task/impl/AlbumTask.java
  73. 231
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/task/impl/ImageTask.java
  74. 102
      lib_media_selector/src/boxing/java/com/bilibili/boxing/model/task/impl/VideoTask.java
  75. 132
      lib_media_selector/src/boxing/java/com/bilibili/boxing/presenter/PickerContract.java
  76. 178
      lib_media_selector/src/boxing/java/com/bilibili/boxing/presenter/PickerPresenter.java
  77. 91
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/BoxingExecutor.java
  78. 87
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/BoxingExifHelper.java
  79. 103
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/BoxingFileHelper.java
  80. 37
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/BoxingLog.java
  81. 308
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/CameraPickerHelper.java
  82. 84
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/CompressTask.java
  83. 351
      lib_media_selector/src/boxing/java/com/bilibili/boxing/utils/ImageCompressor.java
  84. 20
      lib_media_selector/src/boxing/res/values/strings.xml
  85. 24
      lib_media_selector/src/boxing/res/xml/boxing_file_provider.xml
  86. 20
      lib_media_selector/src/main/AndroidManifest.xml

@ -1,6 +1,6 @@
# 多媒体文件选择库 # 多媒体文件选择库
目前基于boxing修改 基于 boxing 修改,因为 [Android Q SQLiteException](https://github.com/bilibili/boxing/issues/154) 问题没有被修复,暂时依赖源码。
## 1 AndroidN 在 FileProvider 的 xm l配置中加入: ## 1 AndroidN 在 FileProvider 的 xm l配置中加入:
@ -13,7 +13,7 @@
[RotatePhotoView](https://github.com/ChenSiLiang/RotatePhotoView) [RotatePhotoView](https://github.com/ChenSiLiang/RotatePhotoView)
## 3 其他备选参考 ## 3 other options
-  [boxing](https://github.com/Bilibili/boxing) -  [boxing](https://github.com/Bilibili/boxing)
-  [Matisse](https://github.com/zhihu/Matisse) -  [Matisse](https://github.com/zhihu/Matisse)

@ -27,6 +27,10 @@ android {
main { main {
java.srcDirs += "src/github/java" java.srcDirs += "src/github/java"
res.srcDirs += "src/github/res" res.srcDirs += "src/github/res"
java.srcDirs += "src/boxing/java"
res.srcDirs += "src/boxing/res"
java.srcDirs += "src/boxing-impl/java"
res.srcDirs += "src/boxing-impl/res"
} }
} }
@ -36,28 +40,8 @@ dependencies {
//support //support
implementation androidLibraries.appcompat implementation androidLibraries.appcompat
implementation androidLibraries.material implementation androidLibraries.material
/*imageLoader*/ /*imageLoader*/
compileOnly thirdLibraries.glide compileOnly thirdLibraries.glide
implementation('com.bilibili:boxing:1.0.4') {
exclude group: 'com.android.support'
exclude group: 'com.chensl.rotatephotoview'
exclude group: 'com.android.support.test.espresso'
exclude group: 'com.android.support.test'
exclude group: 'org.powermock'
exclude group: 'org.robolectric'
}
implementation('com.bilibili:boxing-impl:1.0.4') {
exclude group: 'com.android.support'
exclude group: 'com.chensl.rotatephotoview'
exclude group: 'com.android.support.test.espresso'
exclude group: 'com.android.support.test'
exclude group: 'org.powermock'
exclude group: 'org.robolectric'
}
implementation('com.github.yalantis:ucrop:2.2.4') { implementation('com.github.yalantis:ucrop:2.2.4') {
exclude group: 'com.squareup.okhttp3' exclude group: 'com.squareup.okhttp3'
} }

@ -0,0 +1,32 @@
package com.bilibili.boxing_impl;
import com.bilibili.boxing.model.BoxingManager;
import com.ztiany.mediaselector.R;
import androidx.annotation.DrawableRes;
/**
* Help getting the resource in config.
*
* @author ChenSL
*/
public class BoxingResHelper {
@DrawableRes
public static int getMediaCheckedRes() {
int result = BoxingManager.getInstance().getBoxingConfig().getMediaCheckedRes();
return result > 0 ? result : R.drawable.ic_boxing_checked;
}
@DrawableRes
public static int getMediaUncheckedRes() {
int result = BoxingManager.getInstance().getBoxingConfig().getMediaUnCheckedRes();
return result > 0 ? result : R.drawable.shape_boxing_unchecked;
}
@DrawableRes
public static int getCameraRes() {
return BoxingManager.getInstance().getBoxingConfig().getCameraRes();
}
}

@ -0,0 +1,89 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Display;
import android.view.WindowManager;
/**
* @author ChenSL
*/
public class WindowManagerHelper {
private static WindowManager getWindowManager(Context context) {
Object service = context.getSystemService(Context.WINDOW_SERVICE);
if (service == null)
return null;
return (WindowManager) service;
}
private static Display getDefaultDisplay(Context context) {
WindowManager wm = getWindowManager(context);
if (wm == null)
return null;
return wm.getDefaultDisplay();
}
public static int getScreenHeight(Context context) {
DisplayMetrics dm = getDisplayMetrics(context);
if (dm != null) {
return dm.heightPixels;
}
return 0;
}
public static int getScreenWidth(Context context) {
DisplayMetrics dm = getDisplayMetrics(context);
if (dm != null) {
return dm.widthPixels;
}
return 0;
}
private static DisplayMetrics getDisplayMetrics(Context context) {
Display display = getDefaultDisplay(context);
if (display != null) {
DisplayMetrics result = new DisplayMetrics();
display.getMetrics(result);
return result;
}
return null;
}
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
public static int getToolbarHeight(Context context) {
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
}
return 0;
}
}

@ -0,0 +1,158 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.adapter;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bilibili.boxing.BoxingMediaLoader;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
/**
* Album window adapter.
*
* @author ChenSL
*/
public class BoxingAlbumAdapter extends RecyclerView.Adapter implements View.OnClickListener {
private static final String UNKNOW_ALBUM_NAME = "?";
private int mCurrentAlbumPos;
private List<AlbumEntity> mAlums;
private LayoutInflater mInflater;
private OnAlbumClickListener mAlbumOnClickListener;
private int mDefaultRes;
public BoxingAlbumAdapter(Context context) {
this.mAlums = new ArrayList<>();
this.mAlums.add(AlbumEntity.createDefaultAlbum());
this.mInflater = LayoutInflater.from(context);
this.mDefaultRes = BoxingManager.getInstance().getBoxingConfig().getAlbumPlaceHolderRes();
}
public void setAlbumOnClickListener(OnAlbumClickListener albumOnClickListener) {
this.mAlbumOnClickListener = albumOnClickListener;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new AlbumViewHolder(mInflater.inflate(R.layout.layout_boxing_album_item, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final AlbumViewHolder albumViewHolder = (AlbumViewHolder) holder;
albumViewHolder.mCoverImg.setImageResource(mDefaultRes);
final int adapterPos = holder.getAdapterPosition();
final AlbumEntity album = mAlums.get(adapterPos);
if (album != null && album.hasImages()) {
String albumName = TextUtils.isEmpty(album.mBucketName) ?
albumViewHolder.mNameTxt.getContext().getString(R.string.boxing_default_album_name) :album.mBucketName;
albumViewHolder.mNameTxt.setText(albumName);
ImageMedia media = (ImageMedia) album.mImageList.get(0);
if (media != null) {
BoxingMediaLoader.getInstance().displayThumbnail(albumViewHolder.mCoverImg, media.getPath(), 50, 50);
albumViewHolder.mCoverImg.setTag(R.string.boxing_app_name, media.getPath());
}
albumViewHolder.mLayout.setTag(adapterPos);
albumViewHolder.mLayout.setOnClickListener(this);
albumViewHolder.mCheckedImg.setVisibility(album.mIsSelected ? View.VISIBLE : View.GONE);
albumViewHolder.mSizeTxt.setText(albumViewHolder.mSizeTxt.
getResources().getString(R.string.boxing_album_images_fmt, album.mCount));
} else {
albumViewHolder.mNameTxt.setText(UNKNOW_ALBUM_NAME);
albumViewHolder.mSizeTxt.setVisibility(View.GONE);
}
}
public void addAllData(List<AlbumEntity> alums) {
mAlums.clear();
mAlums.addAll(alums);
notifyDataSetChanged();
}
public List<AlbumEntity> getAlums() {
return mAlums;
}
public int getCurrentAlbumPos() {
return mCurrentAlbumPos;
}
public void setCurrentAlbumPos(int currentAlbumPos) {
mCurrentAlbumPos = currentAlbumPos;
}
public AlbumEntity getCurrentAlbum() {
if (mAlums == null || mAlums.size() <= 0) {
return null;
}
return mAlums.get(mCurrentAlbumPos);
}
@Override
public int getItemCount() {
return mAlums != null ? mAlums.size() : 0;
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.album_layout) {
if (mAlbumOnClickListener != null) {
mAlbumOnClickListener.onClick(v, (Integer) v.getTag());
}
}
}
private static class AlbumViewHolder extends RecyclerView.ViewHolder {
ImageView mCoverImg;
TextView mNameTxt;
TextView mSizeTxt;
View mLayout;
ImageView mCheckedImg;
AlbumViewHolder(final View itemView) {
super(itemView);
mCoverImg = (ImageView) itemView.findViewById(R.id.album_thumbnail);
mNameTxt = (TextView) itemView.findViewById(R.id.album_name);
mSizeTxt = (TextView) itemView.findViewById(R.id.album_size);
mLayout = itemView.findViewById(R.id.album_layout);
mCheckedImg = (ImageView) itemView.findViewById(R.id.album_checked);
}
}
public interface OnAlbumClickListener {
void onClick(View view, int pos);
}
}

@ -0,0 +1,212 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing_impl.BoxingResHelper;
import com.bilibili.boxing_impl.view.MediaItemLayout;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
/**
* A RecyclerView.Adapter for image or video picker showing.
*
* @author ChenSL
*/
public class BoxingMediaAdapter extends RecyclerView.Adapter {
private static final int CAMERA_TYPE = 0;
private static final int NORMAL_TYPE = 1;
private int mOffset;
private boolean mMultiImageMode;
private List<BaseMedia> mMedias;
private List<BaseMedia> mSelectedMedias;
private LayoutInflater mInflater;
private BoxingConfig mMediaConfig;
private View.OnClickListener mOnCameraClickListener;
private View.OnClickListener mOnMediaClickListener;
private OnCheckListener mOnCheckListener;
private OnMediaCheckedListener mOnCheckedListener;
private int mDefaultRes;
public BoxingMediaAdapter(Context context) {
this.mInflater = LayoutInflater.from(context);
this.mMedias = new ArrayList<>();
this.mSelectedMedias = new ArrayList<>(9);
this.mMediaConfig = BoxingManager.getInstance().getBoxingConfig();
this.mOffset = mMediaConfig.isNeedCamera() ? 1 : 0;
this.mMultiImageMode = mMediaConfig.getMode() == BoxingConfig.Mode.MULTI_IMG;
this.mOnCheckListener = new OnCheckListener();
this.mDefaultRes = mMediaConfig.getMediaPlaceHolderRes();
}
@Override
public int getItemViewType(int position) {
if (position == 0 && mMediaConfig.isNeedCamera()) {
return CAMERA_TYPE;
}
return NORMAL_TYPE;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (CAMERA_TYPE == viewType) {
return new CameraViewHolder(mInflater.inflate(R.layout.layout_boxing_recycleview_header, parent, false));
}
return new ImageViewHolder(mInflater.inflate(R.layout.layout_boxing_recycleview_item, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof CameraViewHolder) {
CameraViewHolder viewHolder = (CameraViewHolder) holder;
viewHolder.mCameraLayout.setOnClickListener(mOnCameraClickListener);
viewHolder.mCameraImg.setImageResource(BoxingResHelper.getCameraRes());
} else {
int pos = position - mOffset;
final BaseMedia media = mMedias.get(pos);
final ImageViewHolder vh = (ImageViewHolder) holder;
vh.mItemLayout.setImageRes(mDefaultRes);
vh.mItemLayout.setTag(media);
vh.mItemLayout.setOnClickListener(mOnMediaClickListener);
vh.mItemLayout.setTag(R.id.media_item_check, pos);
vh.mItemLayout.setMedia(media);
vh.mItemChecked.setVisibility(mMultiImageMode ? View.VISIBLE : View.GONE);
if (mMultiImageMode && media instanceof ImageMedia) {
vh.mItemLayout.setChecked(((ImageMedia) media).isSelected());
vh.mItemChecked.setTag(R.id.media_layout, vh.mItemLayout);
vh.mItemChecked.setTag(media);
vh.mItemChecked.setOnClickListener(mOnCheckListener);
}
}
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return mMedias.size() + mOffset;
}
public void setOnCameraClickListener(View.OnClickListener onCameraClickListener) {
mOnCameraClickListener = onCameraClickListener;
}
public void setOnCheckedListener(OnMediaCheckedListener onCheckedListener) {
mOnCheckedListener = onCheckedListener;
}
public void setOnMediaClickListener(View.OnClickListener onMediaClickListener) {
mOnMediaClickListener = onMediaClickListener;
}
public List<BaseMedia> getSelectedMedias() {
return mSelectedMedias;
}
public void setSelectedMedias(List<BaseMedia> selectedMedias) {
if (selectedMedias == null) {
return;
}
mSelectedMedias.clear();
mSelectedMedias.addAll(selectedMedias);
notifyDataSetChanged();
}
public void addAllData(@NonNull List<BaseMedia> data) {
int oldSize = mMedias.size();
this.mMedias.addAll(data);
int size = data.size();
notifyItemRangeInserted(oldSize, size);
}
public void clearData() {
int size = mMedias.size();
this.mMedias.clear();
notifyItemRangeRemoved(0, size);
}
public List<BaseMedia> getAllMedias() {
return mMedias;
}
private static class ImageViewHolder extends RecyclerView.ViewHolder {
MediaItemLayout mItemLayout;
View mItemChecked;
ImageViewHolder(View itemView) {
super(itemView);
mItemLayout = (MediaItemLayout) itemView.findViewById(R.id.media_layout);
mItemChecked = itemView.findViewById(R.id.media_item_check);
}
}
private static class CameraViewHolder extends RecyclerView.ViewHolder {
View mCameraLayout;
ImageView mCameraImg;
CameraViewHolder(final View itemView) {
super(itemView);
mCameraLayout = itemView.findViewById(R.id.camera_layout);
mCameraImg = (ImageView) itemView.findViewById(R.id.camera_img);
}
}
private class OnCheckListener implements View.OnClickListener {
@Override
public void onClick(View v) {
MediaItemLayout itemLayout = (MediaItemLayout) v.getTag(R.id.media_layout);
BaseMedia media = (BaseMedia) v.getTag();
if (mMediaConfig.getMode() == BoxingConfig.Mode.MULTI_IMG) {
if (mOnCheckedListener != null) {
mOnCheckedListener.onChecked(itemLayout, media);
}
}
}
}
public interface OnMediaCheckedListener {
/**
* In multi image mode, selecting a {@link BaseMedia} or undo.
*/
void onChecked(View v, BaseMedia iMedia);
}
}

@ -0,0 +1,93 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import com.bilibili.boxing.AbsBoxingActivity;
import com.bilibili.boxing.AbsBoxingViewFragment;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
/**
* Default UI Activity for simplest usage.
* A simple subclass of {@link AbsBoxingActivity}. Holding a {@link AbsBoxingViewFragment} to display medias.
*/
public class BoxingActivity extends AbsBoxingActivity {
private BoxingViewFragment mPickerFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_boxing);
createToolbar();
setTitleTxt(getBoxingConfig());
}
@NonNull
@Override
public AbsBoxingViewFragment onCreateBoxingView(ArrayList<BaseMedia> medias) {
mPickerFragment = (BoxingViewFragment) getSupportFragmentManager().findFragmentByTag(BoxingViewFragment.TAG);
if (mPickerFragment == null) {
mPickerFragment = (BoxingViewFragment) BoxingViewFragment.newInstance().setSelectedBundle(medias);
getSupportFragmentManager().beginTransaction().replace(R.id.content_layout, mPickerFragment, BoxingViewFragment.TAG).commit();
}
return mPickerFragment;
}
private void createToolbar() {
Toolbar bar = (Toolbar) findViewById(R.id.nav_top_bar);
setSupportActionBar(bar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowTitleEnabled(false);
bar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
}
private void setTitleTxt(BoxingConfig config) {
TextView titleTxt = (TextView) findViewById(R.id.pick_album_txt);
if (config.getMode() == BoxingConfig.Mode.VIDEO) {
titleTxt.setText(R.string.boxing_video_title);
titleTxt.setCompoundDrawables(null, null, null, null);
return;
}
mPickerFragment.setTitleTxt(titleTxt);
}
@Override
public void onBoxingFinish(Intent intent, @Nullable List<BaseMedia> medias) {
setResult(Activity.RESULT_OK, intent);
finish();
}
}

@ -0,0 +1,36 @@
package com.bilibili.boxing_impl.ui;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
/**
* Created by ChenSL on 2017/4/5.
*/
public class BoxingBaseFragment extends Fragment {
private boolean mNeedPendingUserVisibileHint;
private boolean mLastUserVisibileHint;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mNeedPendingUserVisibileHint) {
setUserVisibleCompat(mLastUserVisibileHint);
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (getActivity() == null) {
mNeedPendingUserVisibileHint = true;
mLastUserVisibileHint = isVisibleToUser;
} else {
setUserVisibleCompat(isVisibleToUser);
}
}
void setUserVisibleCompat(boolean userVisibleCompat) {
}
}

@ -0,0 +1,142 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.bilibili.boxing.AbsBoxingActivity;
import com.bilibili.boxing.AbsBoxingViewFragment;
import com.bilibili.boxing.BoxingMediaLoader;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
/**
* Default UI Activity for simplest usage, containing layout achieve {@link BottomSheetBehavior}.
* Only support SINGLE_IMG and VIDEO Mode.
*
* @author ChenSL
*/
public class BoxingBottomSheetActivity extends AbsBoxingActivity implements View.OnClickListener {
private BottomSheetBehavior<FrameLayout> mBehavior;
private ImageView mImage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_boxing_bottom_sheet);
createToolbar();
FrameLayout bottomSheet = (FrameLayout) findViewById(R.id.content_layout);
mBehavior = BottomSheetBehavior.from(bottomSheet);
mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
mImage = (ImageView) findViewById(R.id.media_result);
mImage.setOnClickListener(this);
}
@NonNull
@Override
public AbsBoxingViewFragment onCreateBoxingView(ArrayList<BaseMedia> medias) {
BoxingBottomSheetFragment fragment = (BoxingBottomSheetFragment) getSupportFragmentManager().findFragmentByTag(BoxingBottomSheetFragment.TAG);
if (fragment == null) {
fragment = BoxingBottomSheetFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.add(R.id.content_layout, fragment, BoxingBottomSheetFragment.TAG).commit();
}
return fragment;
}
private void createToolbar() {
Toolbar bar = (Toolbar) findViewById(R.id.nav_top_bar);
setSupportActionBar(bar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.boxing_default_album);
bar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
}
private boolean hideBottomSheet() {
if (mBehavior != null && mBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
return true;
}
return false;
}
private boolean collapseBottomSheet() {
if (mBehavior != null && mBehavior.getState() != BottomSheetBehavior.STATE_COLLAPSED) {
mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
return true;
}
return false;
}
private void toggleBottomSheet() {
if (mBehavior == null) {
return;
}
if (mBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
@Override
public void onBackPressed() {
if (hideBottomSheet()) {
return;
}
super.onBackPressed();
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.media_result) {
toggleBottomSheet();
}
}
@Override
public void onBoxingFinish(Intent intent, @Nullable List<BaseMedia> medias) {
if (mImage != null && medias != null && !medias.isEmpty()) {
ImageMedia imageMedia = (ImageMedia) medias.get(0);
BoxingMediaLoader.getInstance().displayRaw(mImage, imageMedia.getPath(), 1080, 720, null);
}
hideBottomSheet();
}
}

@ -0,0 +1,249 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.ui;
import android.Manifest;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.bilibili.boxing.AbsBoxingViewFragment;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.presenter.PickerContract;
import com.bilibili.boxing.utils.BoxingFileHelper;
import com.bilibili.boxing_impl.adapter.BoxingMediaAdapter;
import com.bilibili.boxing_impl.view.HackyGridLayoutManager;
import com.bilibili.boxing_impl.view.SpacesItemDecoration;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* the most easy to implement {@link PickerContract.View} to show medias with google's Bottom Sheet </br>
* for simplest purpose, it only support SINGLE_IMG and VIDEO Mode.
* for MULTI_IMG mode, use {@link BoxingViewFragment} instead.
*
* @author ChenSL
*/
public class BoxingBottomSheetFragment extends AbsBoxingViewFragment implements View.OnClickListener {
public static final String TAG = "com.bilibili.boxing_impl.ui.BoxingBottomSheetFragment";
private static final int GRID_COUNT = 3;
private boolean mIsCamera;
private BoxingMediaAdapter mMediaAdapter;
private ProgressDialog mDialog;
private RecyclerView mRecycleView;
private TextView mEmptyTxt;
private ProgressBar mLoadingView;
public static BoxingBottomSheetFragment newInstance() {
return new BoxingBottomSheetFragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMediaAdapter = new BoxingMediaAdapter(getActivity());
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_boxing_bottom_sheet, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mEmptyTxt = (TextView) view.findViewById(R.id.empty_txt);
mRecycleView = (RecyclerView) view.findViewById(R.id.media_recycleview);
mRecycleView.setHasFixedSize(true);
mLoadingView = (ProgressBar) view.findViewById(R.id.loading);
GridLayoutManager gridLayoutManager = new HackyGridLayoutManager(getActivity(), GRID_COUNT);
gridLayoutManager.setSmoothScrollbarEnabled(true);
mRecycleView.setLayoutManager(gridLayoutManager);
mRecycleView.addItemDecoration(new SpacesItemDecoration(getResources().getDimensionPixelOffset(R.dimen.boxing_media_margin), GRID_COUNT));
mRecycleView.setAdapter(mMediaAdapter);
mRecycleView.addOnScrollListener(new ScrollListener());
mMediaAdapter.setOnMediaClickListener(new OnMediaClickListener());
mMediaAdapter.setOnCameraClickListener(new OnCameraClickListener());
view.findViewById(R.id.finish_txt).setOnClickListener(this);
}
@Override
public void onCameraActivityResult(int requestCode, int resultCode) {
showProgressDialog();
super.onCameraActivityResult(requestCode, resultCode);
}
private void showProgressDialog() {
if (mDialog == null) {
mDialog = new ProgressDialog(getActivity());
mDialog.setIndeterminate(true);
mDialog.setMessage(getString(R.string.boxing_handling));
}
if (!mDialog.isShowing()) {
mDialog.show();
}
}
private void dismissProgressDialog() {
if (mDialog != null && mDialog.isShowing()) {
mDialog.hide();
mDialog.dismiss();
}
}
@Override
public void showMedia(List<BaseMedia> medias, int count) {
if (medias == null || isEmptyData(medias)
&& isEmptyData(mMediaAdapter.getAllMedias())) {
showEmptyData();
return;
}
showData();
mMediaAdapter.addAllData(medias);
}
private boolean isEmptyData(List<BaseMedia> medias) {
return medias.isEmpty() && !BoxingManager.getInstance().getBoxingConfig().isNeedCamera();
}
private void showEmptyData() {
mEmptyTxt.setVisibility(View.VISIBLE);
mRecycleView.setVisibility(View.GONE);
mLoadingView.setVisibility(View.GONE);
}
private void showData() {
mLoadingView.setVisibility(View.GONE);
mEmptyTxt.setVisibility(View.GONE);
mRecycleView.setVisibility(View.VISIBLE);
}
@Override
public void onCameraFinish(BaseMedia media) {
dismissProgressDialog();
mIsCamera = false;
if (media != null) {
List<BaseMedia> selectedMedias = mMediaAdapter.getSelectedMedias();
selectedMedias.add(media);
BoxingBottomSheetFragment.this.onFinish(selectedMedias);
}
}
@Override
public void onCameraError() {
mIsCamera = false;
dismissProgressDialog();
}
@Override
public void startLoading() {
loadMedias();
}
@Override
public void onRequestPermissionError(String[] permissions, Exception e) {
if (permissions.length > 0) {
if (permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
showEmptyData();
Toast.makeText(getContext(), R.string.boxing_storage_permission_deny, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onRequestPermissionSuc(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (permissions[0].equals(STORAGE_PERMISSIONS[0])) {
startLoading();
}
}
@Override
public void clearMedia() {
mMediaAdapter.clearData();
}
@Override
public void onClick(View v) {
int id = v.getId();
if (R.id.finish_txt == id) {
onFinish(null);
}
}
private class OnMediaClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
ArrayList<BaseMedia> iMedias = new ArrayList<>();
BaseMedia media = (BaseMedia) v.getTag();
iMedias.add(media);
onFinish(iMedias);
}
}
private class OnCameraClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (!mIsCamera) {
mIsCamera = true;
startCamera(getActivity(), BoxingBottomSheetFragment.this, BoxingFileHelper.DEFAULT_SUB_DIR);
}
}
}
private class ScrollListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
final int childCount = recyclerView.getChildCount();
if (childCount > 0) {
View lastChild = recyclerView.getChildAt(childCount - 1);
RecyclerView.Adapter outerAdapter = recyclerView.getAdapter();
int lastVisible = recyclerView.getChildAdapterPosition(lastChild);
if (lastVisible == outerAdapter.getItemCount() - 1 && hasNextPage() && canLoadNextPage()) {
onLoadNextPage();
}
}
}
}
}

@ -0,0 +1,191 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.ui;
import android.app.Activity;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import com.bilibili.boxing.AbsBoxingViewActivity;
import com.bilibili.boxing.loader.IBoxingCallback;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.utils.BoxingLog;
import com.ztiany.mediaselector.R;
import java.lang.ref.WeakReference;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import uk.co.senab.photoview.PhotoView;
import uk.co.senab.photoview.PhotoViewAttacher;
/**
* show raw image with the control of finger gesture.
*
* @author ChenSL
*/
public class BoxingRawImageFragment extends BoxingBaseFragment {
private static final String BUNDLE_IMAGE = "com.bilibili.boxing_impl.ui.BoxingRawImageFragment.image";
private static final int MAX_SCALE = 15;
private static final long MAX_IMAGE1 = 1024 * 1024L;
private static final long MAX_IMAGE2 = 4 * MAX_IMAGE1;
private PhotoView mImageView;
private ProgressBar mProgress;
private ImageMedia mMedia;
private PhotoViewAttacher mAttacher;
public static BoxingRawImageFragment newInstance(@NonNull ImageMedia image) {
BoxingRawImageFragment fragment = new BoxingRawImageFragment();
Bundle args = new Bundle();
args.putParcelable(BUNDLE_IMAGE, image);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMedia = getArguments().getParcelable(BUNDLE_IMAGE);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_boxing_raw_image, container, false);
}
@Override
public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mProgress = (ProgressBar) view.findViewById(R.id.loading);
mImageView = (PhotoView) view.findViewById(R.id.photo_view);
mAttacher = new PhotoViewAttacher(mImageView);
mAttacher.setRotatable(true);
mAttacher.setToRightAngle(true);
}
@Override
void setUserVisibleCompat(boolean isVisibleToUser) {
if (isVisibleToUser) {
Point point = getResizePointer(mMedia.getSize());
((AbsBoxingViewActivity) getActivity()).loadRawImage(mImageView, mMedia.getPath(), point.x, point.y, new BoxingCallback(this));
}
}
/**
* resize the image or not according to size.
*
* @param size the size of image
*/
private Point getResizePointer(long size) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
Point point = new Point(metrics.widthPixels, metrics.heightPixels);
if (size >= MAX_IMAGE2) {
point.x >>= 2;
point.y >>= 2;
} else if (size >= MAX_IMAGE1) {
point.x >>= 1;
point.y >>= 1;
} else if (size > 0) {
// avoid some images do not have a size.
point.x = 0;
point.y = 0;
}
return point;
}
private void dismissProgressDialog() {
if (mProgress != null) {
mProgress.setVisibility(View.GONE);
}
BoxingViewActivity activity = getThisActivity();
if (activity != null && activity.mProgressBar != null) {
activity.mProgressBar.setVisibility(View.GONE);
}
}
private BoxingViewActivity getThisActivity() {
Activity activity = getActivity();
if (activity instanceof BoxingViewActivity) {
return (BoxingViewActivity) activity;
}
return null;
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (mAttacher != null) {
mAttacher.cleanup();
mAttacher = null;
mImageView = null;
}
}
private static class BoxingCallback implements IBoxingCallback {
private WeakReference<BoxingRawImageFragment> mWr;
BoxingCallback(BoxingRawImageFragment fragment) {
mWr = new WeakReference<>(fragment);
}
@Override
public void onSuccess() {
if (mWr.get() == null || mWr.get().mImageView == null) {
return;
}
mWr.get().dismissProgressDialog();
Drawable drawable = mWr.get().mImageView.getDrawable();
PhotoViewAttacher attacher = mWr.get().mAttacher;
if (attacher != null) {
if (drawable.getIntrinsicHeight() > (drawable.getIntrinsicWidth() << 2)) {
// handle the super height image.
int scale = drawable.getIntrinsicHeight() / drawable.getIntrinsicWidth();
scale = Math.min(MAX_SCALE, scale);
attacher.setMaximumScale(scale);
attacher.setScale(scale, true);
}
attacher.update();
}
BoxingViewActivity activity = mWr.get().getThisActivity();
if (activity != null && activity.mGallery != null) {
activity.mGallery.setVisibility(View.VISIBLE);
}
}
@Override
public void onFail(Throwable t) {
if (mWr.get() == null) {
return;
}
BoxingLog.d(t != null ? t.getMessage() : "load raw image error.");
mWr.get().dismissProgressDialog();
mWr.get().mImageView.setImageResource(R.drawable.ic_boxing_broken_image);
if (mWr.get().mAttacher != null) {
mWr.get().mAttacher.update();
}
}
}
}

@ -0,0 +1,333 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.bilibili.boxing.AbsBoxingViewActivity;
import com.bilibili.boxing.Boxing;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.model.task.IMediaTask;
import com.bilibili.boxing_impl.BoxingResHelper;
import com.bilibili.boxing_impl.view.HackyViewPager;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
/**
* An Activity to show raw image by holding {@link BoxingViewFragment}.
*
* @author ChenSL
*/
public class BoxingViewActivity extends AbsBoxingViewActivity {
public static final String EXTRA_TYPE_BACK = "com.bilibili.boxing_impl.ui.BoxingViewActivity.type_back";
HackyViewPager mGallery;
ProgressBar mProgressBar;
private boolean mNeedEdit;
private boolean mNeedLoading;
private boolean mFinishLoading;
private boolean mNeedAllCount = true;
private int mCurrentPage;
private int mTotalCount;
private int mStartPos;
private int mPos;
private int mMaxCount;
private String mAlbumId;
private Toolbar mToolbar;
private ImagesAdapter mAdapter;
private ImageMedia mCurrentImageItem;
private Button mOkBtn;
private ArrayList<BaseMedia> mImages;
private ArrayList<BaseMedia> mSelectedImages;
private MenuItem mSelectedMenuItem;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_boxing_view);
createToolbar();
initData();
initView();
startLoading();
}
private void createToolbar() {
mToolbar = (Toolbar) findViewById(R.id.nav_top_bar);
setSupportActionBar(mToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
getSupportActionBar().setDisplayShowTitleEnabled(false);
}
private void initData() {
mSelectedImages = getSelectedImages();
mAlbumId = getAlbumId();
mStartPos = getStartPos();
mNeedLoading = BoxingManager.getInstance().getBoxingConfig().isNeedLoading();
mNeedEdit = BoxingManager.getInstance().getBoxingConfig().isNeedEdit();
mMaxCount = getMaxCount();
mImages = new ArrayList<>();
if (!mNeedLoading && mSelectedImages != null) {
mImages.addAll(mSelectedImages);
}
}
private void initView() {
mAdapter = new ImagesAdapter(getSupportFragmentManager());
mOkBtn = (Button) findViewById(R.id.image_items_ok);
mGallery = (HackyViewPager) findViewById(R.id.pager);
mProgressBar = (ProgressBar) findViewById(R.id.loading);
mGallery.setAdapter(mAdapter);
mGallery.addOnPageChangeListener(new OnPagerChangeListener());
if (!mNeedEdit) {
View chooseLayout = findViewById(R.id.item_choose_layout);
chooseLayout.setVisibility(View.GONE);
} else {
setOkTextNumber();
mOkBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finishByBackPressed(false);
}
});
}
}
private void setOkTextNumber() {
if (mNeedEdit) {
int selectedSize = mSelectedImages.size();
int size = Math.max(mSelectedImages.size(), mMaxCount);
mOkBtn.setText(getString(R.string.boxing_image_preview_ok_fmt, String.valueOf(selectedSize)
, String.valueOf(size)));
mOkBtn.setEnabled(selectedSize > 0);
}
}
private void finishByBackPressed(boolean value) {
Intent intent = new Intent();
intent.putParcelableArrayListExtra(Boxing.EXTRA_SELECTED_MEDIA, mSelectedImages);
intent.putExtra(EXTRA_TYPE_BACK, value);
setResult(RESULT_OK, intent);
finish();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
if (mNeedEdit) {
getMenuInflater().inflate(R.menu.activity_boxing_image_viewer, menu);
mSelectedMenuItem = menu.findItem(R.id.menu_image_item_selected);
if (mCurrentImageItem != null) {
setMenuIcon(mCurrentImageItem.isSelected());
} else {
setMenuIcon(false);
}
return true;
}
return false;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_image_item_selected) {
if (mCurrentImageItem == null) {
return false;
}
if (mSelectedImages.size() >= mMaxCount && !mCurrentImageItem.isSelected()) {
String warning = getString(R.string.boxing_max_image_over_fmt, mMaxCount);
Toast.makeText(this, warning, Toast.LENGTH_SHORT).show();
return true;
}
if (mCurrentImageItem.isSelected()) {
cancelImage();
} else {
if (!mSelectedImages.contains(mCurrentImageItem)) {
if (mCurrentImageItem.isGifOverSize()) {
Toast.makeText(getApplicationContext(), R.string.boxing_gif_too_big, Toast.LENGTH_SHORT).show();
return true;
}
mCurrentImageItem.setSelected(true);
mSelectedImages.add(mCurrentImageItem);
}
}
setOkTextNumber();
setMenuIcon(mCurrentImageItem.isSelected());
return true;
}
return super.onOptionsItemSelected(item);
}
private void cancelImage() {
if (mSelectedImages.contains(mCurrentImageItem)) {
mSelectedImages.remove(mCurrentImageItem);
}
mCurrentImageItem.setSelected(false);
}
private void setMenuIcon(boolean isSelected) {
if (mNeedEdit) {
mSelectedMenuItem.setIcon(isSelected ? BoxingResHelper.getMediaCheckedRes(): BoxingResHelper.getMediaUncheckedRes());
}
}
@Override
public void startLoading() {
if (!mNeedLoading) {
mCurrentImageItem = (ImageMedia) mSelectedImages.get(mStartPos);
mToolbar.setTitle(getString(R.string.boxing_image_preview_title_fmt, String.valueOf(mStartPos + 1)
, String.valueOf(mSelectedImages.size())));
mProgressBar.setVisibility(View.GONE);
mGallery.setVisibility(View.VISIBLE);
mAdapter.setMedias(mImages);
if (mStartPos > 0 && mStartPos < mSelectedImages.size()) {
mGallery.setCurrentItem(mStartPos, false);
}
} else {
loadMedia(mAlbumId, mStartPos, mCurrentPage);
mAdapter.setMedias(mImages);
}
}
private void loadMedia(String albumId, int startPos, int page) {
this.mPos = startPos;
loadMedias(page, albumId);
}
@Override
public void showMedia(@Nullable List<BaseMedia> medias, int totalCount) {
if (medias == null || totalCount <= 0) {
return;
}
mImages.addAll(medias);
mAdapter.notifyDataSetChanged();
checkSelectedMedia(mImages, mSelectedImages);
setupGallery();
if (mToolbar != null && mNeedAllCount) {
mToolbar.setTitle(getString(R.string.boxing_image_preview_title_fmt,
String.valueOf(++mPos), String.valueOf(totalCount)));
mNeedAllCount = false;
}
loadOtherPagesInAlbum(totalCount);
}
private void setupGallery() {
int startPos = mStartPos;
if (mGallery == null || startPos < 0) {
return;
}
if (startPos < mImages.size() && !mFinishLoading) {
mGallery.setCurrentItem(mStartPos, false);
mCurrentImageItem = (ImageMedia) mImages.get(startPos);
mProgressBar.setVisibility(View.GONE);
mGallery.setVisibility(View.VISIBLE);
mFinishLoading = true;
invalidateOptionsMenu();
} else if (startPos >= mImages.size()) {
mProgressBar.setVisibility(View.VISIBLE);
mGallery.setVisibility(View.GONE);
}
}
private void loadOtherPagesInAlbum(int totalCount) {
mTotalCount = totalCount;
if (mCurrentPage <= (mTotalCount / IMediaTask.PAGE_LIMIT)) {
mCurrentPage++;
loadMedia(mAlbumId, mStartPos, mCurrentPage);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (mSelectedImages != null) {
outState.putParcelableArrayList(Boxing.EXTRA_SELECTED_MEDIA, mSelectedImages);
}
outState.putString(Boxing.EXTRA_ALBUM_ID, mAlbumId);
super.onSaveInstanceState(outState);
}
@Override
public void onBackPressed() {
finishByBackPressed(true);
}
private class ImagesAdapter extends FragmentStatePagerAdapter {
private ArrayList<BaseMedia> mMedias;
ImagesAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int i) {
return BoxingRawImageFragment.newInstance((ImageMedia) mMedias.get(i));
}
@Override
public int getCount() {
return mMedias == null ? 0 : mMedias.size();
}
public void setMedias(ArrayList<BaseMedia> medias) {
this.mMedias = medias;
notifyDataSetChanged();
}
}
private class OnPagerChangeListener extends ViewPager.SimpleOnPageChangeListener {
@Override
public void onPageSelected(int position) {
if (mToolbar != null && position < mImages.size()) {
mToolbar.setTitle(getString(R.string.boxing_image_preview_title_fmt, String.valueOf(position + 1)
, mNeedLoading ? String.valueOf(mTotalCount) : String.valueOf(mImages.size())));
mCurrentImageItem = (ImageMedia) mImages.get(position);
invalidateOptionsMenu();
}
}
}
}

@ -0,0 +1,526 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.ui;
import android.Manifest;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.bilibili.boxing.AbsBoxingViewFragment;
import com.bilibili.boxing.Boxing;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.utils.BoxingFileHelper;
import com.bilibili.boxing_impl.WindowManagerHelper;
import com.bilibili.boxing_impl.adapter.BoxingAlbumAdapter;
import com.bilibili.boxing_impl.adapter.BoxingMediaAdapter;
import com.bilibili.boxing_impl.view.HackyGridLayoutManager;
import com.bilibili.boxing_impl.view.MediaItemLayout;
import com.bilibili.boxing_impl.view.SpacesItemDecoration;
import com.ztiany.mediaselector.R;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* A full implement for {@link com.bilibili.boxing.presenter.PickerContract.View} supporting all the mode
* in {@link BoxingConfig.Mode}.
* use this to pick the picture.
*
* @author ChenSL
*/
public class BoxingViewFragment extends AbsBoxingViewFragment implements View.OnClickListener {
public static final String TAG = "com.bilibili.boxing_impl.ui.BoxingViewFragment";
private static final int IMAGE_PREVIEW_REQUEST_CODE = 9086;
private static final int IMAGE_CROP_REQUEST_CODE = 9087;
private static final int GRID_COUNT = 3;
private boolean mIsPreview;
private boolean mIsCamera;
private Button mPreBtn;
private Button mOkBtn;
private RecyclerView mRecycleView;
private BoxingMediaAdapter mMediaAdapter;
private BoxingAlbumAdapter mAlbumWindowAdapter;
private ProgressDialog mDialog;
private TextView mEmptyTxt;
private TextView mTitleTxt;
private PopupWindow mAlbumPopWindow;
private ProgressBar mLoadingView;
private int mMaxCount;
public static BoxingViewFragment newInstance() {
return new BoxingViewFragment();
}
@Override
public void onCreateWithSelectedMedias(Bundle savedInstanceState, @Nullable List<BaseMedia> selectedMedias) {
mAlbumWindowAdapter = new BoxingAlbumAdapter(getContext());
mMediaAdapter = new BoxingMediaAdapter(getContext());
mMediaAdapter.setSelectedMedias(selectedMedias);
mMaxCount = getMaxCount();
}
@Override
public void startLoading() {
loadMedias();
loadAlbum();
}
@Override
public void onRequestPermissionError(String[] permissions, Exception e) {
if (permissions.length > 0) {
if (permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Toast.makeText(getContext(), R.string.boxing_storage_permission_deny, Toast.LENGTH_SHORT).show();
showEmptyData();
} else if (permissions[0].equals(Manifest.permission.CAMERA)){
Toast.makeText(getContext(), R.string.boxing_camera_permission_deny, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onRequestPermissionSuc(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (permissions[0].equals(STORAGE_PERMISSIONS[0])) {
startLoading();
} else if (permissions[0].equals(CAMERA_PERMISSIONS[0])) {
startCamera(getActivity(), this, null);
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragmant_boxing_view, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
initViews(view);
super.onViewCreated(view, savedInstanceState);
}
private void initViews(View view) {
mEmptyTxt = (TextView) view.findViewById(R.id.empty_txt);
mRecycleView = (RecyclerView) view.findViewById(R.id.media_recycleview);
mRecycleView.setHasFixedSize(true);
mLoadingView = (ProgressBar) view.findViewById(R.id.loading);
initRecycleView();
boolean isMultiImageMode = BoxingManager.getInstance().getBoxingConfig().isMultiImageMode();
View multiImageLayout = view.findViewById(R.id.multi_picker_layout);
multiImageLayout.setVisibility(isMultiImageMode ? View.VISIBLE : View.GONE);
if (isMultiImageMode) {
mPreBtn = (Button) view.findViewById(R.id.choose_preview_btn);
mOkBtn = (Button) view.findViewById(R.id.choose_ok_btn);
mPreBtn.setOnClickListener(this);
mOkBtn.setOnClickListener(this);
updateMultiPickerLayoutState(mMediaAdapter.getSelectedMedias());
}
}
private void initRecycleView() {
GridLayoutManager gridLayoutManager = new HackyGridLayoutManager(getActivity(), GRID_COUNT);
gridLayoutManager.setSmoothScrollbarEnabled(true);
mRecycleView.setLayoutManager(gridLayoutManager);
mRecycleView.addItemDecoration(new SpacesItemDecoration(getResources().getDimensionPixelOffset(R.dimen.boxing_media_margin), GRID_COUNT));
mMediaAdapter.setOnCameraClickListener(new OnCameraClickListener());
mMediaAdapter.setOnCheckedListener(new OnMediaCheckedListener());
mMediaAdapter.setOnMediaClickListener(new OnMediaClickListener());
mRecycleView.setAdapter(mMediaAdapter);
mRecycleView.addOnScrollListener(new ScrollListener());
}
@Override
public void showMedia(@Nullable List<BaseMedia> medias, int allCount) {
if (medias == null || isEmptyData(medias)
&& isEmptyData(mMediaAdapter.getAllMedias())) {
showEmptyData();
return;
}
showData();
mMediaAdapter.addAllData(medias);
checkSelectedMedia(medias, mMediaAdapter.getSelectedMedias());
}
private boolean isEmptyData(List<BaseMedia> medias) {
return medias.isEmpty() && !BoxingManager.getInstance().getBoxingConfig().isNeedCamera();
}
private void showEmptyData() {
mLoadingView.setVisibility(View.GONE);
mEmptyTxt.setVisibility(View.VISIBLE);
mRecycleView.setVisibility(View.GONE);
}
private void showData() {
mLoadingView.setVisibility(View.GONE);
mEmptyTxt.setVisibility(View.GONE);
mRecycleView.setVisibility(View.VISIBLE);
}
@Override
public void showAlbum(@Nullable List<AlbumEntity> albums) {
if ((albums == null || albums.isEmpty())
&& mTitleTxt != null) {
mTitleTxt.setCompoundDrawables(null, null, null, null);
mTitleTxt.setOnClickListener(null);
return;
}
mAlbumWindowAdapter.addAllData(albums);
}
public BoxingMediaAdapter getMediaAdapter() {
return mMediaAdapter;
}
@Override
public void clearMedia() {
mMediaAdapter.clearData();
}
private void updateMultiPickerLayoutState(List<BaseMedia> medias) {
updateOkBtnState(medias);
updatePreviewBtnState(medias);
}
private void updatePreviewBtnState(List<BaseMedia> medias) {
if (mPreBtn == null || medias == null) {
return;
}
boolean enabled = medias.size() > 0 && medias.size() <= mMaxCount;
mPreBtn.setEnabled(enabled);
}
private void updateOkBtnState(List<BaseMedia> medias) {
if (mOkBtn == null || medias == null) {
return;
}
boolean enabled = medias.size() > 0 && medias.size() <= mMaxCount;
mOkBtn.setEnabled(enabled);
mOkBtn.setText(enabled ? getString(R.string.boxing_image_select_ok_fmt, String.valueOf(medias.size())
, String.valueOf(mMaxCount)) : getString(R.string.boxing_ok));
}
@Override
public void onCameraFinish(BaseMedia media) {
dismissProgressDialog();
mIsCamera = false;
if (media == null) {
return;
}
if (hasCropBehavior()) {
startCrop(media, IMAGE_CROP_REQUEST_CODE);
} else if (mMediaAdapter != null && mMediaAdapter.getSelectedMedias() != null) {
List<BaseMedia> selectedMedias = mMediaAdapter.getSelectedMedias();
selectedMedias.add(media);
onFinish(selectedMedias);
}
}
@Override
public void onCameraError() {
mIsCamera = false;
dismissProgressDialog();
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.choose_ok_btn) {
onFinish(mMediaAdapter.getSelectedMedias());
} else if (id == R.id.choose_preview_btn) {
if (!mIsPreview) {
mIsPreview = true;
ArrayList<BaseMedia> medias = (ArrayList<BaseMedia>) mMediaAdapter.getSelectedMedias();
Boxing.get().withIntent(getActivity(), BoxingViewActivity.class, medias)
.start(this, BoxingViewFragment.IMAGE_PREVIEW_REQUEST_CODE, BoxingConfig.ViewMode.PRE_EDIT);
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) {
return;
}
if (resultCode == Activity.RESULT_OK && requestCode == IMAGE_PREVIEW_REQUEST_CODE) {
mIsPreview = false;
boolean isBackClick = data.getBooleanExtra(BoxingViewActivity.EXTRA_TYPE_BACK, false);
List<BaseMedia> selectedMedias = data.getParcelableArrayListExtra(Boxing.EXTRA_SELECTED_MEDIA);
onViewActivityRequest(selectedMedias, mMediaAdapter.getAllMedias(), isBackClick);
if (isBackClick) {
mMediaAdapter.setSelectedMedias(selectedMedias);
}
updateMultiPickerLayoutState(selectedMedias);
}
}
private void onViewActivityRequest(List<BaseMedia> selectedMedias, List<BaseMedia> allMedias, boolean isBackClick) {
if (isBackClick) {
checkSelectedMedia(allMedias, selectedMedias);
} else {
onFinish(selectedMedias);
}
}
@Override
public void onCameraActivityResult(int requestCode, int resultCode) {
showProgressDialog();
super.onCameraActivityResult(requestCode, resultCode);
}
private void showProgressDialog() {
if (mDialog == null) {
mDialog = new ProgressDialog(getActivity());
mDialog.setIndeterminate(true);
mDialog.setMessage(getString(R.string.boxing_handling));
}
if (!mDialog.isShowing()) {
mDialog.show();
}
}
private void dismissProgressDialog() {
if (mDialog != null && mDialog.isShowing()) {
mDialog.hide();
mDialog.dismiss();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ArrayList<BaseMedia> medias = (ArrayList<BaseMedia>) getMediaAdapter().getSelectedMedias();
onSaveMedias(outState, medias);
}
public void setTitleTxt(TextView titleTxt) {
mTitleTxt = titleTxt;
mTitleTxt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mAlbumPopWindow == null) {
int height = WindowManagerHelper.getScreenHeight(v.getContext()) -
(WindowManagerHelper.getToolbarHeight(v.getContext())
+ WindowManagerHelper.getStatusBarHeight(v.getContext()));
View windowView = createWindowView();
mAlbumPopWindow = new PopupWindow(windowView, ViewGroup.LayoutParams.MATCH_PARENT,
height, true);
mAlbumPopWindow.setAnimationStyle(R.style.Boxing_PopupAnimation);
mAlbumPopWindow.setOutsideTouchable(true);
mAlbumPopWindow.setBackgroundDrawable(new ColorDrawable
(ContextCompat.getColor(v.getContext(), R.color.boxing_colorPrimaryAlpha)));
mAlbumPopWindow.setContentView(windowView);
}
mAlbumPopWindow.showAsDropDown(v, 0, 0);
}
@NonNull
private View createWindowView() {
View view = LayoutInflater.from(getActivity()).inflate(R.layout.layout_boxing_album, null);
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.album_recycleview);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false));
recyclerView.addItemDecoration(new SpacesItemDecoration(2, 1));
View albumShadowLayout = view.findViewById(R.id.album_shadow);
albumShadowLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismissAlbumWindow();
}
});
mAlbumWindowAdapter.setAlbumOnClickListener(new OnAlbumItemOnClickListener());
recyclerView.setAdapter(mAlbumWindowAdapter);
return view;
}
});
}
private void dismissAlbumWindow() {
if (mAlbumPopWindow != null && mAlbumPopWindow.isShowing()) {
mAlbumPopWindow.dismiss();
}
}
private class ScrollListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
final int childCount = recyclerView.getChildCount();
if (childCount > 0) {
View lastChild = recyclerView.getChildAt(childCount - 1);
RecyclerView.Adapter outerAdapter = recyclerView.getAdapter();
int lastVisible = recyclerView.getChildAdapterPosition(lastChild);
if (lastVisible == outerAdapter.getItemCount() - 1 && hasNextPage() && canLoadNextPage()) {
onLoadNextPage();
}
}
}
}
private class OnMediaClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
BaseMedia media = (BaseMedia) v.getTag();
int pos = (int) v.getTag(R.id.media_item_check);
BoxingConfig.Mode mode = BoxingManager.getInstance().getBoxingConfig().getMode();
if (mode == BoxingConfig.Mode.SINGLE_IMG) {
singleImageClick(media);
} else if (mode == BoxingConfig.Mode.MULTI_IMG) {
multiImageClick(pos);
} else if (mode == BoxingConfig.Mode.VIDEO) {
videoClick(media);
}
}
private void videoClick(BaseMedia media) {
ArrayList<BaseMedia> iMedias = new ArrayList<>();
iMedias.add(media);
onFinish(iMedias);
}
private void multiImageClick(int pos) {
if (!mIsPreview) {
AlbumEntity albumMedia = mAlbumWindowAdapter.getCurrentAlbum();
String albumId = albumMedia != null ? albumMedia.mBucketId : AlbumEntity.DEFAULT_NAME;
mIsPreview = true;
ArrayList<BaseMedia> medias = (ArrayList<BaseMedia>) mMediaAdapter.getSelectedMedias();
Boxing.get().withIntent(getContext(), BoxingViewActivity.class, medias, pos, albumId)
.start(BoxingViewFragment.this, BoxingViewFragment.IMAGE_PREVIEW_REQUEST_CODE, BoxingConfig.ViewMode.EDIT);
}
}
private void singleImageClick(BaseMedia media) {
ArrayList<BaseMedia> iMedias = new ArrayList<>();
iMedias.add(media);
if (hasCropBehavior()) {
startCrop(media, IMAGE_CROP_REQUEST_CODE);
} else {
onFinish(iMedias);
}
}
}
private class OnCameraClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (!mIsCamera) {
mIsCamera = true;
startCamera(getActivity(), BoxingViewFragment.this, BoxingFileHelper.DEFAULT_SUB_DIR);
}
}
}
private class OnMediaCheckedListener implements BoxingMediaAdapter.OnMediaCheckedListener {
@Override
public void onChecked(View view, BaseMedia iMedia) {
if (!(iMedia instanceof ImageMedia)) {
return;
}
ImageMedia photoMedia = (ImageMedia) iMedia;
boolean isSelected = !photoMedia.isSelected();
MediaItemLayout layout = (MediaItemLayout) view;
List<BaseMedia> selectedMedias = mMediaAdapter.getSelectedMedias();
if (isSelected) {
if (selectedMedias.size() >= mMaxCount) {
String warning = getString(R.string.boxing_too_many_picture_fmt, mMaxCount);
Toast.makeText(getActivity(), warning, Toast.LENGTH_SHORT).show();
return;
}
if (!selectedMedias.contains(photoMedia)) {
if (photoMedia.isGifOverSize()) {
Toast.makeText(getActivity(), R.string.boxing_gif_too_big, Toast.LENGTH_SHORT).show();
return;
}
selectedMedias.add(photoMedia);
}
} else {
if (selectedMedias.size() >= 1 && selectedMedias.contains(photoMedia)) {
selectedMedias.remove(photoMedia);
}
}
photoMedia.setSelected(isSelected);
layout.setChecked(isSelected);
updateMultiPickerLayoutState(selectedMedias);
}
}
private class OnAlbumItemOnClickListener implements BoxingAlbumAdapter.OnAlbumClickListener {
@Override
public void onClick(View view, int pos) {
BoxingAlbumAdapter adapter = mAlbumWindowAdapter;
if (adapter != null && adapter.getCurrentAlbumPos() != pos) {
List<AlbumEntity> albums = adapter.getAlums();
adapter.setCurrentAlbumPos(pos);
AlbumEntity albumMedia = albums.get(pos);
loadMedias(0, albumMedia.mBucketId);
mTitleTxt.setText(albumMedia.mBucketName == null ? getString(R.string.boxing_default_album_name) : albumMedia.mBucketName);
for (AlbumEntity album : albums) {
album.mIsSelected = false;
}
albumMedia.mIsSelected = true;
adapter.notifyDataSetChanged();
}
dismissAlbumWindow();
}
}
}

@ -0,0 +1,34 @@
package com.bilibili.boxing_impl.view;
import android.content.Context;
import android.util.AttributeSet;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* Created by ChenSL on 2018/3/22.
*/
public class HackyGridLayoutManager extends GridLayoutManager {
public HackyGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public HackyGridLayoutManager(Context context, int spanCount) {
super(context, spanCount);
}
public HackyGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
super(context, spanCount, orientation, reverseLayout);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
}
}

@ -0,0 +1,67 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
/**
* https://github.com/chrisbanes/PhotoView/issues/35
*/
public class HackyViewPager extends ViewPager {
private boolean mIsLocked;
public HackyViewPager(Context context) {
super(context);
mIsLocked = false;
}
public HackyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
mIsLocked = false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mIsLocked) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return false;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return !mIsLocked && super.onTouchEvent(event);
}
public boolean isLocked() {
return mIsLocked;
}
public void setLocked(boolean isLocked) {
this.mIsLocked = isLocked;
}
}

@ -0,0 +1,162 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.view;
import android.content.Context;
import android.content.res.Configuration;
import android.text.TextUtils;
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 com.bilibili.boxing.BoxingMediaLoader;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.model.entity.impl.VideoMedia;
import com.bilibili.boxing_impl.BoxingResHelper;
import com.bilibili.boxing_impl.WindowManagerHelper;
import com.ztiany.mediaselector.R;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
/**
* A media layout for {@link androidx.recyclerview.widget.RecyclerView} item, including image and video <br/>
*
* @author ChenSL
*/
public class MediaItemLayout extends FrameLayout {
private static final int BIG_IMG_SIZE = 5 * 1024 * 1024;
private ImageView mCheckImg;
private View mVideoLayout;
private View mFontLayout;
private ImageView mCoverImg;
private ScreenType mScreenType;
private enum ScreenType {
SMALL(100), NORMAL(180), LARGE(320);
int value;
ScreenType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public MediaItemLayout(Context context) {
this(context, null, 0);
}
public MediaItemLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MediaItemLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
View view = LayoutInflater.from(context).inflate(R.layout.layout_boxing_media_item, this, true);
mCoverImg = (ImageView) view.findViewById(R.id.media_item);
mCheckImg = (ImageView) view.findViewById(R.id.media_item_check);
mVideoLayout = view.findViewById(R.id.video_layout);
mFontLayout = view.findViewById(R.id.media_font_layout);
mScreenType = getScreenType(context);
setImageRect(context);
}
private void setImageRect(Context context) {
int screenHeight = WindowManagerHelper.getScreenHeight(context);
int screenWidth = WindowManagerHelper.getScreenWidth(context);
int width = 100;
if (screenHeight != 0 && screenWidth != 0) {
width = (screenWidth - getResources().getDimensionPixelOffset(R.dimen.boxing_media_margin) * 4) / 3;
}
mCoverImg.getLayoutParams().width = width;
mCoverImg.getLayoutParams().height = width;
mFontLayout.getLayoutParams().width = width;
mFontLayout.getLayoutParams().height = width;
}
private ScreenType getScreenType(Context context) {
int type = context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
ScreenType result;
switch (type) {
case Configuration.SCREENLAYOUT_SIZE_SMALL:
result = ScreenType.SMALL;
break;
case Configuration.SCREENLAYOUT_SIZE_NORMAL:
result = ScreenType.NORMAL;
break;
case Configuration.SCREENLAYOUT_SIZE_LARGE:
result = ScreenType.LARGE;
break;
default:
result = ScreenType.NORMAL;
break;
}
return result;
}
public void setImageRes(@DrawableRes int imageRes) {
if (mCoverImg != null) {
mCoverImg.setImageResource(imageRes);
}
}
public void setMedia(BaseMedia media) {
if (media instanceof ImageMedia) {
mVideoLayout.setVisibility(GONE);
setCover(((ImageMedia) media).getThumbnailPath());
} else if (media instanceof VideoMedia) {
mVideoLayout.setVisibility(VISIBLE);
VideoMedia videoMedia = (VideoMedia) media;
TextView durationTxt = ((TextView) mVideoLayout.findViewById(R.id.video_duration_txt));
durationTxt.setText(videoMedia.getDuration());
durationTxt.setCompoundDrawablesWithIntrinsicBounds(BoxingManager.getInstance().getBoxingConfig().getVideoDurationRes(), 0, 0, 0);
((TextView) mVideoLayout.findViewById(R.id.video_size_txt)).setText(videoMedia.getSizeByUnit());
setCover(videoMedia.getPath());
}
}
private void setCover(@NonNull String path) {
if (mCoverImg == null || TextUtils.isEmpty(path)) {
return;
}
mCoverImg.setTag(R.string.boxing_app_name, path);
BoxingMediaLoader.getInstance().displayThumbnail(mCoverImg, path, mScreenType.getValue(), mScreenType.getValue());
}
@SuppressWarnings("deprecation")
public void setChecked(boolean isChecked) {
if (isChecked) {
mFontLayout.setVisibility(View.VISIBLE);
mCheckImg.setImageDrawable(getResources().getDrawable(BoxingResHelper.getMediaCheckedRes()));
} else {
mFontLayout.setVisibility(View.GONE);
mCheckImg.setImageDrawable(getResources().getDrawable(BoxingResHelper.getMediaUncheckedRes()));
}
}
}

@ -0,0 +1,103 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing_impl.view;
import android.graphics.Rect;
import android.view.View;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
/**
* @author ChenSL
*/
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int mSpace;
private int mSpanCount;
private int mRadixX;
private int mItemCountInLastLine;
private int mOldItemCount = -1;
public SpacesItemDecoration(int space) {
this(space, 1);
}
public SpacesItemDecoration(int space, int spanCount) {
this.mSpace = space;
this.mSpanCount = spanCount;
this.mRadixX = space / spanCount;
}
@Override
public void getItemOffsets(Rect outRect, View view, final RecyclerView parent, RecyclerView.State state) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
final int sumCount = state.getItemCount();
final int position = params.getViewLayoutPosition();
final int spanSize;
final int index;
if (params instanceof GridLayoutManager.LayoutParams) {
GridLayoutManager.LayoutParams gridParams = (GridLayoutManager.LayoutParams) params;
spanSize = gridParams.getSpanSize();
index = gridParams.getSpanIndex();
if ((position == 0 || mOldItemCount != sumCount) && mSpanCount > 1) {
int countInLine = 0;
int spanIndex;
for (int tempPosition = sumCount - mSpanCount; tempPosition < sumCount; tempPosition++) {
spanIndex = ((GridLayoutManager) parent.getLayoutManager()).getSpanSizeLookup().getSpanIndex(tempPosition, mSpanCount);
countInLine = spanIndex == 0 ? 1 : countInLine + 1;
}
mItemCountInLastLine = countInLine;
if (mOldItemCount != sumCount) {
mOldItemCount = sumCount;
if (position != 0) {
parent.post(new Runnable() {
@Override
public void run() {
parent.invalidateItemDecorations();
}
});
}
}
}
} else if (params instanceof StaggeredGridLayoutManager.LayoutParams) {
spanSize = ((StaggeredGridLayoutManager.LayoutParams) params).isFullSpan() ? mSpanCount : 1;
index = ((StaggeredGridLayoutManager.LayoutParams) params).getSpanIndex();
} else {
spanSize = 1;
index = 0;
}
if (spanSize < 1 || index < 0 || spanSize > mSpanCount) {
return;
}
outRect.left = mSpace - mRadixX * index;
outRect.right = mRadixX + mRadixX * (index + spanSize - 1);
if (mSpanCount == 1 && position == sumCount - 1) {
outRect.bottom = mSpace;
} else if (position >= sumCount - mItemCountInLastLine && position < sumCount) {
outRect.bottom = mSpace;
}
outRect.top = mSpace;
}
}

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha
android:duration="150"
android:fromAlpha="0.1"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="1.0" />
</set>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha
android:duration="150"
android:fromAlpha="1.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="0.0" />
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_pressed="true">
<shape>
<corners android:radius="@dimen/boxing_corner_radius" />
<solid android:color="@color/boxing_black" />
</shape>
</item>
<item android:state_enabled="true">
<shape>
<solid android:color="@color/boxing_black" />
<corners android:radius="@dimen/boxing_corner_radius" />
</shape>
</item>
<item android:state_enabled="false">
<shape>
<solid android:color="@color/boxing_gray" />
<corners android:radius="@dimen/boxing_corner_radius" />
</shape>
</item>
</selector>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="@dimen/boxing_corner_radius" />
<solid
android:color="#fafafa" />
</shape>

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/boxing_colorPrimaryAlpha"/>
<stroke
android:width="0.3dp"
android:color="@color/boxing_white"/>
<size
android:width="18dp"
android:height="18dp"/>
</shape>

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/nav_top_bar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:elevation="2dp"
app:theme="@style/Boxing.ToolbarTheme">
<TextView
android:id="@+id/pick_album_txt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:drawablePadding="8dp"
android:drawableRight="@drawable/abc_spinner_mtrl_am_alpha"
android:gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:text="@string/boxing_default_album"/>
</androidx.appcompat.widget.Toolbar>
<FrameLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/layout_boxing_app_bar"/>
<ImageView
android:id="@+id/media_result"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="@dimen/boxing_activity_horizontal_margin"/>
</LinearLayout>
<FrameLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/boxing_white1"
app:behavior_hideable="true"
app:behavior_peekHeight="300dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/boxing_black1"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/nav_top_bar"
android:layout_width="match_parent"
android:elevation="2dp"
android:layout_height="?android:attr/actionBarSize"
app:theme="@style/Boxing.ToolbarTheme">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"/>
</androidx.appcompat.widget.Toolbar>
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:indeterminate="true"/>
<com.bilibili.boxing_impl.view.HackyViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/item_choose_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:elevation="2dp">
<Button
android:id="@+id/image_items_ok"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:layout_marginRight="@dimen/boxing_item_spacing"
android:minWidth="60dp"
android:paddingLeft="@dimen/boxing_item_half_spacing"
android:paddingRight="@dimen/boxing_item_half_spacing"
android:text="@string/boxing_ok"
android:background="@drawable/selector_boxing_btn_solid"
android:textColor="@color/boxing_white"/>
</FrameLayout>
</LinearLayout>

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_recycleview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"
android:scrollbars="vertical"/>
<include
android:id="@+id/empty_txt"
layout="@layout/layout_boxing_empty_txt"/>
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:indeterminate="true"/>
<FrameLayout
android:id="@+id/multi_picker_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/boxing_white">
<Button
android:id="@+id/choose_preview_btn"
android:layout_width="60dp"
android:layout_height="30dp"
android:layout_gravity="left|center_vertical"
android:layout_marginLeft="@dimen/boxing_item_spacing"
android:background="@drawable/selector_boxing_btn_solid"
android:text="@string/boxing_preview"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"
android:textColor="@color/boxing_white"/>
<Button
android:id="@+id/choose_ok_btn"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:layout_marginRight="@dimen/boxing_item_spacing"
android:background="@drawable/selector_boxing_btn_solid"
android:minWidth="60dp"
android:paddingLeft="@dimen/boxing_item_half_spacing"
android:paddingRight="@dimen/boxing_item_half_spacing"
android:text="@string/boxing_ok"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"
android:textColor="@color/boxing_white"/>
</FrameLayout>
</LinearLayout>

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/boxing_black"
android:padding="@dimen/boxing_activity_vertical_margin">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="left"
android:text="@string/boxing_default_album"
android:textColor="@color/boxing_white"/>
<TextView
android:id="@+id/finish_txt"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="right"
android:text="@string/boxing_finish"
android:textColor="@color/boxing_white"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_recycleview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/boxing_white1"
android:visibility="gone"/>
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_gravity="center"
android:indeterminate="true"/>
<include
android:id="@+id/empty_txt"
layout="@layout/layout_boxing_empty_txt"/>
</LinearLayout>

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<uk.co.senab.photoview.PhotoView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="@dimen/boxing_item_half_spacing"/>
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
</FrameLayout>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/album_recycleview"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/boxing_white"
android:scrollbars="vertical"/>
<View
android:id="@+id/album_shadow"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/boxing_colorPrimaryAlpha"/>
</LinearLayout>

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/album_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?android:selectableItemBackground"
android:paddingTop="@dimen/boxing_item_spacing">
<ImageView
android:id="@+id/album_thumbnail"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/album_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/album_thumbnail"
android:ellipsize="end"
android:maxEms="12"
android:singleLine="true"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"
android:textColor="@color/boxing_gray1"
tools:text="My Favorite Album"/>
<TextView
android:id="@+id/album_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="4dp"
android:layout_toRightOf="@id/album_name"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"
android:textColor="@color/boxing_gray1"
tools:text="(980)"/>
<ImageView
android:id="@+id/album_checked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="8dp"
android:src="@drawable/ic_boxing_check_black"
android:visibility="gone"
tools:visibility="visible"/>
</RelativeLayout>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<androidx.appcompat.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_top_bar"
android:layout_width="match_parent"
android:layout_height="56dp"
app:theme="@style/Boxing.ToolbarTheme"/>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/empty_txt"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="center"
android:text="@string/boxing_nothing_found"
android:textColor="@color/boxing_black"
android:visibility="gone"
tools:visibility="visible"/>

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/media_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"/>
<ImageView
android:id="@+id/media_item_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:paddingTop="6dp"
android:paddingRight="6dp"
android:paddingBottom="12dp"
android:paddingLeft="12dp"
android:src="@drawable/shape_boxing_unchecked"/>
<FrameLayout
android:id="@+id/video_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/boxing_colorPrimaryAlpha"
android:paddingBottom="2dp"
android:paddingTop="4dp"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/video_duration_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|left"
android:drawablePadding="4dp"
android:maxEms="6"
android:paddingLeft="4dp"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"
android:textColor="@color/boxing_white"
tools:text="00:30"/>
<TextView
android:id="@+id/video_size_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:maxEms="4"
android:paddingRight="4dp"
android:textAppearance="@style/Boxing.TextAppearance.App.Medium"
android:textColor="@color/boxing_white"
tools:text="20M"/>
</FrameLayout>
<View
android:id="@+id/media_font_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/boxing_black_alpha15"/>
</merge>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/camera_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/boxing_black"
android:foreground="?android:selectableItemBackground"
android:minHeight="100dp"
android:minWidth="100dp">
<ImageView
android:id="@+id/camera_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/boxing_white"
android:textSize="14sp"/>
</RelativeLayout>

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<com.bilibili.boxing_impl.view.MediaItemLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="centerCrop"/>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_image_item_selected"
android:icon="@drawable/shape_boxing_unchecked"
android:title=""
app:showAsAction="always" />
</menu>

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<resources>
<string name="boxing_ok">OK</string>
<string name="boxing_fab_label_camera">take a shot</string>
<string name="boxing_image_preview_title_fmt">%1$s/%2$s</string>
<string name="boxing_image_preview_ok_fmt">%1$s/%2$s OK</string>
<string name="boxing_image_select_ok_fmt">(%1$s/%2$s) OK</string>
<string name="boxing_max_image_over_fmt">%d picture is limited</string>
<string name="boxing_gif_too_big">the gif is too big!</string>
<string name="boxing_pick_single_video">Pick a video</string>
<string name="boxing_pick_multi_image">Pick pictures</string>
<string name="boxing_pick_single_image_crop">Pick a picture with crop</string>
<string name="boxing_pick_single_image">Pick a picture</string>
<string name="boxing_video_title">videos</string>
<string name="boxing_storage_deny">Device storage read error or temporarily unavailable. Please try again later</string>
<string name="boxing_finish">finish</string>
<string name="boxing_start_pick">start</string>
<string name="boxing_load_image_fail">load fail!</string>
<string name="boxing_storage_permission_deny">You need to access the storage device to select the picture
, please allow "storage space" right in the "System Settings" dialog box or authorization.</string>
<string name="boxing_camera_permission_deny">To access your camera to take pictures,
enable the "Use Camera" permission in the "System Settings" or Authorization dialog box.</string>
<string name="boxing_default_album">Pictures</string>
<string name="boxing_crop_error">sorry, error occurs when cropping</string>
<string name="boxing_nothing_found">nothing!</string>
<string name="boxing_too_many_picture_fmt">You can only select up to %d pictures</string>
<string name="boxing_default_album_name">Pictures</string>
<string name="boxing_preview">Preview</string>
</resources>

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<resources>
<color name="boxing_colorPrimary">#000000</color>
<color name="boxing_colorPrimaryDark">#ff212121</color>
<color name="boxing_colorPrimaryAlpha">#44000000</color>
<color name="boxing_colorAccent">#FF4081</color>
<color name="boxing_gray">#999999</color>
<color name="boxing_gray1">#333333</color>
<color name="boxing_white">#ffffff</color>
<color name="boxing_white1">#fafafa</color>
<color name="boxing_white2">#eaeaea</color>
<color name="boxing_black">#000000</color>
<color name="boxing_black1">#1b1b1b</color>
<color name="boxing_black_alpha15">#26000000</color>
<color name="boxing_black_alpha20">#33000000</color>
<color name="boxing_black_alpha25">#3c000000</color>
</resources>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<resources>
<dimen name="boxing_activity_horizontal_margin">16dp</dimen>
<dimen name="boxing_activity_vertical_margin">16dp</dimen>
<dimen name="boxing_text_size_xlarge">16sp</dimen>
<dimen name="boxing_text_size_large">14sp</dimen>
<dimen name="boxing_text_size_medium">12sp</dimen>
<dimen name="boxing_text_size_small">10sp</dimen>
<dimen name="boxing_item_spacing">8dp</dimen>
<dimen name="boxing_item_half_spacing">4dp</dimen>
<dimen name="boxing_media_margin">2dp</dimen>
<dimen name="boxing_corner_radius">2dp</dimen>
</resources>

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<resources>
<string name="boxing_app_name">Boxing</string>
<string name="boxing_ok">确定</string>
<string name="boxing_fab_label_camera">拍照</string>
<string name="boxing_image_preview_title_fmt">%1$s/%2$s</string>
<string name="boxing_image_preview_ok_fmt">%1$s/%2$s 确定</string>
<string name="boxing_image_select_ok_fmt">(%1$s/%2$s) 确定</string>
<string name="boxing_album_images_fmt">(%d)</string>
<string name="boxing_max_image_over_fmt">你最多只能选择%d张图片</string>
<string name="boxing_gif_too_big">您选择的gif图片过大,请压缩后再上传</string>
<string name="boxing_pick_single_video">视频单选</string>
<string name="boxing_pick_multi_image">图片多选</string>
<string name="boxing_pick_single_image_crop">图片单选加裁剪</string>
<string name="boxing_pick_single_image">图片单选</string>
<string name="boxing_video_title">视频相册</string>
<string name="boxing_finish">完成</string>
<string name="boxing_start_pick">开始选图</string>
<string name="boxing_load_image_fail">肥肠抱歉,加载出错啦</string>
<string name="boxing_storage_permission_deny">需要访问你的存储设备来选择图片,请在“系统设置”或授权对话框中允许“存储空间”权限。</string>
<string name="boxing_camera_permission_deny">需要访问你的相机来拍照,请在“系统设置”或授权对话框中允许“使用相机”权限。</string>
<string name="boxing_storage_deny">设备存储读取出错或暂不可用,请稍候重试</string>
<string name="boxing_crop_error">肥肠抱歉,裁剪出错啦</string>
<string name="boxing_camera_unavailble">相机不可用,请检查相机权限!</string>
<string name="boxing_handling">处理中</string>
<string name="boxing_default_album">默认相册</string>
<string name="boxing_nothing_found">什么也找不到</string>
<string name="boxing_too_many_picture_fmt">您最多只能选择%d张图片</string>
<string name="boxing_default_album_name">所有相片</string>
<string name="boxing_preview">预览</string>
</resources>

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<resources>
<style name="Boxing.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/boxing_colorPrimary</item>
<item name="colorPrimaryDark">@color/boxing_colorPrimaryDark</item>
<item name="colorAccent">@color/boxing_colorAccent</item>
</style>
<!-- BoxingActivity Theme-->
<style name="Boxing.AppTheme.NoActionBar" parent="Boxing.AppTheme">
<item name="android:windowNoTitle">true</item>
<item name="android:fitsSystemWindows">false</item>
</style>
<style name="Boxing.PopupAnimation" parent="android:Animation">
<item name="android:windowEnterAnimation">@anim/boxing_fade_in</item>
<item name="android:windowExitAnimation">@anim/boxing_fade_out</item>
</style>
<style name="Boxing.ToolbarTheme" parent="Boxing.AppTheme.NoActionBar" >
<item name="titleTextColor">@color/boxing_white</item>
<item name="android:textColorSecondary">@color/boxing_white</item>
<item name="android:textColor">@color/boxing_white</item>
<item name="android:background">@color/boxing_colorPrimaryDark</item>
</style>
<!-- TextAppearance -->
<style name="Boxing.TextAppearance.App.Medium" parent="TextAppearance.AppCompat">
<item name="android:textSize">@dimen/boxing_text_size_medium</item>
</style>
</resources>

@ -0,0 +1,67 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing;
import android.content.Intent;
import android.os.Bundle;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.presenter.PickerContract;
import com.bilibili.boxing.presenter.PickerPresenter;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
/**
* A abstract class to connect {@link com.bilibili.boxing.presenter.PickerContract.View} and {@link com.bilibili.boxing.presenter.PickerContract.Presenter}.
* one job has to be done. override {@link #onCreateBoxingView(ArrayList)} to create a subclass for {@link AbsBoxingViewFragment}.
*
* @author ChenSL
*/
public abstract class AbsBoxingActivity extends AppCompatActivity implements Boxing.OnBoxingFinishListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AbsBoxingViewFragment view = onCreateBoxingView(getSelectedMedias(getIntent()));
BoxingConfig pickerConfig = BoxingManager.getInstance().getBoxingConfig();
view.setPresenter(new PickerPresenter(view));
view.setPickerConfig(pickerConfig);
Boxing.get().setupFragment(view, this);
}
private ArrayList<BaseMedia> getSelectedMedias(Intent intent) {
return intent.getParcelableArrayListExtra(Boxing.EXTRA_SELECTED_MEDIA);
}
public BoxingConfig getBoxingConfig() {
return BoxingManager.getInstance().getBoxingConfig();
}
/**
* create a {@link PickerContract.View} attaching to
* {@link PickerContract.Presenter},call in {@link #onCreate(Bundle)}
*/
@NonNull
public abstract AbsBoxingViewFragment onCreateBoxingView(ArrayList<BaseMedia> medias);
}

@ -0,0 +1,241 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing;
import android.content.ContentResolver;
import android.content.Intent;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.widget.ImageView;
import com.bilibili.boxing.loader.IBoxingCallback;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.presenter.PickerContract;
import com.bilibili.boxing.presenter.PickerPresenter;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
/**
* A abstract class which implements {@link PickerContract.View} for custom media view.
* For view big images.
*
* @author ChenSL
*/
public abstract class AbsBoxingViewActivity extends AppCompatActivity implements PickerContract.View {
ArrayList<BaseMedia> mSelectedImages;
String mAlbumId;
int mStartPos;
private PickerContract.Presenter mPresenter;
/**
* start loading when the permission request is completed.
* call {@link #loadMedias()} or {@link #loadMedias(int, String)}.
*/
public abstract void startLoading();
/**
* override this method to handle the medias.
* make sure {@link #loadMedias()} ()} being called first.
*
* @param medias the results of medias
*/
@Override
public void showMedia(@Nullable List<BaseMedia> medias, int allCount) {
}
@Override
public void showAlbum(@Nullable List<AlbumEntity> albums) {
}
/**
* to clear all medias the first time(the page number is 0). do some clean work.
*/
@Override
public void clearMedia() {
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BoxingConfig config;
if (savedInstanceState != null) {
config = savedInstanceState.getParcelable(Boxing.EXTRA_CONFIG);
} else {
config = BoxingManager.getInstance().getBoxingConfig();
}
setPickerConfig(config);
parseSelectedMedias(savedInstanceState, getIntent());
setPresenter(new PickerPresenter(this));
}
private void parseSelectedMedias(Bundle savedInstanceState, Intent intent) {
if (savedInstanceState != null) {
mSelectedImages = savedInstanceState.getParcelableArrayList(Boxing.EXTRA_SELECTED_MEDIA);
mAlbumId = savedInstanceState.getString(Boxing.EXTRA_ALBUM_ID);
mStartPos = savedInstanceState.getInt(Boxing.EXTRA_START_POS, 0);
} else if (intent != null) {
mStartPos = intent.getIntExtra(Boxing.EXTRA_START_POS, 0);
mSelectedImages = intent.getParcelableArrayListExtra(Boxing.EXTRA_SELECTED_MEDIA);
mAlbumId = intent.getStringExtra(Boxing.EXTRA_ALBUM_ID);
}
}
@Override
public final void setPresenter(@NonNull PickerContract.Presenter presenter) {
this.mPresenter = presenter;
}
/**
* get the {@link ContentResolver}
*/
@NonNull
@Override
public final ContentResolver getAppCr() {
return getApplicationContext().getContentResolver();
}
public final void loadRawImage(@NonNull ImageView img, @NonNull String path, int width, int height, IBoxingCallback callback) {
BoxingMediaLoader.getInstance().displayRaw(img, path, width, height, callback);
}
/**
* called the job is done.Click the ok button, take a photo from camera, crop a photo.
* most of the time, you do not have to override.
*
* @param medias the list of selection
*/
@Override
public void onFinish(@NonNull List<BaseMedia> medias) {
Intent intent = new Intent();
intent.putParcelableArrayListExtra(Boxing.EXTRA_RESULT, (ArrayList<BaseMedia>) medias);
}
/**
* need crop or not
*
* @return true, need it.
*/
public final boolean hasCropBehavior() {
BoxingConfig config = BoxingManager.getInstance().getBoxingConfig();
return config != null && config.isSingleImageMode() && config.getCropOption() != null;
}
/**
* to start the crop behavior, call it when {@link #hasCropBehavior()} return true.
*
* @param media the media to be cropped.
* @param requestCode The integer request code originally supplied to
* startActivityForResult(), allowing you to identify who this
* result came from.
*/
@Override
public final void startCrop(@NonNull BaseMedia media, int requestCode) {
}
/**
* set or update the config.most of the time, you do not have to call it.
*
* @param config {@link BoxingConfig}
*/
@Override
public final void setPickerConfig(BoxingConfig config) {
if (config == null) {
return;
}
BoxingManager.getInstance().setBoxingConfig(config);
}
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
outState.putParcelable(Boxing.EXTRA_CONFIG, BoxingManager.getInstance().getBoxingConfig());
}
/**
* call this to clear resource.
*/
@Override
public void onDestroy() {
super.onDestroy();
if (mPresenter != null) {
mPresenter.destroy();
}
}
/**
* in {@link BoxingConfig.Mode#MULTI_IMG}, call this to pick the selected medias in all medias.
*/
public final void checkSelectedMedia(List<BaseMedia> allMedias, List<BaseMedia> selectedMedias) {
mPresenter.checkSelectedMedia(allMedias, selectedMedias);
}
/**
* load first page of medias.
* use {@link #showMedia(List, int)} to get the result.
*/
public final void loadMedias() {
mPresenter.loadMedias(0, AlbumEntity.DEFAULT_NAME);
}
/**
* load the medias for the specify page and album id.
* use {@link #showMedia(List, int)} to get the result.
*
* @param page page numbers.
* @param albumId the album id is {@link AlbumEntity#mBucketId}.
*/
public final void loadMedias(int page, String albumId) {
mPresenter.loadMedias(page, albumId);
}
/**
* get the max count set before
*/
public final int getMaxCount() {
BoxingConfig config = BoxingManager.getInstance().getBoxingConfig();
if (config == null) {
return BoxingConfig.DEFAULT_SELECTED_COUNT;
}
return config.getMaxCount();
}
@NonNull
public final ArrayList<BaseMedia> getSelectedImages() {
if (mSelectedImages != null) {
return mSelectedImages;
}
return new ArrayList<>();
}
public final String getAlbumId() {
return mAlbumId;
}
public final int getStartPos() {
return mStartPos;
}
}

@ -0,0 +1,485 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing;
import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import com.bilibili.boxing.model.BoxingBuilderConfig;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.config.BoxingCropOption;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.presenter.PickerContract;
import com.bilibili.boxing.utils.CameraPickerHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
/**
* A abstract class which implements {@link PickerContract.View} for custom media view.
* only one methods need to override {@link #startLoading()}, but there is more function to achieve by
* checking every method can override.
*
* @author ChenSL
*/
public abstract class AbsBoxingViewFragment extends Fragment implements PickerContract.View {
public static final String[] STORAGE_PERMISSIONS =
{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
public static final String[] CAMERA_PERMISSIONS = {Manifest.permission.CAMERA};
private static final int REQUEST_CODE_PERMISSION = 233;
private PickerContract.Presenter mPresenter;
private CameraPickerHelper mCameraPicker;
private Boxing.OnBoxingFinishListener mOnFinishListener;
/**
* start loading when the permission request is completed.
* call {@link #loadMedias()} or {@link #loadMedias(int, String)}, call {@link #loadAlbum()} if albums needed.
*/
public abstract void startLoading();
/**
* called when request {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} and {@link Manifest.permission#CAMERA} permission error.
*
* @param e a IllegalArgumentException, IllegalStateException or SecurityException will be throw
*/
public void onRequestPermissionError(String[] permissions, Exception e) {
}
/**
* called when request {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} and {@link Manifest.permission#CAMERA} permission successfully.
*/
public void onRequestPermissionSuc(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
}
/**
* get the result of using camera to take a photo.
*
* @param media {@link BaseMedia}
*/
public void onCameraFinish(BaseMedia media) {
}
/**
* called when camera start error
*/
public void onCameraError() {
}
/**
* must override when care about the input medias, which means you call {@link #setSelectedBundle(ArrayList)} first.
* this method is called in {@link Fragment#onCreate(Bundle)}, so override this rather than {@link Fragment#onCreate(Bundle)}.
*
* @param bundle If the fragment is being re-created from
* a previous saved state, this is the state.
* @param selectedMedias the input medias, the parameter of {@link #setSelectedBundle(ArrayList)}.
*/
public void onCreateWithSelectedMedias(Bundle bundle, @Nullable List<BaseMedia> selectedMedias) {
}
/**
* override this method to handle the medias.
* make sure {@link #loadMedias()} ()} being called first.
*
* @param medias the results of medias
*/
@Override
public void showMedia(@Nullable List<BaseMedia> medias, int allCount) {
}
/**
* override this method to handle the album.
* make sure {@link #loadAlbum()} being called first.
*
* @param albums the results of albums
*/
@Override
public void showAlbum(@Nullable List<AlbumEntity> albums) {
}
/**
* to clear all medias the first time(the page number is 0). do some clean work.
*/
@Override
public void clearMedia() {
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
BoxingConfig config;
if (savedInstanceState != null) {
config = savedInstanceState.getParcelable(Boxing.EXTRA_CONFIG);
} else {
config = BoxingManager.getInstance().getBoxingConfig();
}
setPickerConfig(config);
onCreateWithSelectedMedias(savedInstanceState, parseSelectedMedias(savedInstanceState, getArguments()));
super.onCreate(savedInstanceState);
initCameraPhotoPicker(savedInstanceState);
}
@Nullable
private ArrayList<BaseMedia> parseSelectedMedias(Bundle savedInstanceState, Bundle argument) {
ArrayList<BaseMedia> selectedMedias = null;
if (savedInstanceState != null) {
selectedMedias = savedInstanceState.getParcelableArrayList(Boxing.EXTRA_SELECTED_MEDIA);
} else if (argument != null) {
selectedMedias = argument.getParcelableArrayList(Boxing.EXTRA_SELECTED_MEDIA);
}
return selectedMedias;
}
private void initCameraPhotoPicker(Bundle savedInstanceState) {
BoxingConfig config = BoxingManager.getInstance().getBoxingConfig();
if (config == null || !config.isNeedCamera()) {
return;
}
mCameraPicker = new CameraPickerHelper(savedInstanceState);
mCameraPicker.setPickCallback(new CameraListener(this));
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
checkPermissionAndLoad();
}
private void checkPermissionAndLoad() {
try {
if (!BoxingBuilderConfig.TESTING && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& ContextCompat.checkSelfPermission(getActivity(), STORAGE_PERMISSIONS[0]) != PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(getActivity(), STORAGE_PERMISSIONS[1]) != PERMISSION_GRANTED) {
requestPermissions(STORAGE_PERMISSIONS, REQUEST_CODE_PERMISSION);
} else {
startLoading();
}
} catch (IllegalArgumentException | IllegalStateException e) {
onRequestPermissionError(STORAGE_PERMISSIONS, e);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (REQUEST_CODE_PERMISSION == requestCode) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
onRequestPermissionSuc(requestCode, permissions, grantResults);
} else {
onRequestPermissionError(permissions,
new SecurityException("request android.permission.READ_EXTERNAL_STORAGE error."));
}
}
}
/**
* called when you have input medias, then call {@link #onCreateWithSelectedMedias(Bundle, List)} to get the input medias.
*
* @param selectedMedias input medias
* @return {@link AbsBoxingViewFragment}
*/
public final AbsBoxingViewFragment setSelectedBundle(ArrayList<BaseMedia> selectedMedias) {
Bundle bundle = new Bundle();
if (selectedMedias != null && !selectedMedias.isEmpty()) {
bundle.putParcelableArrayList(Boxing.EXTRA_SELECTED_MEDIA, selectedMedias);
}
setArguments(bundle);
return this;
}
@Override
public final void setPresenter(@NonNull PickerContract.Presenter presenter) {
this.mPresenter = presenter;
}
/**
* get the {@link ContentResolver}
*/
@NonNull
@Override
public final ContentResolver getAppCr() {
return getActivity().getApplicationContext().getContentResolver();
}
/**
* if {@link AbsBoxingViewFragment} is not working with {@link AbsBoxingActivity}, it needs a listener to call
* when the jobs done.
*
* @param onFinishListener {@link Boxing.OnBoxingFinishListener}
*/
final void setOnFinishListener(Boxing.OnBoxingFinishListener onFinishListener) {
mOnFinishListener = onFinishListener;
}
/**
* called the job is done.Click the ok button, take a photo from camera, crop a photo.
* most of the time, you do not have to override.
*
* @param medias the list of selection
*/
@Override
public void onFinish(@NonNull List<BaseMedia> medias) {
Intent intent = new Intent();
intent.putParcelableArrayListExtra(Boxing.EXTRA_RESULT, (ArrayList<BaseMedia>) medias);
if (mOnFinishListener != null) {
mOnFinishListener.onBoxingFinish(intent, medias);
}
}
/**
* need crop or not
*
* @return true, need it.
*/
public final boolean hasCropBehavior() {
BoxingConfig config = BoxingManager.getInstance().getBoxingConfig();
return config != null && config.isSingleImageMode() && config.getCropOption() != null;
}
/**
* to start the crop behavior, call it when {@link #hasCropBehavior()} return true.
*
* @param media the media to be cropped.
* @param requestCode The integer request code originally supplied to
* startActivityForResult(), allowing you to identify who this
* result came from.
*/
@Override
public final void startCrop(@NonNull BaseMedia media, int requestCode) {
BoxingCropOption cropConfig = BoxingManager.getInstance().getBoxingConfig().getCropOption();
BoxingCrop.getInstance().onStartCrop(getActivity(), this, cropConfig, media.getPath(), requestCode);
}
/**
* set or update the config.most of the time, you do not have to call it.
*
* @param config {@link BoxingConfig}
*/
@Override
public final void setPickerConfig(BoxingConfig config) {
if (config == null) {
return;
}
BoxingManager.getInstance().setBoxingConfig(config);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (mCameraPicker != null && requestCode == CameraPickerHelper.REQ_CODE_CAMERA) {
onCameraActivityResult(requestCode, resultCode);
}
if (hasCropBehavior()) {
onCropActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mCameraPicker != null) {
mCameraPicker.onSaveInstanceState(outState);
}
outState.putParcelable(Boxing.EXTRA_CONFIG, BoxingManager.getInstance().getBoxingConfig());
}
/**
* in {@link BoxingConfig.Mode#MULTI_IMG}, call this in {@link Fragment#onSaveInstanceState(Bundle)}.
*
* @param outState Bundle in which to place your saved state.
* @param selected the selected medias.
*/
public final void onSaveMedias(Bundle outState, ArrayList<BaseMedia> selected) {
if (selected != null && !selected.isEmpty()) {
outState.putParcelableArrayList(Boxing.EXTRA_SELECTED_MEDIA, selected);
}
}
/**
* call this to clear resource.
*/
@Override
public void onDestroy() {
super.onDestroy();
if (mPresenter != null) {
mPresenter.destroy();
}
if (mCameraPicker != null) {
mCameraPicker.release();
}
}
/**
* in {@link BoxingConfig.Mode#MULTI_IMG}, call this to pick the selected medias in all medias.
*/
public final void checkSelectedMedia(List<BaseMedia> allMedias, List<BaseMedia> selectedMedias) {
mPresenter.checkSelectedMedia(allMedias, selectedMedias);
}
/**
* load first page of medias.
* use {@link #showMedia(List, int)} to get the result.
*/
public final void loadMedias() {
mPresenter.loadMedias(0, AlbumEntity.DEFAULT_NAME);
}
/**
* load the medias for the specify page and album id.
* use {@link #showMedia(List, int)} to get the result.
*
* @param page page numbers.
* @param albumId the album id is {@link AlbumEntity#mBucketId}.
*/
public final void loadMedias(int page, String albumId) {
mPresenter.loadMedias(page, albumId);
}
/**
* extra call to load albums in database, use {@link #showAlbum(List)} to get result.
* In {@link BoxingConfig.Mode#VIDEO} it is not necessary.
*/
public void loadAlbum() {
if (!BoxingManager.getInstance().getBoxingConfig().isVideoMode()) {
mPresenter.loadAlbums();
}
}
public final boolean hasNextPage() {
return mPresenter.hasNextPage();
}
public final boolean canLoadNextPage() {
return mPresenter.canLoadNextPage();
}
public final void onLoadNextPage() {
mPresenter.onLoadNextPage();
}
/**
* get the max count set before
*/
public final int getMaxCount() {
BoxingConfig config = BoxingManager.getInstance().getBoxingConfig();
if (config == null) {
return BoxingConfig.DEFAULT_SELECTED_COUNT;
}
return config.getMaxCount();
}
/**
* successfully get result from camera in {@link #onActivityResult(int, int, Intent)}.
* call this after other operations.
*/
public void onCameraActivityResult(int requestCode, int resultCode) {
mCameraPicker.onActivityResult(requestCode, resultCode);
}
/**
* successfully get result from crop in {@link #onActivityResult(int, int, Intent)}
*/
public void onCropActivityResult(int requestCode, int resultCode, @NonNull Intent data) {
Uri output = BoxingCrop.getInstance().onCropFinish(resultCode, data);
if (output != null) {
List<BaseMedia> medias = new ArrayList<>(1);
ImageMedia media = new ImageMedia(String.valueOf(System.currentTimeMillis()), output.getPath());
medias.add(media);
onFinish(medias);
}
}
/**
* start camera to take a photo.
*
* @param activity the caller activity.
* @param fragment the caller fragment, may be null.
* @param subFolderPath the folder name in "DCIM/bili/boxing/"
*/
public final void startCamera(Activity activity, Fragment fragment, String subFolderPath) {
try {
if (!BoxingBuilderConfig.TESTING && ContextCompat.checkSelfPermission(getActivity(), CAMERA_PERMISSIONS[0]) != PERMISSION_GRANTED) {
requestPermissions(CAMERA_PERMISSIONS, REQUEST_CODE_PERMISSION);
} else {
if (!BoxingManager.getInstance().getBoxingConfig().isVideoMode()) {
mCameraPicker.startCamera(activity, fragment, subFolderPath);
}
}
} catch (IllegalArgumentException | IllegalStateException e) {
onRequestPermissionError(CAMERA_PERMISSIONS, e);
}
}
private static final class CameraListener implements CameraPickerHelper.Callback {
private WeakReference<AbsBoxingViewFragment> mWr;
CameraListener(AbsBoxingViewFragment fragment) {
mWr = new WeakReference<>(fragment);
}
@Override
public void onFinish(@NonNull CameraPickerHelper helper) {
AbsBoxingViewFragment fragment = mWr.get();
if (fragment == null) {
return;
}
File file = new File(helper.getSourceFilePath());
if (!file.exists()) {
onError(helper);
return;
}
ImageMedia cameraMedia = new ImageMedia(file);
cameraMedia.saveMediaStore(fragment.getAppCr());
fragment.onCameraFinish(cameraMedia);
}
@Override
public void onError(@NonNull CameraPickerHelper helper) {
AbsBoxingViewFragment fragment = mWr.get();
if (fragment == null) {
return;
}
fragment.onCameraError();
}
}
}

@ -0,0 +1,255 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.presenter.PickerPresenter;
import java.util.ArrayList;
import java.util.List;
/**
* An entry for {@link AbsBoxingActivity} and {@link AbsBoxingViewFragment}.<br/>
* 1.call {@link #of(BoxingConfig)} to pick a mode.<br/>
* 2.to use {@link AbsBoxingActivity} + {@link AbsBoxingViewFragment} combination,
* call {@link #withIntent(Context, Class)} to make a intent and {@link #start(Activity)} to start a new Activity.<br/>
* to use {@link AbsBoxingViewFragment} only, just call {@link #setupFragment(AbsBoxingViewFragment, OnBoxingFinishListener)}.<br/>
* 3 4.to get result from a new Activity, call {@link #getResult(Intent)} in {@link Activity#onActivityResult(int, int, Intent)}.
*
* @author ChenSL
*/
public class Boxing {
public static final String EXTRA_SELECTED_MEDIA = "com.bilibili.boxing.Boxing.selected_media";
public static final String EXTRA_ALBUM_ID = "com.bilibili.boxing.Boxing.album_id";
static final String EXTRA_CONFIG = "com.bilibili.boxing.Boxing.config";
static final String EXTRA_RESULT = "com.bilibili.boxing.Boxing.result";
static final String EXTRA_START_POS = "com.bilibili.boxing.Boxing.start_pos";
private Intent mIntent;
private Boxing(BoxingConfig config) {
BoxingManager.getInstance().setBoxingConfig(config);
this.mIntent = new Intent();
}
/**
* get the media result.
*/
@Nullable
public static ArrayList<BaseMedia> getResult(Intent data) {
if (data != null) {
return data.getParcelableArrayListExtra(EXTRA_RESULT);
}
return null;
}
/**
* call {@link #of(BoxingConfig)} first to specify the mode otherwise {@link BoxingConfig.Mode#MULTI_IMG} is used.<br/>
*/
public static Boxing get() {
BoxingConfig config = BoxingManager.getInstance().getBoxingConfig();
if (config == null) {
config = new BoxingConfig(BoxingConfig.Mode.MULTI_IMG).needGif();
BoxingManager.getInstance().setBoxingConfig(config);
}
return new Boxing(config);
}
/**
* create a boxing entry.
*
* @param config {@link BoxingConfig}
*/
public static Boxing of(BoxingConfig config) {
return new Boxing(config);
}
/**
* create a boxing entry.
*
* @param mode {@link BoxingConfig.Mode}
*/
public static Boxing of(BoxingConfig.Mode mode) {
return new Boxing(new BoxingConfig(mode));
}
/**
* create a boxing entry. use {@link BoxingConfig.Mode#MULTI_IMG}.
*/
public static Boxing of() {
BoxingConfig config = new BoxingConfig(BoxingConfig.Mode.MULTI_IMG).needGif();
return new Boxing(config);
}
/**
* get the intent build by boxing after call {@link #withIntent}.
*/
public Intent getIntent() {
return mIntent;
}
/**
* same as {@link Intent#setClass(Context, Class)}
*/
public Boxing withIntent(Context context, Class<?> cls) {
return withIntent(context, cls, null);
}
/**
* {@link Intent#setClass(Context, Class)} with input medias.
*/
public Boxing withIntent(Context context, Class<?> cls, ArrayList<? extends BaseMedia> selectedMedias) {
mIntent.setClass(context, cls);
if (selectedMedias != null && !selectedMedias.isEmpty()) {
mIntent.putExtra(EXTRA_SELECTED_MEDIA, selectedMedias);
}
return this;
}
/**
* use to start image viewer.
*
* @param medias selected medias.
* @param pos the start position.
*/
public Boxing withIntent(Context context, Class<?> cls, ArrayList<? extends BaseMedia> medias, int pos) {
withIntent(context, cls, medias, pos, "");
return this;
}
/**
* use to start image viewer.
*
* @param medias selected medias.
* @param pos the start position.
* @param albumId the specify album id.
*/
public Boxing withIntent(Context context, Class<?> cls, ArrayList<? extends BaseMedia> medias, int pos, String albumId) {
mIntent.setClass(context, cls);
if (medias != null && !medias.isEmpty()) {
mIntent.putExtra(EXTRA_SELECTED_MEDIA, medias);
}
if (pos >= 0) {
mIntent.putExtra(EXTRA_START_POS, pos);
}
if (albumId != null) {
mIntent.putExtra(EXTRA_ALBUM_ID, albumId);
}
return this;
}
/**
* same as {@link Activity#startActivity(Intent)}
*/
public void start(@NonNull Activity activity) {
activity.startActivity(mIntent);
}
/**
* use to start raw image viewer.
*
* @param viewMode {@link BoxingConfig.ViewMode}
*/
public void start(@NonNull Activity activity, BoxingConfig.ViewMode viewMode) {
BoxingManager.getInstance().getBoxingConfig().withViewer(viewMode);
activity.startActivity(mIntent);
}
/**
* same as {@link Activity#startActivityForResult(Intent, int, Bundle)}
*/
public void start(@NonNull Activity activity, int requestCode) {
activity.startActivityForResult(mIntent, requestCode);
}
/**
* same as {@link Fragment#startActivityForResult(Intent, int, Bundle)}
*/
public void start(@NonNull Fragment fragment, int requestCode) {
fragment.startActivityForResult(mIntent, requestCode);
}
/**
* use to start raw image viewer.
*
* @param viewMode {@link BoxingConfig.ViewMode}
*/
public void start(@NonNull Fragment fragment, int requestCode, BoxingConfig.ViewMode viewMode) {
BoxingManager.getInstance().getBoxingConfig().withViewer(viewMode);
fragment.startActivityForResult(mIntent, requestCode);
}
/**
* same as {@link android.app.Fragment#startActivityForResult(Intent, int, Bundle)}
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void start(@NonNull android.app.Fragment fragment, int requestCode) {
fragment.startActivityForResult(mIntent, requestCode);
}
/**
* use to start raw image viewer.
*
* @param viewMode {@link BoxingConfig.ViewMode}
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void start(@NonNull android.app.Fragment fragment, int requestCode, BoxingConfig.ViewMode viewMode) {
BoxingManager.getInstance().getBoxingConfig().withViewer(viewMode);
fragment.startActivityForResult(mIntent, requestCode);
}
/**
* set up a subclass of {@link AbsBoxingViewFragment} without a {@link AbsBoxingActivity}.
*
* @param fragment subclass of {@link AbsBoxingViewFragment}
* @param onFinishListener a listener fo media result
*/
public void setupFragment(@NonNull AbsBoxingViewFragment fragment, OnBoxingFinishListener onFinishListener) {
fragment.setPresenter(new PickerPresenter(fragment));
fragment.setOnFinishListener(onFinishListener);
}
/**
* work with a subclass of {@link AbsBoxingViewFragment} without a {@link AbsBoxingActivity}.
*/
public interface OnBoxingFinishListener {
/**
* live with {@link com.bilibili.boxing.presenter.PickerContract.View#onFinish(List)}
*
* @param medias the selection of medias.
*/
void onBoxingFinish(Intent intent, @Nullable List<BaseMedia> medias);
}
}

@ -0,0 +1,74 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.bilibili.boxing.loader.IBoxingCrop;
import com.bilibili.boxing.model.config.BoxingCropOption;
/**
* A loader holding {@link IBoxingCrop} to crop images.
*
* @author ChenSL
*/
public class BoxingCrop {
private static final BoxingCrop INSTANCE = new BoxingCrop();
private IBoxingCrop mCrop;
private BoxingCrop() {
}
public static BoxingCrop getInstance() {
return INSTANCE;
}
public void init(@NonNull IBoxingCrop loader) {
this.mCrop = loader;
}
public void onStartCrop(Activity activity, Fragment fragment, @NonNull BoxingCropOption cropConfig,
@NonNull String path, int requestCode) {
if (ensureLoader()) {
throw new IllegalStateException("init method should be called first");
}
if (cropConfig == null) {
throw new IllegalArgumentException("crop config is null.");
}
mCrop.onStartCrop(activity, fragment, cropConfig, path, requestCode);
}
public Uri onCropFinish(int resultCode, Intent data) {
if (ensureLoader()) {
throw new IllegalStateException("init method should be called first");
}
return mCrop.onCropFinish(resultCode, data);
}
public IBoxingCrop getCrop() {
return mCrop;
}
private boolean ensureLoader() {
return mCrop == null;
}
}

@ -0,0 +1,67 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing;
import androidx.annotation.NonNull;
import android.widget.ImageView;
import com.bilibili.boxing.loader.IBoxingCallback;
import com.bilibili.boxing.loader.IBoxingMediaLoader;
/**
* A loader holding {@link IBoxingMediaLoader} to displayThumbnail medias.
*
* @author ChenSL
*/
public class BoxingMediaLoader {
private static final BoxingMediaLoader INSTANCE = new BoxingMediaLoader();
private IBoxingMediaLoader mLoader;
private BoxingMediaLoader() {
}
public static BoxingMediaLoader getInstance() {
return INSTANCE;
}
public void init(@NonNull IBoxingMediaLoader loader) {
this.mLoader = loader;
}
public void displayThumbnail(@NonNull ImageView img, @NonNull String path, int width, int height) {
if (ensureLoader()) {
throw new IllegalStateException("init method should be called first");
}
mLoader.displayThumbnail(img, path, width, height);
}
public void displayRaw(@NonNull ImageView img, @NonNull String path, int width, int height, IBoxingCallback callback) {
if (ensureLoader()) {
throw new IllegalStateException("init method should be called first");
}
mLoader.displayRaw(img, path, width, height, callback);
}
public IBoxingMediaLoader getLoader() {
return mLoader;
}
private boolean ensureLoader() {
return mLoader == null;
}
}

@ -0,0 +1,36 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.loader;
/**
* Simple callback only cares about success/fail.
*
* @author ChenSL
*/
public interface IBoxingCallback {
/**
* Successfully handle a task;
*/
void onSuccess();
/**
* Error happened when running a task;
*/
void onFail(Throwable t);
}

@ -0,0 +1,55 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.loader;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.bilibili.boxing.model.config.BoxingCropOption;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
/**
* Cropping interface.
*
* @author ChenSL
*/
public interface IBoxingCrop {
/***
* start crop operation.
*
* @param cropConfig {@link BoxingCropOption}
* @param path the absolute path of media.
* @param requestCode request code for the crop.
*/
void onStartCrop(Context context, Fragment fragment, @NonNull BoxingCropOption cropConfig,
@NonNull String path, int requestCode);
/**
* get the result of cropping.
*
* @param resultCode the code in {@link android.app.Activity#onActivityResult(int, int, Intent)}
* @param data the data intent
* @return the cropped image uri.
*/
Uri onCropFinish(int resultCode, Intent data);
}

@ -0,0 +1,50 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.loader;
import android.widget.ImageView;
import androidx.annotation.NonNull;
/**
* Define how media display.
*
* @author ChenSL
*/
public interface IBoxingMediaLoader {
/**
* display thumbnail images for a ImageView.
*
* @param img the display ImageView. Through ImageView.getTag(R.string.boxing_app_name) to get the absolute path of the exact path to display.
* @param absPath the absolute path to display, may be out of date when fast scrolling.
* @param width the resize with for the image.
* @param height the resize height for the image.
*/
void displayThumbnail(@NonNull ImageView img, @NonNull String absPath, int width, int height);
/**
* display raw images for a ImageView, need more work to do.
*
* @param img the display ImageView.Through ImageView.getTag(R.string.boxing_app_name) to get the absolute path of the exact path to display.
* @param absPath the absolute path to display, may be out of date when fast scrolling.
* @param width the expected width, 0 means the raw width.
* @param height the expected height, 0 means the raw height.
* @param callback the callback for the load result.
*/
void displayRaw(@NonNull ImageView img, @NonNull String absPath, int width, int height, IBoxingCallback callback);
}

@ -0,0 +1,35 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model;
/**
* Marks for building and running
*
* @author ChenSL
*/
public class BoxingBuilderConfig {
/**
* mark for debug
*/
public static final boolean DEBUG = false;
/**
* mark for unit testing
*/
public static final boolean TESTING = false;
}

@ -0,0 +1,80 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model;
import android.content.ContentResolver;
import androidx.annotation.NonNull;
import com.bilibili.boxing.model.callback.IAlbumTaskCallback;
import com.bilibili.boxing.model.callback.IMediaTaskCallback;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.task.IMediaTask;
import com.bilibili.boxing.model.task.impl.AlbumTask;
import com.bilibili.boxing.model.task.impl.ImageTask;
import com.bilibili.boxing.model.task.impl.VideoTask;
import com.bilibili.boxing.utils.BoxingExecutor;
/**
* The Manager to load {@link IMediaTask} and {@link AlbumTask}, holding {@link BoxingConfig}.
*
* @author ChenSL
*/
public class BoxingManager {
private static final BoxingManager INSTANCE = new BoxingManager();
private BoxingConfig mConfig;
private BoxingManager() {
}
public static BoxingManager getInstance() {
return INSTANCE;
}
public void setBoxingConfig(BoxingConfig config) {
mConfig = config;
}
public BoxingConfig getBoxingConfig() {
return mConfig;
}
public void loadMedia(@NonNull final ContentResolver cr, final int page,
final String id, @NonNull final IMediaTaskCallback callback) {
final IMediaTask task = mConfig.isVideoMode() ? new VideoTask() : new ImageTask();
BoxingExecutor.getInstance().runWorker(new Runnable() {
@Override
public void run() {
task.load(cr, page, id, callback);
}
});
}
public void loadAlbum(@NonNull final ContentResolver cr, @NonNull final IAlbumTaskCallback callback) {
BoxingExecutor.getInstance().runWorker(new Runnable() {
@Override
public void run() {
new AlbumTask().start(cr, callback);
}
});
}
}

@ -0,0 +1,41 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.callback;
import com.bilibili.boxing.model.entity.AlbumEntity;
import java.util.List;
import androidx.annotation.Nullable;
/**
* A callback for load album.
*
* @author ChenSL
*/
public interface IAlbumTaskCallback {
/**
* get all album in database
*
* @param list album list
*/
void postAlbumList(@Nullable List<AlbumEntity> list);
}

@ -0,0 +1,48 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.callback;
import com.bilibili.boxing.model.entity.BaseMedia;
import java.util.List;
import androidx.annotation.Nullable;
/**
* A callback to load {@link BaseMedia}.
*
* @author ChenSL
*/
public interface IMediaTaskCallback<T extends BaseMedia> {
/**
* get a page of medias in a album
*
* @param medias page of medias
* @param count the count for the photo in album
*/
void postMedia(@Nullable List<T> medias, int count);
/**
* judge the path needing filer
*
* @param path photo path
* @return true:be filter
*/
boolean needFilter(String path);
}

@ -0,0 +1,319 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.config;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.DrawableRes;
/**
* The pick config.<br/>
* 1.{@link Mode} is necessary. <br/>
* 2.specify functions: camera, gif, paging. <br/>
* calling {@link #needCamera(int)} to displayThumbnail a camera icon. <br/>
* calling {@link #needGif()} to displayThumbnail gif photos. <br/>
* calling {@link #needPaging(boolean)} to create load medias page by page, by default is true.
*
* @author ChenSL
*/
public class BoxingConfig implements Parcelable {
public static final int DEFAULT_SELECTED_COUNT = 9;
private Mode mMode = Mode.SINGLE_IMG;
private ViewMode mViewMode = ViewMode.PREVIEW;
private BoxingCropOption mCropOption;
private int mMediaPlaceHolderRes;
private int mMediaCheckedRes;
private int mMediaUnCheckedRes;
private int mAlbumPlaceHolderRes;
private int mVideoDurationRes;
private int mCameraRes;
private boolean mNeedCamera;
private boolean mNeedGif;
private boolean mNeedPaging = true;
private int mMaxCount = DEFAULT_SELECTED_COUNT;
public enum Mode {
SINGLE_IMG, MULTI_IMG, VIDEO
}
public enum ViewMode {
PREVIEW, EDIT, PRE_EDIT
}
public BoxingConfig() {
}
public BoxingConfig(Mode mode) {
this.mMode = mode;
}
public boolean isNeedCamera() {
return mNeedCamera;
}
public boolean isNeedPaging() {
return mNeedPaging;
}
public Mode getMode() {
return mMode;
}
public ViewMode getViewMode() {
return mViewMode;
}
public BoxingCropOption getCropOption() {
return mCropOption;
}
/**
* get the max count set by {@link #withMaxCount(int)}, otherwise return 9.
*/
public int getMaxCount() {
if (mMaxCount > 0) {
return mMaxCount;
}
return DEFAULT_SELECTED_COUNT;
}
/**
* get the image drawable resource by {@link BoxingConfig#withMediaPlaceHolderRes(int)}.
* @return >0, set a valid drawable resource; otherwise without a placeholder.
*/
public @DrawableRes
int getMediaPlaceHolderRes() {
return mMediaPlaceHolderRes;
}
/**
* get the media checked drawable resource by {@link BoxingConfig#withMediaCheckedRes(int)}.
* @return >0, set a valid drawable resource; otherwise without a placeholder.
*/
public @DrawableRes int getMediaCheckedRes() {
return mMediaCheckedRes;
}
/**
* get the media unchecked drawable resource by {@link BoxingConfig#withMediaUncheckedRes(int)} (int)}.
* @return >0, set a valid drawable resource; otherwise without a placeholder.
*/
public @DrawableRes int getMediaUnCheckedRes() {
return mMediaUnCheckedRes;
}
/**
* get the media unchecked drawable resource by {@link BoxingConfig#withMediaPlaceHolderRes(int)}.
* @return >0, set a valid drawable resource; otherwise without a placeholder.
*/
public @DrawableRes int getCameraRes() {
return mCameraRes;
}
/**
* get the album drawable resource by {@link BoxingConfig#withAlbumPlaceHolderRes(int)}.
* @return >0, set a valid drawable resource; otherwise without a placeholder.
*/
public @DrawableRes int getAlbumPlaceHolderRes() {
return mAlbumPlaceHolderRes;
}
/**
* get the video drawable resource by {@link BoxingConfig#withVideoDurationRes(int)}.
* @return >0, set a valid drawable resource; otherwise without a placeholder.
*/
public @DrawableRes int getVideoDurationRes() {
return mVideoDurationRes;
}
public boolean isNeedLoading() {
return mViewMode == ViewMode.EDIT;
}
public boolean isNeedEdit() {
return mViewMode != ViewMode.PREVIEW;
}
public boolean isVideoMode() {
return mMode == Mode.VIDEO;
}
public boolean isMultiImageMode() {
return mMode == Mode.MULTI_IMG;
}
public boolean isSingleImageMode() {
return mMode == Mode.SINGLE_IMG;
}
public boolean isNeedGif() {
return mNeedGif;
}
/**
* call this means gif is needed.
*/
public BoxingConfig needGif() {
this.mNeedGif = true;
return this;
}
/**
* set the camera res.
*/
public BoxingConfig needCamera(@DrawableRes int cameraRes) {
this.mCameraRes = cameraRes;
this.mNeedCamera = true;
return this;
}
/**
* call this means paging is needed,by default is true.
*/
public BoxingConfig needPaging(boolean needPaging) {
this.mNeedPaging = needPaging;
return this;
}
public BoxingConfig withViewer(ViewMode viewMode) {
this.mViewMode = viewMode;
return this;
}
public BoxingConfig withCropOption(BoxingCropOption cropOption) {
this.mCropOption = cropOption;
return this;
}
/**
* set the max count of selected medias in {@link Mode#MULTI_IMG}
* @param count max count
*/
public BoxingConfig withMaxCount(int count) {
if (count < 1) {
return this;
}
this.mMaxCount = count;
return this;
}
/**
* set the image placeholder, default 0
*/
public BoxingConfig withMediaPlaceHolderRes(@DrawableRes int mediaPlaceHolderRes) {
this.mMediaPlaceHolderRes = mediaPlaceHolderRes;
return this;
}
/**
* set the image placeholder, otherwise use default drawable.
*/
public BoxingConfig withMediaCheckedRes(@DrawableRes int mediaCheckedResRes) {
this.mMediaCheckedRes = mediaCheckedResRes;
return this;
}
/**
* set the image placeholder, otherwise use default drawable.
*/
public BoxingConfig withMediaUncheckedRes(@DrawableRes int mediaUncheckedRes) {
this.mMediaUnCheckedRes = mediaUncheckedRes;
return this;
}
/**
* set the album placeholder, default 0
*/
public BoxingConfig withAlbumPlaceHolderRes(@DrawableRes int albumPlaceHolderRes) {
this.mAlbumPlaceHolderRes = albumPlaceHolderRes;
return this;
}
/**
* set the video duration resource in video mode, default 0
*/
public BoxingConfig withVideoDurationRes(@DrawableRes int videoDurationRes) {
this.mVideoDurationRes = videoDurationRes;
return this;
}
@Override
public String toString() {
return "BoxingConfig{" +
"mMode=" + mMode +
", mViewMode=" + mViewMode +
'}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mMode == null ? -1 : this.mMode.ordinal());
dest.writeInt(this.mViewMode == null ? -1 : this.mViewMode.ordinal());
dest.writeParcelable(this.mCropOption, flags);
dest.writeInt(this.mMediaPlaceHolderRes);
dest.writeInt(this.mMediaCheckedRes);
dest.writeInt(this.mMediaUnCheckedRes);
dest.writeInt(this.mAlbumPlaceHolderRes);
dest.writeInt(this.mVideoDurationRes);
dest.writeInt(this.mCameraRes);
dest.writeByte(this.mNeedCamera ? (byte) 1 : (byte) 0);
dest.writeByte(this.mNeedGif ? (byte) 1 : (byte) 0);
dest.writeByte(this.mNeedPaging ? (byte) 1 : (byte) 0);
dest.writeInt(this.mMaxCount);
}
protected BoxingConfig(Parcel in) {
int tmpMMode = in.readInt();
this.mMode = tmpMMode == -1 ? null : Mode.values()[tmpMMode];
int tmpMViewMode = in.readInt();
this.mViewMode = tmpMViewMode == -1 ? null : ViewMode.values()[tmpMViewMode];
this.mCropOption = in.readParcelable(BoxingCropOption.class.getClassLoader());
this.mMediaPlaceHolderRes = in.readInt();
this.mMediaCheckedRes = in.readInt();
this.mMediaUnCheckedRes = in.readInt();
this.mAlbumPlaceHolderRes = in.readInt();
this.mVideoDurationRes = in.readInt();
this.mCameraRes = in.readInt();
this.mNeedCamera = in.readByte() != 0;
this.mNeedGif = in.readByte() != 0;
this.mNeedPaging = in.readByte() != 0;
this.mMaxCount = in.readInt();
}
public static final Creator<BoxingConfig> CREATOR = new Creator<BoxingConfig>() {
@Override
public BoxingConfig createFromParcel(Parcel source) {
return new BoxingConfig(source);
}
@Override
public BoxingConfig[] newArray(int size) {
return new BoxingConfig[size];
}
};
}

@ -0,0 +1,118 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.config;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
/**
* The cropping config, a cropped photo uri is needed at least.
*
* @author ChenSL
*/
public class BoxingCropOption implements Parcelable {
private Uri mDestination;
private float mAspectRatioX;
private float mAspectRatioY;
private int mMaxWidth;
private int mMaxHeight;
public BoxingCropOption(Uri destination) {
this.mDestination = destination;
}
public static BoxingCropOption with(@NonNull Uri destination) {
return new BoxingCropOption(destination);
}
public BoxingCropOption aspectRatio(float x, float y) {
this.mAspectRatioX = x;
this.mAspectRatioY = y;
return this;
}
public BoxingCropOption useSourceImageAspectRatio() {
this.mAspectRatioX = 0;
this.mAspectRatioY = 0;
return this;
}
public BoxingCropOption withMaxResultSize(int width, int height) {
this.mMaxWidth = width;
this.mMaxHeight = height;
return this;
}
public float getAspectRatioX() {
return mAspectRatioX;
}
public float getAspectRatioY() {
return mAspectRatioY;
}
public int getMaxHeight() {
return mMaxHeight;
}
public int getMaxWidth() {
return mMaxWidth;
}
public Uri getDestination() {
return mDestination;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(this.mDestination, flags);
dest.writeFloat(this.mAspectRatioX);
dest.writeFloat(this.mAspectRatioY);
dest.writeInt(this.mMaxWidth);
dest.writeInt(this.mMaxHeight);
}
BoxingCropOption(Parcel in) {
this.mDestination = in.readParcelable(Uri.class.getClassLoader());
this.mAspectRatioX = in.readFloat();
this.mAspectRatioY = in.readFloat();
this.mMaxWidth = in.readInt();
this.mMaxHeight = in.readInt();
}
public static final Creator<BoxingCropOption> CREATOR = new Creator<BoxingCropOption>() {
@Override
public BoxingCropOption createFromParcel(Parcel source) {
return new BoxingCropOption(source);
}
@Override
public BoxingCropOption[] newArray(int size) {
return new BoxingCropOption[size];
}
};
}

@ -0,0 +1,102 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.entity;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.List;
/**
* An entity for album.
*
* @author ChenSL
*/
public class AlbumEntity implements Parcelable {
public static final String DEFAULT_NAME = "";
public int mCount;
public boolean mIsSelected;
public String mBucketId;
public String mBucketName;
public List<BaseMedia> mImageList;
public AlbumEntity() {
mCount = 0;
mImageList = new ArrayList<>();
mIsSelected = false;
}
public static AlbumEntity createDefaultAlbum() {
AlbumEntity result = new AlbumEntity();
result.mBucketId = DEFAULT_NAME;
result.mIsSelected = true;
return result;
}
public boolean hasImages() {
return mImageList != null && mImageList.size() > 0;
}
@Override
public String toString() {
return "AlbumEntity{" +
"mCount=" + mCount +
", mBucketName='" + mBucketName + '\'' +
", mImageList=" + mImageList +
'}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.mBucketId);
dest.writeInt(this.mCount);
dest.writeString(this.mBucketName);
dest.writeList(this.mImageList);
dest.writeByte(this.mIsSelected ? (byte) 1 : (byte) 0);
}
protected AlbumEntity(Parcel in) {
this.mBucketId = in.readString();
this.mCount = in.readInt();
this.mBucketName = in.readString();
this.mImageList = new ArrayList<>();
in.readList(this.mImageList, BaseMedia.class.getClassLoader());
this.mIsSelected = in.readByte() != 0;
}
public static final Creator<AlbumEntity> CREATOR = new Creator<AlbumEntity>() {
@Override
public AlbumEntity createFromParcel(Parcel source) {
return new AlbumEntity(source);
}
@Override
public AlbumEntity[] newArray(int size) {
return new AlbumEntity[size];
}
};
}

@ -0,0 +1,94 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.entity;
import android.os.Parcel;
import android.os.Parcelable;
/**
* The base entity for media.
*
* @author ChenSL
*/
public abstract class BaseMedia implements Parcelable {
protected enum TYPE {
IMAGE, VIDEO
}
protected String mPath;
protected String mId;
protected String mSize;
public BaseMedia() {
}
public BaseMedia(String id, String path) {
mId = id;
mPath = path;
}
public abstract TYPE getType();
public String getId() {
return mId;
}
public long getSize() {
try {
long result = Long.parseLong(mSize);
return result > 0 ? result : 0;
}catch (NumberFormatException size) {
return 0;
}
}
public void setId(String id) {
mId = id;
}
public void setSize(String size) {
mSize = size;
}
public String getPath(){
return mPath;
}
public void setPath(String path) {
mPath = path;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.mPath);
dest.writeString(this.mId);
dest.writeString(this.mSize);
}
protected BaseMedia(Parcel in) {
this.mPath = in.readString();
this.mId = in.readString();
this.mSize = in.readString();
}
}

@ -0,0 +1,340 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.entity.impl;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.text.TextUtils;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.utils.BoxingExecutor;
import com.bilibili.boxing.utils.BoxingExifHelper;
import com.bilibili.boxing.utils.BoxingFileHelper;
import com.bilibili.boxing.utils.CompressTask;
import com.bilibili.boxing.utils.ImageCompressor;
import java.io.File;
import androidx.annotation.NonNull;
/**
* Id and absolute path is necessary.Builder Mode can be used too.
* compress image through {@link #compress(ImageCompressor)}.
*
* @author ChenSL
*/
public class ImageMedia extends BaseMedia implements Parcelable {
private static final long MAX_GIF_SIZE = 1024 * 1024L;
private static final long MAX_IMAGE_SIZE = 1024 * 1024L;
private boolean mIsSelected;
private String mThumbnailPath;
private String mCompressPath;
private int mHeight;
private int mWidth;
private IMAGE_TYPE mImageType;
private String mMimeType;
public enum IMAGE_TYPE {
PNG, JPG, GIF
}
public ImageMedia(String id, String imagePath) {
super(id, imagePath);
}
public ImageMedia(@NonNull File file) {
this.mId = String.valueOf(System.currentTimeMillis());
this.mPath = file.getAbsolutePath();
this.mSize = String.valueOf(file.length());
this.mIsSelected = true;
}
public ImageMedia(Builder builder) {
super(builder.mId, builder.mImagePath);
this.mThumbnailPath = builder.mThumbnailPath;
this.mSize = builder.mSize;
this.mHeight = builder.mHeight;
this.mIsSelected = builder.mIsSelected;
this.mWidth = builder.mWidth;
this.mMimeType = builder.mMimeType;
this.mImageType = getImageTypeByMime(builder.mMimeType);
}
@Override
public TYPE getType() {
return TYPE.IMAGE;
}
public boolean isSelected() {
return mIsSelected;
}
public void setSelected(boolean selected) {
mIsSelected = selected;
}
public boolean isGifOverSize() {
return isGif() && getSize() > MAX_GIF_SIZE;
}
public boolean isGif() {
return getImageType() == IMAGE_TYPE.GIF;
}
public boolean compress(ImageCompressor imageCompressor) {
return CompressTask.compress(imageCompressor, this, MAX_IMAGE_SIZE);
}
/**
* @param maxSize the proximate max size for compression
* @return may be a little bigger than expected for performance.
*/
public boolean compress(ImageCompressor imageCompressor, long maxSize) {
return CompressTask.compress(imageCompressor, this, maxSize);
}
/**
* get mime type displayed in database.
*
* @return "image/gif" or "image/jpeg".
*/
public String getMimeType() {
if (getImageType() == IMAGE_TYPE.GIF) {
return "image/gif";
} else if (getImageType() == IMAGE_TYPE.JPG) {
return "image/jpeg";
}
return "image/jpeg";
}
public IMAGE_TYPE getImageType() {
return mImageType;
}
private IMAGE_TYPE getImageTypeByMime(String mimeType) {
if (!TextUtils.isEmpty(mimeType)) {
if ("image/gif".equals(mimeType)) {
return IMAGE_TYPE.GIF;
} else if ("image/png".equals(mimeType)) {
return IMAGE_TYPE.PNG;
} else {
return IMAGE_TYPE.JPG;
}
}
return IMAGE_TYPE.PNG;
}
public void setImageType(IMAGE_TYPE imageType) {
mImageType = imageType;
}
public String getId() {
return mId;
}
public int getHeight() {
return mHeight;
}
public int getWidth() {
return mWidth;
}
public String getCompressPath() {
return mCompressPath;
}
public void removeExif() {
BoxingExifHelper.removeExif(getPath());
}
/**
* save image to MediaStore.
*/
public void saveMediaStore(final ContentResolver cr) {
BoxingExecutor.getInstance().runWorker(new Runnable() {
@Override
public void run() {
if (cr != null && !TextUtils.isEmpty(getId())) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.TITLE, getId());
values.put(MediaStore.Images.Media.MIME_TYPE, getMimeType());
values.put(MediaStore.Images.Media.DATA, getPath());
cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
}
});
}
public void setCompressPath(String compressPath) {
mCompressPath = compressPath;
}
public void setSize(String size) {
mSize = size;
}
public void setHeight(int height) {
mHeight = height;
}
public void setWidth(int width) {
mWidth = width;
}
@Override
public String toString() {
return "ImageMedia{" +
", mThumbnailPath='" + mThumbnailPath + '\'' +
", mCompressPath='" + mCompressPath + '\'' +
", mSize='" + mSize + '\'' +
", mHeight=" + mHeight +
", mWidth=" + mWidth;
}
@Override
public int hashCode() {
int result = mId.hashCode();
result = 31 * result + (mPath != null ? mPath.hashCode() : 0);
return result;
}
@NonNull
public String getThumbnailPath() {
if (BoxingFileHelper.isFileValid(mThumbnailPath)) {
return mThumbnailPath;
} else if (BoxingFileHelper.isFileValid(mCompressPath)) {
return mCompressPath;
}
return mPath;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ImageMedia other = (ImageMedia) obj;
return !(TextUtils.isEmpty(mPath) || TextUtils.isEmpty(other.mPath)) && this.mPath.equals(other.mPath);
}
public static class Builder {
private String mId;
private String mImagePath;
private boolean mIsSelected;
private String mThumbnailPath;
private String mSize;
private int mHeight;
private int mWidth;
private String mMimeType;
public Builder(String id, String path) {
this.mId = id;
this.mImagePath = path;
}
public Builder setSelected(boolean selected) {
this.mIsSelected = selected;
return this;
}
public Builder setThumbnailPath(String thumbnailPath) {
mThumbnailPath = thumbnailPath;
return this;
}
public Builder setHeight(int height) {
mHeight = height;
return this;
}
public Builder setWidth(int width) {
mWidth = width;
return this;
}
public Builder setMimeType(String mimeType) {
mMimeType = mimeType;
return this;
}
public Builder setSize(String size) {
this.mSize = size;
return this;
}
public ImageMedia build() {
return new ImageMedia(this);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeByte(this.mIsSelected ? (byte) 1 : (byte) 0);
dest.writeString(this.mThumbnailPath);
dest.writeString(this.mCompressPath);
dest.writeInt(this.mHeight);
dest.writeInt(this.mWidth);
dest.writeInt(this.mImageType == null ? -1 : this.mImageType.ordinal());
dest.writeString(this.mMimeType);
}
protected ImageMedia(Parcel in) {
super(in);
this.mIsSelected = in.readByte() != 0;
this.mThumbnailPath = in.readString();
this.mCompressPath = in.readString();
this.mHeight = in.readInt();
this.mWidth = in.readInt();
int tmpMImageType = in.readInt();
this.mImageType = tmpMImageType == -1 ? null : IMAGE_TYPE.values()[tmpMImageType];
this.mMimeType = in.readString();
}
public static final Creator<ImageMedia> CREATOR = new Creator<ImageMedia>() {
@Override
public ImageMedia createFromParcel(Parcel source) {
return new ImageMedia(source);
}
@Override
public ImageMedia[] newArray(int size) {
return new ImageMedia[size];
}
};
}

@ -0,0 +1,196 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.entity.impl;
import android.os.Parcel;
import android.os.Parcelable;
import com.bilibili.boxing.model.entity.BaseMedia;
import java.util.Locale;
/**
* Entity represent a Video.
*
* @author ChenSL
*/
public class VideoMedia extends BaseMedia {
private static final long MB = 1024 * 1024;
private String mTitle;
private String mDuration;
private String mDateTaken;
private String mMimeType;
private VideoMedia() {
}
@Override
public TYPE getType() {
return TYPE.VIDEO;
}
public VideoMedia(Builder builder) {
super(builder.mId, builder.mPath);
this.mTitle = builder.mTitle;
this.mDuration = builder.mDuration;
this.mSize = builder.mSize;
this.mDateTaken = builder.mDateTaken;
this.mMimeType = builder.mMimeType;
}
public String getDuration() {
try {
long duration = Long.parseLong(mDuration);
return formatTimeWithMin(duration);
} catch (NumberFormatException e) {
return "0:00";
}
}
public String formatTimeWithMin(long duration) {
if (duration <= 0) {
return String.format(Locale.US, "%02d:%02d", 0, 0);
}
long totalSeconds = duration / 1000;
long seconds = totalSeconds % 60;
long minutes = (totalSeconds / 60) % 60;
long hours = totalSeconds / 3600;
if (hours > 0) {
return String.format(Locale.US, "%02d:%02d", hours * 60 + minutes,
seconds);
} else {
return String.format(Locale.US, "%02d:%02d", minutes, seconds);
}
}
public void setTitle(String title) {
mTitle = title;
}
public void setDuration(String duration) {
mDuration = duration;
}
public String getTitle() {
return mTitle;
}
public String getSizeByUnit() {
double size = getSize();
if (size == 0) {
return "0K";
}
if (size >= MB) {
double sizeInM = size / MB;
return String.format(Locale.getDefault(), "%.1f", sizeInM) + "M";
}
double sizeInK = size / 1024;
return String.format(Locale.getDefault(), "%.1f", sizeInK) + "K";
}
public String getDateTaken() {
return mDateTaken;
}
public String getMimeType() {
return mMimeType;
}
public static class Builder {
private String mId;
private String mTitle;
private String mPath;
private String mDuration;
private String mSize;
private String mDateTaken;
private String mMimeType;
public Builder(String id, String path) {
this.mId = id;
this.mPath = path;
}
public Builder setTitle(String title) {
this.mTitle = title;
return this;
}
public Builder setDuration(String duration) {
this.mDuration = duration;
return this;
}
public Builder setSize(String size) {
this.mSize = size;
return this;
}
public Builder setDataTaken(String dateTaken) {
this.mDateTaken = dateTaken;
return this;
}
public Builder setMimeType(String type) {
this.mMimeType = type;
return this;
}
public VideoMedia build() {
return new VideoMedia(this);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(this.mTitle);
dest.writeString(this.mDuration);
dest.writeString(this.mDateTaken);
dest.writeString(this.mMimeType);
}
protected VideoMedia(Parcel in) {
super(in);
this.mTitle = in.readString();
this.mDuration = in.readString();
this.mDateTaken = in.readString();
this.mMimeType = in.readString();
}
public static final Parcelable.Creator<VideoMedia> CREATOR = new Parcelable.Creator<VideoMedia>() {
@Override
public VideoMedia createFromParcel(Parcel source) {
return new VideoMedia(source);
}
@Override
public VideoMedia[] newArray(int size) {
return new VideoMedia[size];
}
};
}

@ -0,0 +1,36 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.task;
import android.content.ContentResolver;
import com.bilibili.boxing.model.callback.IMediaTaskCallback;
import com.bilibili.boxing.model.entity.BaseMedia;
/**
* The interface to load {@link BaseMedia}.
*
* @author ChenSL
*/
public interface IMediaTask<T extends BaseMedia> {
int PAGE_LIMIT = 1000;
void load(ContentResolver cr, int page, String id, IMediaTaskCallback<T> callback);
}

@ -0,0 +1,187 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.task.impl;
import android.content.ContentResolver;
import android.database.Cursor;
import android.provider.MediaStore.Images.Media;
import android.text.TextUtils;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.callback.IAlbumTaskCallback;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.utils.BoxingExecutor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.collection.ArrayMap;
/**
* A task to load albums.
*
* @author ChenSL
*/
@WorkerThread
public class AlbumTask {
private static final String UNKNOWN_ALBUM_NAME = "unknow";
private static final String SELECTION_IMAGE_MIME_TYPE = Media.MIME_TYPE + "=? or " + Media.MIME_TYPE + "=? or " + Media.MIME_TYPE + "=? or " + Media.MIME_TYPE + "=?";
private static final String SELECTION_IMAGE_MIME_TYPE_WITHOUT_GIF = Media.MIME_TYPE + "=? or " + Media.MIME_TYPE + "=? or " + Media.MIME_TYPE + "=?";
private static final String SELECTION_ID = Media.BUCKET_ID + "=? and (" + SELECTION_IMAGE_MIME_TYPE + " )";
private static final String SELECTION_ID_WITHOUT_GIF = Media.BUCKET_ID + "=? and (" + SELECTION_IMAGE_MIME_TYPE_WITHOUT_GIF + " )";
private static final String[] SELECTION_ARGS_IMAGE_MIME_TYPE = {"image/jpeg", "image/png", "image/jpg", "image/gif"};
private static final String[] SELECTION_ARGS_IMAGE_MIME_TYPE_WITHOUT_GIF = {"image/jpeg", "image/png", "image/jpg"};
private int mUnknownAlbumNumber = 1;
private Map<String, AlbumEntity> mBucketMap;
private AlbumEntity mDefaultAlbum;
private BoxingConfig mPickerConfig;
public AlbumTask() {
this.mBucketMap = new ArrayMap<>();
this.mDefaultAlbum = AlbumEntity.createDefaultAlbum();
this.mPickerConfig = BoxingManager.getInstance().getBoxingConfig();
}
public void start(@NonNull final ContentResolver cr, @NonNull final IAlbumTaskCallback callback) {
buildAlbumInfo(cr);
getAlbumList(callback);
}
private void buildAlbumInfo(ContentResolver cr) {
String[] distinctBucketColumns = new String[]{Media.BUCKET_ID, Media.BUCKET_DISPLAY_NAME};
Cursor bucketCursor = null;
try {
bucketCursor = cr.query(Media.EXTERNAL_CONTENT_URI, distinctBucketColumns, "0==0)" + " GROUP BY (" + Media.BUCKET_ID, null,
Media.DATE_MODIFIED + " desc");
if (bucketCursor != null && bucketCursor.moveToFirst()) {
do {
String buckId = bucketCursor.getString(bucketCursor.getColumnIndex(Media.BUCKET_ID));
String name = bucketCursor.getString(bucketCursor.getColumnIndex(Media.BUCKET_DISPLAY_NAME));
AlbumEntity album = buildAlbumInfo(name, buckId);
if (!TextUtils.isEmpty(buckId)) {
buildAlbumCover(cr, buckId, album);
}
} while (bucketCursor.moveToNext());
}
} finally {
if (bucketCursor != null) {
bucketCursor.close();
}
}
}
/**
* get the cover and count
*
* @param buckId album id
*/
private void buildAlbumCover(ContentResolver cr, String buckId, AlbumEntity album) {
String[] photoColumn = new String[]{Media._ID, Media.DATA};
boolean isNeedGif = mPickerConfig != null && mPickerConfig.isNeedGif();
String selectionId = isNeedGif ? SELECTION_ID : SELECTION_ID_WITHOUT_GIF;
String[] args = isNeedGif ? SELECTION_ARGS_IMAGE_MIME_TYPE : SELECTION_ARGS_IMAGE_MIME_TYPE_WITHOUT_GIF;
String[] selectionArgs = new String[args.length + 1];
selectionArgs[0] = buckId;
for (int i = 1; i < selectionArgs.length; i++) {
selectionArgs[i] = args[i-1];
}
Cursor coverCursor = cr.query(Media.EXTERNAL_CONTENT_URI, photoColumn, selectionId,
selectionArgs, Media.DATE_MODIFIED + " desc");
try {
if (coverCursor != null && coverCursor.moveToFirst()) {
String picPath = coverCursor.getString(coverCursor.getColumnIndex(Media.DATA));
String id = coverCursor.getString(coverCursor.getColumnIndex(Media._ID));
album.mCount = coverCursor.getCount();
album.mImageList.add(new ImageMedia(id, picPath));
if (album.mImageList.size() > 0) {
mBucketMap.put(buckId, album);
}
}
} finally {
if (coverCursor != null) {
coverCursor.close();
}
}
}
private void getAlbumList(@NonNull final IAlbumTaskCallback callback) {
mDefaultAlbum.mCount = 0;
List<AlbumEntity> tmpList = new ArrayList<>();
if (mBucketMap == null) {
postAlbums(callback, tmpList);
return;
}
for (Map.Entry<String, AlbumEntity> entry : mBucketMap.entrySet()) {
tmpList.add(entry.getValue());
mDefaultAlbum.mCount += entry.getValue().mCount;
}
if (tmpList.size() > 0 && tmpList.get(0) != null) {
mDefaultAlbum.mImageList = tmpList.get(0).mImageList;
tmpList.add(0, mDefaultAlbum);
}
postAlbums(callback, tmpList);
clear();
}
private void postAlbums(@NonNull final IAlbumTaskCallback callback, final List<AlbumEntity> result) {
BoxingExecutor.getInstance().runUI(new Runnable() {
@Override
public void run() {
callback.postAlbumList(result);
}
});
}
@NonNull
private AlbumEntity buildAlbumInfo(String bucketName, String bucketId) {
AlbumEntity album = null;
if (!TextUtils.isEmpty(bucketId)) {
album = mBucketMap.get(bucketId);
}
if (album == null) {
album = new AlbumEntity();
if (!TextUtils.isEmpty(bucketId)) {
album.mBucketId = bucketId;
} else {
album.mBucketId = String.valueOf(mUnknownAlbumNumber);
mUnknownAlbumNumber++;
}
if (!TextUtils.isEmpty(bucketName)) {
album.mBucketName = bucketName;
} else {
album.mBucketName = UNKNOWN_ALBUM_NAME;
mUnknownAlbumNumber++;
}
if (album.mImageList.size() > 0) {
mBucketMap.put(bucketId, album);
}
}
return album;
}
private void clear() {
if (mBucketMap != null) {
mBucketMap.clear();
}
}
}

@ -0,0 +1,231 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.task.impl;
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Build;
import android.provider.MediaStore.Images;
import android.text.TextUtils;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.callback.IMediaTaskCallback;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.model.task.IMediaTask;
import com.bilibili.boxing.utils.BoxingExecutor;
import com.bilibili.boxing.utils.BoxingLog;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.collection.ArrayMap;
/**
* A Task to load photos.
*
* @author ChenSL
*/
@WorkerThread
public class ImageTask implements IMediaTask<ImageMedia> {
private static final String CONJUNCTION_SQL = "=? or";
private static final String SELECTION_IMAGE_MIME_TYPE = Images.Media.MIME_TYPE + CONJUNCTION_SQL + " " + Images.Media.MIME_TYPE + CONJUNCTION_SQL + " " + Images.Media.MIME_TYPE + CONJUNCTION_SQL + " " + Images.Media.MIME_TYPE + "=?";
private static final String SELECTION_IMAGE_MIME_TYPE_WITHOUT_GIF = Images.Media.MIME_TYPE + CONJUNCTION_SQL + " " + Images.Media.MIME_TYPE + CONJUNCTION_SQL + " " + Images.Media.MIME_TYPE + "=?";
private static final String SELECTION_ID = Images.Media.BUCKET_ID + "=? and (" + SELECTION_IMAGE_MIME_TYPE + " )";
private static final String SELECTION_ID_WITHOUT_GIF = Images.Media.BUCKET_ID + "=? and (" + SELECTION_IMAGE_MIME_TYPE_WITHOUT_GIF + " )";
private static final String IMAGE_JPEG = "image/jpeg";
private static final String IMAGE_PNG = "image/png";
private static final String IMAGE_JPG = "image/jpg";
private static final String IMAGE_GIF = "image/gif";
private static final String[] SELECTION_ARGS_IMAGE_MIME_TYPE = {IMAGE_JPEG, IMAGE_PNG, IMAGE_JPG, IMAGE_GIF};
private static final String[] SELECTION_ARGS_IMAGE_MIME_TYPE_WITHOUT_GIF = {IMAGE_JPEG, IMAGE_PNG, IMAGE_JPG};
private static final String DESC = " desc";
private BoxingConfig mPickerConfig;
private Map<String, String> mThumbnailMap;
public ImageTask() {
this.mThumbnailMap = new ArrayMap<>();
this.mPickerConfig = BoxingManager.getInstance().getBoxingConfig();
}
@Override
public void load(@NonNull final ContentResolver cr, final int page, final String id,
@NonNull final IMediaTaskCallback<ImageMedia> callback) {
buildThumbnail(cr);
buildAlbumList(cr, id, page, callback);
}
private void buildThumbnail(ContentResolver cr) {
String[] projection = {Images.Thumbnails.IMAGE_ID, Images.Thumbnails.DATA};
queryThumbnails(cr, projection);
}
private void queryThumbnails(ContentResolver cr, String[] projection) {
Cursor cur = null;
try {
cur = Images.Thumbnails.queryMiniThumbnails(cr, Images.Thumbnails.EXTERNAL_CONTENT_URI,
Images.Thumbnails.MINI_KIND, projection);
if (cur != null && cur.moveToFirst()) {
do {
String imageId = cur.getString(cur.getColumnIndex(Images.Thumbnails.IMAGE_ID));
String imagePath = cur.getString(cur.getColumnIndex(Images.Thumbnails.DATA));
mThumbnailMap.put(imageId, imagePath);
} while (cur.moveToNext() && !cur.isLast());
}
} finally {
if (cur != null) {
cur.close();
}
}
}
private List<ImageMedia> buildAlbumList(ContentResolver cr, String bucketId, int page,
@NonNull final IMediaTaskCallback<ImageMedia> callback) {
List<ImageMedia> result = new ArrayList<>();
String columns[] = getColumns();
Cursor cursor = null;
try {
boolean isDefaultAlbum = TextUtils.isEmpty(bucketId);
boolean isNeedPaging = mPickerConfig == null || mPickerConfig.isNeedPaging();
boolean isNeedGif = mPickerConfig != null && mPickerConfig.isNeedGif();
int totalCount = getTotalCount(cr, bucketId, columns, isDefaultAlbum, isNeedGif);
String imageMimeType = isNeedGif ? SELECTION_IMAGE_MIME_TYPE : SELECTION_IMAGE_MIME_TYPE_WITHOUT_GIF;
String[] args = isNeedGif ? SELECTION_ARGS_IMAGE_MIME_TYPE : SELECTION_ARGS_IMAGE_MIME_TYPE_WITHOUT_GIF;
String order = isNeedPaging ? Images.Media.DATE_MODIFIED + DESC + " LIMIT "
+ page * IMediaTask.PAGE_LIMIT + " , " + IMediaTask.PAGE_LIMIT : Images.Media.DATE_MODIFIED + DESC;
String selectionId = isNeedGif ? SELECTION_ID : SELECTION_ID_WITHOUT_GIF;
cursor = query(cr, bucketId, columns, isDefaultAlbum, isNeedGif, imageMimeType, args, order, selectionId);
addItem(totalCount, result, cursor, callback);
} finally {
if (cursor != null) {
cursor.close();
}
}
return result;
}
private void addItem(final int allCount, final List<ImageMedia> result, Cursor cursor, @NonNull final IMediaTaskCallback<ImageMedia> callback) {
if (cursor != null && cursor.moveToFirst()) {
do {
String picPath = cursor.getString(cursor.getColumnIndex(Images.Media.DATA));
if (callback.needFilter(picPath)) {
BoxingLog.d("path:" + picPath + " has been filter");
} else {
String id = cursor.getString(cursor.getColumnIndex(Images.Media._ID));
String size = cursor.getString(cursor.getColumnIndex(Images.Media.SIZE));
String mimeType = cursor.getString(cursor.getColumnIndex(Images.Media.MIME_TYPE));
int width = 0;
int height = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
width = cursor.getInt(cursor.getColumnIndex(Images.Media.WIDTH));
height = cursor.getInt(cursor.getColumnIndex(Images.Media.HEIGHT));
}
ImageMedia imageItem = new ImageMedia.Builder(id, picPath).setThumbnailPath(mThumbnailMap.get(id))
.setSize(size).setMimeType(mimeType).setHeight(height).setWidth(width).build();
if (!result.contains(imageItem)) {
result.add(imageItem);
}
}
} while (!cursor.isLast() && cursor.moveToNext());
postMedias(result, allCount, callback);
} else {
postMedias(result, 0, callback);
}
clear();
}
private void postMedias(final List<ImageMedia> result, final int count, @NonNull final IMediaTaskCallback<ImageMedia> callback) {
BoxingExecutor.getInstance().runUI(new Runnable() {
@Override
public void run() {
callback.postMedia(result, count);
}
});
}
private Cursor query(ContentResolver cr, String bucketId, String[] columns, boolean isDefaultAlbum,
boolean isNeedGif, String imageMimeType, String[] args, String order, String selectionId) {
Cursor resultCursor;
if (isDefaultAlbum) {
resultCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, columns, imageMimeType,
args, order);
} else {
if (isNeedGif) {
resultCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, columns, selectionId,
new String[]{bucketId, args[0], args[1], args[2], args[3]}, order);
} else {
resultCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, columns, selectionId,
new String[]{bucketId, args[0], args[1], args[2]}, order);
}
}
return resultCursor;
}
@NonNull
private String[] getColumns() {
String[] columns;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
columns = new String[]{Images.Media._ID, Images.Media.DATA, Images.Media.SIZE, Images.Media.MIME_TYPE, Images.Media.WIDTH, Images.Media.HEIGHT};
} else {
columns = new String[]{Images.Media._ID, Images.Media.DATA, Images.Media.SIZE, Images.Media.MIME_TYPE};
}
return columns;
}
private int getTotalCount(ContentResolver cr, String bucketId, String[] columns, boolean isDefaultAlbum, boolean isNeedGif) {
Cursor allCursor = null;
int result = 0;
try {
if (isDefaultAlbum) {
allCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, columns,
SELECTION_IMAGE_MIME_TYPE, SELECTION_ARGS_IMAGE_MIME_TYPE,
Images.Media.DATE_MODIFIED + DESC);
} else {
if (isNeedGif) {
allCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, columns, SELECTION_ID,
new String[]{bucketId, IMAGE_JPEG, IMAGE_PNG, IMAGE_JPG, IMAGE_GIF}, Images.Media.DATE_MODIFIED + DESC);
} else {
allCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, columns, SELECTION_ID_WITHOUT_GIF,
new String[]{bucketId, IMAGE_JPEG, IMAGE_PNG, IMAGE_JPG}, Images.Media.DATE_MODIFIED + DESC);
}
}
if (allCursor != null) {
result = allCursor.getCount();
}
} finally {
if (allCursor != null) {
allCursor.close();
}
}
return result;
}
private void clear() {
if (mThumbnailMap != null) {
mThumbnailMap.clear();
}
}
}

@ -0,0 +1,102 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.model.task.impl;
import android.content.ContentResolver;
import android.database.Cursor;
import android.provider.MediaStore;
import com.bilibili.boxing.model.callback.IMediaTaskCallback;
import com.bilibili.boxing.model.entity.impl.VideoMedia;
import com.bilibili.boxing.model.task.IMediaTask;
import com.bilibili.boxing.utils.BoxingExecutor;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
/**
* A Task to load {@link VideoMedia} in database.
*
* @author ChenSL
*/
@WorkerThread
public class VideoTask implements IMediaTask<VideoMedia> {
private final static String[] MEDIA_COL = new String[]{
MediaStore.Video.Media.DATA,
MediaStore.Video.Media._ID,
MediaStore.Video.Media.TITLE,
MediaStore.Video.Media.MIME_TYPE,
MediaStore.Video.Media.SIZE,
MediaStore.Video.Media.DATE_TAKEN,
MediaStore.Video.Media.DURATION
};
@Override
public void load(final ContentResolver cr, final int page, String id, final IMediaTaskCallback<VideoMedia> callback) {
loadVideos(cr, page, callback);
}
private void loadVideos(ContentResolver cr, int page, @NonNull final IMediaTaskCallback<VideoMedia> callback) {
final List<VideoMedia> videoMedias = new ArrayList<>();
final Cursor cursor = cr.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MEDIA_COL, null, null,
MediaStore.Images.Media.DATE_MODIFIED + " desc" + " LIMIT " + page * IMediaTask.PAGE_LIMIT + " , " + IMediaTask.PAGE_LIMIT);
try {
int count = 0;
if (cursor != null && cursor.moveToFirst()) {
count = cursor.getCount();
do {
String data = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATA));
String id = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media._ID));
String title = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));
String type = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
String size = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.SIZE));
String date = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATE_TAKEN));
String duration = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DURATION));
VideoMedia video = new VideoMedia.Builder(id, data).setTitle(title).setDuration(duration)
.setSize(size).setDataTaken(date).setMimeType(type).build();
videoMedias.add(video);
} while (!cursor.isLast() && cursor.moveToNext());
postMedias(callback, videoMedias, count);
} else {
postMedias(callback, videoMedias, 0);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private void postMedias(@NonNull final IMediaTaskCallback<VideoMedia> callback,
final List<VideoMedia> videoMedias, final int count) {
BoxingExecutor.getInstance().runUI(new Runnable() {
@Override
public void run() {
callback.postMedia(videoMedias, count);
}
});
}
}

@ -0,0 +1,132 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.presenter;
import android.content.ContentResolver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bilibili.boxing.model.config.BoxingConfig;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.BaseMedia;
import java.util.List;
/**
* This specifies the contract between the view and the presenter.
*
* @author ChenSL
*/
public interface PickerContract {
/**
* define the functions of the view, interacting with presenter
*/
interface View {
/**
* set the presenter attaching to the view
*/
void setPresenter(@NonNull Presenter presenter);
/**
* show a list the {@link BaseMedia} in the view
*/
void showMedia(@Nullable List<BaseMedia> medias, int allCount);
/**
* show all the {@link AlbumEntity} in the view
*/
void showAlbum(@Nullable List<AlbumEntity> albums);
/**
* get the {@link ContentResolver} in the view
*/
@NonNull
ContentResolver getAppCr();
/**
* call when the view should be finished or the process is finished
*
* @param medias the selection of medias.
*/
void onFinish(@NonNull List<BaseMedia> medias);
/**
* clear all the {@link BaseMedia} in the view
*/
void clearMedia();
/**
* start crop the {@link BaseMedia} in the single media mode
*/
void startCrop(@NonNull BaseMedia media, int requestCode);
/**
* set or update the config.
*
* @param config {@link BoxingConfig}
*/
void setPickerConfig(BoxingConfig config);
}
/**
* define the function of presenter, to control the module to load data and to tell view to displayRaw the data
*/
interface Presenter {
/**
* load the specify data from {@link ContentResolver}
*
* @param page the page need to load
* @param albumId album albumId
*/
void loadMedias(int page, String albumId);
/**
* load all the album from {@link ContentResolver}
*/
void loadAlbums();
/**
* destroy the presenter and set the view null
*/
void destroy();
/**
* has more data to load
*
* @return true, have more
*/
boolean hasNextPage();
boolean canLoadNextPage();
/**
* load next page
*/
void onLoadNextPage();
/**
* Determine the selected allMedias according to mSelectedMedias
*
* @param allMedias all medias
* @param selectedMedias the medias to be selected
*/
void checkSelectedMedia(List<BaseMedia> allMedias, List<BaseMedia> selectedMedias);
}
}

@ -0,0 +1,178 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.presenter;
import android.content.ContentResolver;
import android.text.TextUtils;
import com.bilibili.boxing.model.BoxingManager;
import com.bilibili.boxing.model.callback.IAlbumTaskCallback;
import com.bilibili.boxing.model.callback.IMediaTaskCallback;
import com.bilibili.boxing.model.entity.AlbumEntity;
import com.bilibili.boxing.model.entity.BaseMedia;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import com.bilibili.boxing.model.task.IMediaTask;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A presenter implement {@link com.bilibili.boxing.presenter.PickerContract.Presenter}.
*
* @author ChenSL
*/
public class PickerPresenter implements PickerContract.Presenter {
private PickerContract.View mTasksView;
private int mTotalPage;
private int mCurrentPage;
private boolean mIsLoadingNextPage;
private String mCurrentAlbumId;
private LoadMediaCallback mLoadMediaCallback;
private LoadAlbumCallback mLoadAlbumCallback;
public PickerPresenter(PickerContract.View tasksView) {
this.mTasksView = tasksView;
this.mTasksView.setPresenter(this);
this.mLoadMediaCallback = new LoadMediaCallback(this);
this.mLoadAlbumCallback = new LoadAlbumCallback(this);
}
@Override
public void loadMedias(int page, String albumId) {
mCurrentAlbumId = albumId;
if (page == 0) {
mTasksView.clearMedia();
mCurrentPage = 0;
}
ContentResolver cr = mTasksView.getAppCr();
BoxingManager.getInstance().loadMedia(cr, page, albumId, mLoadMediaCallback);
}
@Override
public void loadAlbums() {
ContentResolver cr = mTasksView.getAppCr();
BoxingManager.getInstance().loadAlbum(cr, mLoadAlbumCallback);
}
@Override
public void destroy() {
mTasksView = null;
}
@Override
public boolean hasNextPage() {
return mCurrentPage < mTotalPage;
}
@Override
public boolean canLoadNextPage() {
return !mIsLoadingNextPage;
}
@Override
public void onLoadNextPage() {
mCurrentPage++;
mIsLoadingNextPage = true;
loadMedias(mCurrentPage, mCurrentAlbumId);
}
@Override
public void checkSelectedMedia(List<BaseMedia> allMedias, List<BaseMedia> selectedMedias) {
if (allMedias == null || allMedias.size() == 0) {
return;
}
Map<String, ImageMedia> map = new HashMap<>(allMedias.size());
for (BaseMedia allMedia : allMedias) {
if (!(allMedia instanceof ImageMedia)) {
return;
}
ImageMedia media = (ImageMedia) allMedia;
media.setSelected(false);
map.put(media.getPath(), media);
}
if (selectedMedias == null || selectedMedias.size() < 0) {
return;
}
for (BaseMedia media : selectedMedias) {
if (map.containsKey(media.getPath())) {
map.get(media.getPath()).setSelected(true);
}
}
}
private static class LoadMediaCallback implements IMediaTaskCallback<BaseMedia> {
private WeakReference<PickerPresenter> mWr;
LoadMediaCallback(PickerPresenter presenter) {
mWr = new WeakReference<>(presenter);
}
private PickerPresenter getPresenter() {
return mWr.get();
}
@Override
public void postMedia(List<BaseMedia> medias, int count) {
PickerPresenter presenter = getPresenter();
if (presenter == null) {
return;
}
PickerContract.View view = presenter.mTasksView;
if (view != null) {
view.showMedia(medias, count);
}
presenter.mTotalPage = count / IMediaTask.PAGE_LIMIT;
presenter.mIsLoadingNextPage = false;
}
@Override
public boolean needFilter(String path) {
return TextUtils.isEmpty(path) || !(new File(path).exists());
}
}
private static class LoadAlbumCallback implements IAlbumTaskCallback {
private WeakReference<PickerPresenter> mWr;
LoadAlbumCallback(PickerPresenter presenter) {
mWr = new WeakReference<>(presenter);
}
private PickerPresenter getPresenter() {
return mWr.get();
}
@Override
public void postAlbumList(List<AlbumEntity> list) {
PickerPresenter presenter = getPresenter();
if (presenter == null) {
return;
}
if (presenter.mTasksView != null) {
presenter.mTasksView.showAlbum(list);
}
}
}
}

@ -0,0 +1,91 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
/**
* @author ChenSL
*/
public class BoxingExecutor {
private static final BoxingExecutor INSTANCE = new BoxingExecutor();
private ExecutorService mExecutorService;
private BoxingExecutor() {
}
public static BoxingExecutor getInstance() {
return INSTANCE;
}
public void runWorker(@NonNull Runnable runnable) {
ensureWorkerHandlerNotNull();
try {
mExecutorService.execute(runnable);
} catch (Exception e) {
BoxingLog.d("runnable stop running unexpected. " + e.getMessage());
}
}
@Nullable
public FutureTask<Boolean> runWorker(@NonNull Callable<Boolean> callable) {
ensureWorkerHandlerNotNull();
FutureTask<Boolean> task = null;
try {
task = new FutureTask<>(callable);
mExecutorService.submit(task);
return task;
} catch (Exception e) {
BoxingLog.d("callable stop running unexpected. " + e.getMessage());
}
return task;
}
public void runUI(@NonNull Runnable runnable) {
if (Looper.myLooper() == Looper.getMainLooper()) {
runnable.run();
return;
}
Handler handler = ensureUiHandlerNotNull();
try {
handler.post(runnable);
} catch (Exception e) {
BoxingLog.d("update UI task fail. " + e.getMessage());
}
}
private void ensureWorkerHandlerNotNull() {
if (mExecutorService == null) {
mExecutorService = Executors.newCachedThreadPool();
}
}
private Handler ensureUiHandlerNotNull() {
return new Handler(Looper.getMainLooper());
}
}

@ -0,0 +1,87 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.media.ExifInterface;
import android.os.Build;
import android.text.TextUtils;
import java.io.IOException;
/**
* @author ChenSL
*/
public class BoxingExifHelper {
public static void removeExif(String path) {
if (!TextUtils.isEmpty(path)) {
return;
}
ExifInterface exifInterface;
try {
exifInterface = new ExifInterface(path);
} catch (IOException ignore) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
exifInterface.setAttribute(ExifInterface.TAG_ARTIST, "");
exifInterface.setAttribute(ExifInterface.TAG_RESOLUTION_UNIT, "0");
exifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "");
exifInterface.setAttribute(ExifInterface.TAG_MAKER_NOTE, "0");
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
exifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, "");
}
exifInterface.setAttribute(ExifInterface.TAG_MAKE, "");
exifInterface.setAttribute(ExifInterface.TAG_MODEL, "");
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, "0");
exifInterface.setAttribute(ExifInterface.TAG_DATETIME, "");
exifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, "");
exifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, "");
exifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, "");
}
static int getRotateDegree(String path) {
int result = 0;
try {
ExifInterface exif = new ExifInterface(path);
int orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
result = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
result = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
result = 270;
break;
}
} catch (IOException ignore) {
return 0;
}
return result;
}
}

@ -0,0 +1,103 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import java.io.File;
import java.util.concurrent.ExecutionException;
/**
* A file helper to make thing easier.
*
* @author ChenSL
*/
public class BoxingFileHelper {
public static final String DEFAULT_SUB_DIR = "/bili/boxing";
public static boolean createFile(String path) throws ExecutionException, InterruptedException {
if (TextUtils.isEmpty(path)) {
return false;
}
final File file = new File(path);
return file.exists() || file.mkdirs();
}
@Nullable
public static String getCacheDir(@NonNull Context context) {
if (context == null) {
return null;
}
context = context.getApplicationContext();
File cacheDir = context.getCacheDir();
if (cacheDir == null) {
BoxingLog.d("cache dir do not exist.");
return null;
}
String result = cacheDir.getAbsolutePath() + "/boxing";
try {
BoxingFileHelper.createFile(result);
} catch (ExecutionException | InterruptedException e) {
BoxingLog.d("cache dir " + result + " not exist");
return null;
}
BoxingLog.d("cache dir is: " + result);
return result;
}
@Nullable
public static String getBoxingPathInDCIM() {
return getExternalDCIM(null);
}
@Nullable
public static String getExternalDCIM(String subDir) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
if (file == null) {
return null;
}
String dir = "/bili/boxing";
if (!TextUtils.isEmpty(subDir)) {
dir = subDir;
}
String result = file.getAbsolutePath() + dir;
BoxingLog.d("external DCIM is: " + result);
return result;
}
BoxingLog.d("external DCIM do not exist.");
return null;
}
public static boolean isFileValid(String path) {
if (TextUtils.isEmpty(path)) {
return false;
}
File file = new File(path);
return isFileValid(file);
}
static boolean isFileValid(File file) {
return file != null && file.exists() && file.isFile() && file.length() > 0 && file.canRead();
}
}

@ -0,0 +1,37 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.util.Log;
import com.bilibili.boxing.model.BoxingBuilderConfig;
/**
* Debug log tool.
*
* @author ChenSL
*/
public class BoxingLog {
private static final String TAG = "com.bilibili.boxing";
public static void d(String log) {
if (BoxingBuilderConfig.DEBUG && !BoxingBuilderConfig.TESTING) {
Log.d(TAG, log);
}
}
}

@ -0,0 +1,308 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
import com.bilibili.boxing.AbsBoxingViewFragment;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
/**
* A helper to start camera.<br/>
* used by {@link AbsBoxingViewFragment}
*
* @author ChenSL
*/
public class CameraPickerHelper {
private static final int MAX_CAMER_PHOTO_SIZE = 4 * 1024 * 1024;
public static final int REQ_CODE_CAMERA = 0x2001;
private static final String STATE_SAVED_KEY = "com.bilibili.boxing.utils.CameraPickerHelper.saved_state";
private String mSourceFilePath;
private File mOutputFile;
private Callback mCallback;
public interface Callback {
void onFinish(@NonNull CameraPickerHelper helper);
void onError(@NonNull CameraPickerHelper helper);
}
public CameraPickerHelper(@Nullable Bundle savedInstance) {
if (savedInstance != null) {
SavedState state = savedInstance.getParcelable(STATE_SAVED_KEY);
if (state != null) {
mOutputFile = state.mOutputFile;
mSourceFilePath = state.mSourceFilePath;
}
}
}
public void setPickCallback(Callback callback) {
this.mCallback = callback;
}
public void onSaveInstanceState(Bundle out) {
SavedState state = new SavedState();
state.mOutputFile = mOutputFile;
state.mSourceFilePath = mSourceFilePath;
out.putParcelable(STATE_SAVED_KEY, state);
}
/**
* start system camera to take a picture
*
* @param activity not null if fragment is null.
* @param fragment not null if activity is null.
* @param subFolderPath a folder in external DCIM,must start with "/".
*/
public void startCamera(final Activity activity, final Fragment fragment, final String subFolderPath) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !takePhotoSecure(activity, fragment, subFolderPath)) {
FutureTask<Boolean> task = BoxingExecutor.getInstance().runWorker(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
try {
// try...try...try
Camera camera = Camera.open();
camera.release();
} catch (Exception e) {
BoxingLog.d("camera is not available.");
return false;
}
return true;
}
});
try {
if (task != null && task.get()) {
startCameraIntent(activity, fragment, subFolderPath, MediaStore.ACTION_IMAGE_CAPTURE, REQ_CODE_CAMERA);
} else {
callbackError();
}
} catch (InterruptedException | ExecutionException ignore) {
callbackError();
}
}
}
private boolean takePhotoSecure(Activity activity, Fragment fragment, String subDir) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try {
startCameraIntent(activity, fragment, subDir, MediaStore.ACTION_IMAGE_CAPTURE, REQ_CODE_CAMERA);
return true;
} catch (ActivityNotFoundException ignore) {
return false;
}
}
return false;
}
private void callbackFinish() {
if (mCallback != null) {
mCallback.onFinish(CameraPickerHelper.this);
}
}
private void callbackError() {
if (mCallback != null) {
mCallback.onError(CameraPickerHelper.this);
}
}
private void startActivityForResult(Activity activity, Fragment fragment, final Intent intent, final int reqCodeCamera) throws ActivityNotFoundException {
if (fragment == null) {
activity.startActivityForResult(intent, reqCodeCamera);
} else {
fragment.startActivityForResult(intent, reqCodeCamera);
}
}
private void startCameraIntent(final Activity activity, final Fragment fragment, String subFolder,
final String action, final int requestCode) {
final String cameraOutDir = BoxingFileHelper.getExternalDCIM(subFolder);
try {
if (BoxingFileHelper.createFile(cameraOutDir)) {
mOutputFile = new File(cameraOutDir, System.currentTimeMillis() + ".jpg");
mSourceFilePath = mOutputFile.getPath();
Intent intent = new Intent(action);
Uri uri = getFileUri(activity.getApplicationContext(), mOutputFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
try {
startActivityForResult(activity, fragment, intent, requestCode);
} catch (ActivityNotFoundException ignore) {
callbackError();
}
}
} catch (ExecutionException | InterruptedException e) {
BoxingLog.d("create file" + cameraOutDir + " error.");
}
}
private Uri getFileUri(@NonNull Context context, @NonNull File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return FileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".file.provider", mOutputFile);
} else {
return Uri.fromFile(file);
}
}
public String getSourceFilePath() {
return mSourceFilePath;
}
/**
* deal with the system camera's shot.
*/
public boolean onActivityResult(final int requestCode, final int resultCode) {
if (requestCode != REQ_CODE_CAMERA) {
return false;
}
if (resultCode != Activity.RESULT_OK) {
callbackError();
return false;
}
FutureTask<Boolean> task = BoxingExecutor.getInstance().runWorker(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return rotateImage(resultCode);
}
});
try {
if (task != null && task.get()) {
callbackFinish();
} else {
callbackError();
}
} catch (InterruptedException | ExecutionException ignore) {
callbackError();
}
return true;
}
private boolean rotateSourceFile(File file) throws IOException {
if (file == null || !file.exists()) {
return false;
}
FileOutputStream outputStream = null;
Bitmap bitmap = null;
Bitmap outBitmap = null;
try {
int degree = BoxingExifHelper.getRotateDegree(file.getAbsolutePath());
if (degree == 0) {
return true;
}
int quality = file.length() >= MAX_CAMER_PHOTO_SIZE ? 90 : 100;
Matrix matrix = new Matrix();
matrix.postRotate(degree);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
outBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
outputStream = new FileOutputStream(file);
outBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
outputStream.flush();
return true;
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
BoxingLog.d("IOException when output stream closing!");
}
}
if (bitmap != null) {
bitmap.recycle();
}
if (outBitmap != null) {
outBitmap.recycle();
}
}
}
private boolean rotateImage(int resultCode) throws IOException {
return resultCode == Activity.RESULT_OK && rotateSourceFile(mOutputFile);
}
public void release() {
mOutputFile = null;
}
private static class SavedState implements Parcelable {
private File mOutputFile;
private String mSourceFilePath;
SavedState() {
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable(this.mOutputFile);
dest.writeString(this.mSourceFilePath);
}
SavedState(Parcel in) {
this.mOutputFile = (File) in.readSerializable();
this.mSourceFilePath = in.readString();
}
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

@ -0,0 +1,84 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.content.Context;
import com.bilibili.boxing.model.entity.impl.ImageMedia;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* A compress task for {@link ImageMedia}
* @author ChenSL
*/
public class CompressTask {
public static boolean compress(Context context, final ImageMedia image) {
return compress(new ImageCompressor(context), image, ImageCompressor.MAX_LIMIT_SIZE_LONG);
}
/**
* @param imageCompressor see {@link ImageCompressor}.
* @param maxSize the proximate max size for compression
* @return may be a little bigger than expected for performance.
*/
public static boolean compress(final ImageCompressor imageCompressor, final ImageMedia image, final long maxSize) {
if (imageCompressor == null || image == null || maxSize <= 0) {
return false;
}
FutureTask<Boolean> task = BoxingExecutor.getInstance().runWorker(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
final String path = image.getPath();
File compressSaveFile = imageCompressor.getCompressOutFile(path);
File needCompressFile = new File(path);
if (BoxingFileHelper.isFileValid(compressSaveFile)) {
image.setCompressPath(compressSaveFile.getAbsolutePath());
return true;
}
if (!BoxingFileHelper.isFileValid(needCompressFile)) {
return false;
} else if (image.getSize() < maxSize) {
image.setCompressPath(path);
return true;
} else {
try {
File result = imageCompressor.compress(needCompressFile, maxSize);
boolean suc = BoxingFileHelper.isFileValid(result);
image.setCompressPath(suc ? result.getAbsolutePath() : null);
return suc;
} catch (IOException | OutOfMemoryError | NullPointerException | IllegalArgumentException e) {
image.setCompressPath(null);
BoxingLog.d("image compress fail!");
}
}
return false;
}
});
try {
return task != null && task.get();
} catch (InterruptedException | ExecutionException ignore) {
return false;
}
}
}

@ -0,0 +1,351 @@
/*
* Copyright (C) 2017 Bilibili
*
* 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.bilibili.boxing.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
/**
* A compress for image.
*
* @author ChenSL
*/
public class ImageCompressor {
public static final long MAX_LIMIT_SIZE_LONG = 1024 * 1024L;
private static final char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
private static final int MAX_WIDTH = 3024;
private static final int MAX_HEIGHT = 4032;
private static final long MAX_LIMIT_SIZE = 300 * 1024L;
private static final String COMPRESS_FILE_PREFIX = "compress-";
private File mOutFileFile;
public ImageCompressor(@NonNull File cachedRootDir) {
if (cachedRootDir != null) {
mOutFileFile = new File(cachedRootDir.getAbsolutePath() + File.separator + ".compress" + File.separator);
}
}
public ImageCompressor(@NonNull Context context) {
if (context != null) {
String rootDir = BoxingFileHelper.getCacheDir(context);
if (TextUtils.isEmpty(rootDir)) {
throw new IllegalStateException("the cache dir is null");
}
mOutFileFile = new File(rootDir + File.separator + ".compress" + File.separator);
}
}
public File compress(@NonNull File file) throws IOException, NullPointerException, IllegalArgumentException {
return compress(file, MAX_LIMIT_SIZE);
}
/**
* @param file file to compress.
* @param maxsize the proximate max size for compression, not for the image with large ratio.
* @return may be a little bigger than expected for performance.
*/
public File compress(@NonNull File file, long maxsize) throws IOException, NullPointerException, IllegalArgumentException {
if (!file.exists()) {
throw new IllegalArgumentException("file not found : " + file.getAbsolutePath());
}
if (!isLegalFile(file)) {
throw new IllegalArgumentException("file is not a legal file : " + file.getAbsolutePath());
}
if (mOutFileFile == null) {
throw new NullPointerException("the external cache dir is null");
}
BitmapFactory.Options checkOptions = new BitmapFactory.Options();
checkOptions.inJustDecodeBounds = true;
String absPath = file.getAbsolutePath();
int angle = BoxingExifHelper.getRotateDegree(absPath);
BitmapFactory.decodeFile(absPath, checkOptions);
if (checkOptions.outWidth <= 0 || checkOptions.outHeight <= 0) {
throw new IllegalArgumentException("file is not a legal bitmap with 0 with or 0 height : " + file.getAbsolutePath());
}
int width = checkOptions.outWidth;
int height = checkOptions.outHeight;
File outFile = createCompressFile(file);
if (outFile == null) {
throw new NullPointerException("the compressed file create fail, the compressed path is null.");
}
if (!isLargeRatio(width, height)) {
int[] display = getCompressDisplay(width, height);
Bitmap bitmap = compressDisplay(absPath, display[0], display[1]);
Bitmap rotatedBitmap = rotatingImage(angle, bitmap);
if (bitmap != rotatedBitmap) {
bitmap.recycle();
}
saveBitmap(rotatedBitmap, outFile);
rotatedBitmap.recycle();
compressQuality(outFile, maxsize, 20);
} else {
if (checkOptions.outHeight >= MAX_HEIGHT && checkOptions.outWidth >= MAX_WIDTH) {
checkOptions.inSampleSize = 2;
}
checkOptions.inJustDecodeBounds = false;
Bitmap originBitmap = BitmapFactory.decodeFile(absPath, checkOptions);
Bitmap rotatedBitmap = rotatingImage(angle, originBitmap);
if (originBitmap != rotatedBitmap) {
originBitmap.recycle();
}
saveBitmap(originBitmap, outFile);
rotatedBitmap.recycle();
compressQuality(outFile, MAX_LIMIT_SIZE_LONG, 50);
}
BoxingLog.d("compress suc: " + outFile.getAbsolutePath());
return outFile;
}
private Bitmap rotatingImage(int angle, Bitmap bitmap) {
if (angle == 0) {
return bitmap;
}
//rotate image
Matrix matrix = new Matrix();
matrix.postRotate(angle);
//create a new image
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
private void saveBitmap(Bitmap bitmap, File outFile) throws IOException {
FileOutputStream fos = new FileOutputStream(outFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
try {
fos.flush();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
BoxingLog.d("IOException when saving a bitmap");
}
}
}
/**
* @param width must > 0
* @param height must > 0
*/
private int[] getCompressDisplay(int width, int height) {
int thumbWidth = width % 2 == 1 ? width + 1 : width;
int thumbHeight = height % 2 == 1 ? height + 1 : height;
int[] results = new int[]{thumbWidth, thumbHeight};
width = thumbWidth > thumbHeight ? thumbHeight : thumbWidth;
height = thumbWidth > thumbHeight ? thumbWidth : thumbHeight;
float scale = (float) width / height;
if (scale <= 1 && scale >= 0.5625) {
if (height < 1664) {
thumbWidth = width;
thumbHeight = height;
} else if (height >= 1664 && height < 4990) {
thumbWidth = width / 2;
thumbHeight = height / 2;
} else if (height >= 4990 && height < 10240) {
thumbWidth = width / 4;
thumbHeight = height / 4;
} else {
int multiple = height / 1280 == 0 ? 1 : height / 1280;
thumbWidth = width / multiple;
thumbHeight = height / multiple;
}
} else if (scale <= 0.5625 && scale > 0.5) {
if (height < 1280) {
thumbWidth = width;
thumbHeight = height;
} else {
int multiple = height / 1280 == 0 ? 1 : height / 1280;
thumbWidth = width / multiple;
thumbHeight = height / multiple;
}
} else {
int multiple = (int) Math.ceil(height / (1280.0 / scale));
thumbWidth = width / multiple;
thumbHeight = height / multiple;
}
results[0] = thumbWidth;
results[1] = thumbHeight;
return results;
}
/**
* @param width must > 0
* @param height must > 0
*/
private Bitmap compressDisplay(String imagePath, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, options);
int outH = options.outHeight;
int outW = options.outWidth;
int inSampleSize = 1;
if (outH > height || outW > width) {
int halfH = outH / 2;
int halfW = outW / 2;
while ((halfH / inSampleSize) > height && (halfW / inSampleSize) > width) {
inSampleSize *= 2;
}
}
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
int heightRatio = (int) Math.ceil(options.outHeight / (float) height);
int widthRatio = (int) Math.ceil(options.outWidth / (float) width);
if (heightRatio > 1 || widthRatio > 1) {
if (heightRatio > widthRatio) {
options.inSampleSize = heightRatio;
} else {
options.inSampleSize = widthRatio;
}
}
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(imagePath, options);
}
private void compressQuality(File outFile, long maxSize, int maxQuality) throws IOException {
long length = outFile.length();
int quality = 90;
if (length > maxSize) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
BoxingLog.d("source file size : " + outFile.length() + ",path : " + outFile);
while (true) {
compressPhotoByQuality(outFile, bos, quality);
long size = bos.size();
BoxingLog.d("compressed file size : " + size);
if (quality <= maxQuality) {
break;
}
if (size < maxSize) {
break;
} else {
quality -= 10;
bos.reset();
}
}
OutputStream fos = new FileOutputStream(outFile);
bos.writeTo(fos);
bos.flush();
fos.close();
bos.close();
}
}
private void compressPhotoByQuality(File file, final OutputStream os, final int quality) throws IOException, OutOfMemoryError {
BoxingLog.d("start compress quality... ");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bitmap != null) {
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, os);
bitmap.recycle();
} else {
throw new NullPointerException("bitmap is null when compress by quality");
}
}
private File createCompressFile(File file) throws IOException {
File outFile = getCompressOutFile(file);
if (!mOutFileFile.exists()) {
mOutFileFile.mkdirs();
}
BoxingLog.d("compress out file : " + outFile);
outFile.createNewFile();
return outFile;
}
public @Nullable File getCompressOutFile(File file) {
String path = getCompressOutFilePath(file);
return TextUtils.isEmpty(path) ? null: new File(path);
}
public @Nullable File getCompressOutFile(String filePth) {
String path = getCompressOutFilePath(filePth);
return TextUtils.isEmpty(path) ? null: new File(path);
}
public @Nullable String getCompressOutFilePath(File file) {
return getCompressOutFilePath(file.getAbsolutePath());
}
public @Nullable String getCompressOutFilePath(String filePath) {
try {
return mOutFileFile + File.separator + COMPRESS_FILE_PREFIX + signMD5(filePath.getBytes("UTF-8")) + ".jpg";
} catch (UnsupportedEncodingException e) {
return null;
}
}
public String signMD5(byte[] source) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return signDigest(source, digest);
} catch (NoSuchAlgorithmException e) {
BoxingLog.d("have no md5");
}
return null;
}
private String signDigest(byte[] source, MessageDigest digest) {
digest.update(source);
byte[] data = digest.digest();
int j = data.length;
char str[] = new char[j * 2];
int k = 0;
for (byte byte0 : data) {
str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf];
str[k++] = HEX_DIGITS[byte0 & 0xf];
}
return new String(str).toLowerCase(Locale.getDefault());
}
private boolean isLargeRatio(int width, int height) {
return width / height >= 3 || height / width >= 3;
}
private boolean isLegalFile(File file) {
return file != null && file.exists() && file.isFile() && file.length() > 0;
}
}

@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<resources>
</resources>

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 Bilibili
~
~ 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.
~
-->
<paths>
<cache-path name="internal" path="boxing" />
<external-path name="external" path="DCIM/bili/boxing" />
</paths>

@ -1,27 +1,29 @@
<manifest package="com.ztiany.mediaselector" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> package="com.ztiany.mediaselector">
<application> <application>
<activity <activity
android:name="com.yalantis.ucrop.UCropActivity" android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<activity <activity
android:name=".LimitedBoxingActivity" android:name="com.bilibili.boxing_impl.ui.BoxingActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Boxing.AppTheme.NoActionBar"> android:theme="@style/Boxing.AppTheme.NoActionBar" />
</activity>
<activity <activity
android:name=".LimitedBoxingViewActivity" android:name="com.bilibili.boxing_impl.ui.BoxingViewActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Boxing.AppTheme.NoActionBar"> android:theme="@style/Boxing.AppTheme.NoActionBar" />
</activity>
<activity
android:name="com.bilibili.boxing_impl.ui.BoxingBottomSheetActivity"
android:screenOrientation="portrait"
android:theme="@style/Boxing.AppTheme.NoActionBar" />
</application> </application>

Loading…
Cancel
Save