📜 ⬆️ ⬇️

Photo Widget DIY



Hello, dear habrasoobschestvo. My previous article about the custom unlock screen received few reviews in the form of comments, but nevertheless a hundred people kept it in their favorites, thus inspiring me to write another article on an unpopular topic.

Many Xperia smartphone users like the beautiful 3D standard photo widget. In terms of terminology android, this is not an AppWidget, but a simple View, very similar to a widget. It can be called a “plug-in” to the standard Xperia Home launcher, so it’s not in the list of widgets of other launchers. In this post I will tell you how to make a similar widget.

Introduction


Imagine the situation as if you wrote your own launcher for android with widget support (perhaps in the next article I will briefly describe how to do this). A three-dimensional photo widget will make your launcher stand out from the rest. As is the case with the standard Xperia photo widget, our widget is a custom View, launched in the process of our launcher.
')

Widget base


The first step is to make ours appear in the list of available widgets (remember, not the widget). Sony Ericsson acted simply - the standard launcher from the list of installed packages in the system chooses those that start with “com.sonyericsson.advancedwidget” and adds the name and icon to the list of widgets. This method is simple, but it has a flaw: one apk - one widget. We will act smarter (I hope) - in the manifesto of our future widget we will write this:

<receiver android:name=".PhotoWidgetReceiver" > <intent-filter> <action android:name="by.arriva.ADVANCED_WIDGET" /> </intent-filter> </receiver> 

Leave the PhotoWidgetReceiver class empty:

 public class PhotoWidgetReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { } } 

Thanks to this trick, our launcher will find all BroadcastRecievers that respond to “by.arriva.ADVANCED_WIDGET”. The following is a listing of code from the launcher.

 final PackageManager manager = getPackageManager(); Intent i = new Intent("by.arriva.ADVANCED_WIDGET"); List<ResolveInfo> lri = manager.queryBroadcastReceivers(i, 0); for(ResolveInfo ri : lri){ String packageName = ri.activityInfo.packageName; String className = ri.activityInfo.name; className = className.replace("Receiver", ""); AdvancedWidgetInfo awi = new AdvancedWidgetInfo(this, packageName, className); if(!awi.isValid) continue; try { WidgetInfo wi = new WidgetInfo(true, awi.label, awi.width+" x "+awi.height, manager.getResourcesForApplication(packageName).getDrawable(awi.preview), new ComponentName(packageName, className)); widgetsSorted.add(wi); } catch (Exception e) {} awi = null; } lri.clear(); 

String className - the name of the class inherited from BroadcastReciever, in this case, PhotoWidgetReceiver. The launcher looks for the PhotoWidget class in the widget, which contains the widget description. Here is its content:

 public class PhotoWidget { public static View getView(Context context){ WidgetView wv = new WidgetView(context); return wv; } public static int getCellWidth(Context context){ return 2; } public static int getCellHeight(Context context){ return 2; } public static String getLabel(Context context){ return context.getString(R.string.app_name); } public static int getIconId(Context context){ return R.drawable.icon; } public static int getPreviewId(Context context){ return R.drawable.preview; } } 

The structure of this class should be strictly constant in all your non-standard (advanced) widgets, since the same structure of methods and in class launcher AdvancedWidgetInfo.

Directly View


The main class of our widget
 public class WidgetView extends View { ContentObserver observer; GestureDetector gd; Scroller scroller; NinePatchDrawable frame; Bitmap loading; Bitmap broken; Item[] items = null; float touchLastY = 0; float touchDownY = 0; boolean touchTap = false; boolean touchScroll = false; long loaderId = -1; int getScrollY = 0; int width = 160; int height = 200; public WidgetView(Context context) { super(context); gd = new GestureDetector(context, new GestureListener()); scroller = new Scroller(context); observer = new ContentObserver(new Handler()){ @Override public void onChange(boolean selfChange){ super.onChange(selfChange); loadThumbnails(); } }; frame = (NinePatchDrawable)getResources().getDrawable(R.drawable.frame); loading = getBmp(R.drawable.loading); broken = getBmp(R.drawable.broken); } @Override public void onAttachedToWindow(){ super.onAttachedToWindow(); loadThumbnails(); getContext().getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer); getContext().getContentResolver().registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, observer); } @Override public void onDetachedFromWindow(){ super.onDetachedFromWindow(); getContext().getContentResolver().unregisterContentObserver(observer); } @Override protected void onDraw(Canvas canvas){ super.onDraw(canvas); Paint p = new Paint(); p.setDither(true); p.setFilterBitmap(true); int pos = getSelected(); float percent = getSelectedPercent(); if(items != null){ if(items.length == 0){ Bitmap camera = getBmp(R.drawable.camera); Transformation transform = new Transformation(0, percent, 160, 140); p.setAlpha(transform.alpha); canvas.drawBitmap(camera, transform.matrix3d, p); return; } if(pos >= 1){ if(items[pos-1] != null){ if(items[pos-1].thumbnail != null){ Bitmap thumbnail = items[pos-1].thumbnail; Transformation transform = new Transformation(-1, percent, thumbnail.getWidth(), thumbnail.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(thumbnail, transform.matrix3d, p); } else { Transformation transform = new Transformation(-1, percent, broken.getWidth(), broken.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(broken, transform.matrix3d, p); } } else { Transformation transform = new Transformation(-1, percent, loading.getWidth(), loading.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(loading, transform.matrix3d, p); } } if(items.length-1 >= pos+1){ if(items[pos+1] != null){ if(items[pos+1].thumbnail != null){ Bitmap thumbnail = items[pos+1].thumbnail; Transformation transform = new Transformation(1, percent, thumbnail.getWidth(), thumbnail.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(thumbnail, transform.matrix3d, p); } else { Transformation transform = new Transformation(1, percent, broken.getWidth(), broken.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(broken, transform.matrix3d, p); } } else { Transformation transform = new Transformation(1, percent, loading.getWidth(), loading.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(loading, transform.matrix3d, p); } } if(items.length-1 >= pos){ if(items[pos] != null){ if(items[pos].thumbnail != null){ Bitmap thumbnail = items[pos].thumbnail; Transformation transform = new Transformation(0, percent, thumbnail.getWidth(), thumbnail.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(thumbnail, transform.matrix3d, p); } else { Transformation transform = new Transformation(0, percent, broken.getWidth(), broken.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(broken, transform.matrix3d, p); } } else { Transformation transform = new Transformation(0, percent, loading.getWidth(), loading.getHeight()); p.setAlpha(transform.alpha); canvas.drawBitmap(loading, transform.matrix3d, p); } } } else { Transformation transform = new Transformation(0, 0, loading.getWidth(), loading.getHeight()); canvas.drawBitmap(loading, transform.matrix3d, null); } } @Override public boolean onTouchEvent(MotionEvent me) { if(items == null) return true; gd.onTouchEvent(me); float touchY = me.getY(); if(me.getAction() == MotionEvent.ACTION_DOWN){ if(!scroller.isFinished()){ scroller.abortAnimation(); } else { touchTap = true; touchScroll = false; } touchDownY = touchLastY = touchY; } else if(me.getAction() == MotionEvent.ACTION_MOVE){ if(Math.abs(touchY - touchDownY) > ViewConfiguration.getTouchSlop() || touchScroll){ getParent().requestDisallowInterceptTouchEvent(true); touchTap = false; touchScroll = true; getScrollY += -(int)(touchY - touchLastY); computeOverscroll(); } touchLastY = touchY; } else if((me.getAction() == MotionEvent.ACTION_UP || me.getAction() == MotionEvent.ACTION_CANCEL) && scroller.isFinished()){ if(touchTap && me.getAction() == MotionEvent.ACTION_UP){ tap(); } setSelected(getSelected(), true); getParent().requestDisallowInterceptTouchEvent(false); } return true; } @Override public void computeScroll(){ if(scroller.computeScrollOffset()){ int oldY = getScrollY; getScrollY = scroller.getCurrY(); if(oldY != getScrollY){ onScrollChanged(0, getScrollY, 0, oldY); } if(scroller.getFinalY() == getScrollY){ scroller.abortAnimation(); setSelected(getSelected(), true); } postInvalidate(); } } @Override protected void onMeasure(int wms, int hms){ width = MeasureSpec.getSize(wms); height = MeasureSpec.getSize(hms); super.onMeasure(wms, hms); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh){ width = w; height = h; super.onSizeChanged(w, h, oldw, oldh); } void loadThumbnails(){ items = null; getScrollY = 0; invalidate(); Thread loader = new Thread(new Runnable() { public void run() { long currentId = Thread.currentThread().getId(); try{ final ArrayList<MediaItem> list = mediaList(); if(list == null || loaderId != currentId) return; items = new Item[list.size()]; postInvalidate(); for(int i=0; i<list.size(); i++){ if(items == null || loaderId != currentId) return; items[i] = new Item(list.get(i).path, getBmp(list.get(i)), list.get(i).type); postInvalidate(); } } catch(Exception e){} } }); loaderId = loader.getId(); loader.start(); } int getSelected(){ return Math.round(getScrollY/140f); } float getSelectedPercent(){ float f = getScrollY/140f; return f-getSelected(); } void setSelected(int pos, boolean anim){ if(anim && getScrollY!=pos*140){ scroller.startScroll(0, getScrollY, 0, pos*140-getScrollY, 250); } else { getScrollY = pos*140; } invalidate(); } void computeOverscroll(){ if(getScrollY < -50){ getScrollY = -50; } else if(getScrollY > Math.max(items.length-1, 0)*140+50){ getScrollY = Math.max(items.length-1, 0)*140+50; } invalidate(); } void tap(){ try{ if(items != null){ if(items.length == 0){ Intent i = new Intent("android.media.action.IMAGE_CAPTURE"); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(i); return; } if(items[getSelected()] == null) return; int type = items[getSelected()].type; String path = items[getSelected()].path; if(type == 1){ Intent view = new Intent(android.content.Intent.ACTION_VIEW); File imageFile = new File(path); view.setDataAndType(Uri.fromFile(imageFile), "image/*"); view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(view); } else if(type == 2){ Intent view = new Intent(android.content.Intent.ACTION_VIEW); File imageFile = new File(path); view.setDataAndType(Uri.fromFile(imageFile), "video/*"); view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(view); } } } catch(Exception e){} } ArrayList<MediaItem> mediaList(){ try{ ArrayList<MediaItem> list = new ArrayList<MediaItem>(); Cursor c = getContext().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media.DATA, MediaStore.Images.Media.DATE_ADDED}, null, null, null); if(c.moveToFirst()){ do{ list.add(new MediaItem(c.getString(c.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)), c.getString(c.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)), 1)); } while(c.moveToNext()); } c.close(); c = getContext().getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Video.Media.DATA, MediaStore.Video.Media.DATE_ADDED}, null, null, null); if(c.moveToFirst()){ do{ list.add(new MediaItem(c.getString(c.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)), c.getString(c.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)), 2)); } while(c.moveToNext()); } c.close(); Collections.sort(list, new Comparator<MediaItem>(){ @Override public int compare(MediaItem mi1, MediaItem mi2) { return mi1.date.compareToIgnoreCase(mi2.date); } }); Collections.reverse(list); return list; } catch(Exception e){ return new ArrayList<MediaItem>(); } } Bitmap getBmp(MediaItem mi){ try{ Paint p = new Paint(); p.setDither(true); p.setFilterBitmap(true); if(mi.type == 1){ int rot = 0; try{ ExifInterface rote = new ExifInterface(mi.path); int r = rote.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1); if(r == 3){ rot = 180; } else if(r == 8){ rot = 270; } else if(r == 6){ rot = 90; } else { rot = 0; } } catch(Exception e){} BitmapFactory.Options bfo = new BitmapFactory.Options(); bfo.inJustDecodeBounds = true; BitmapFactory.decodeFile(mi.path, bfo); if(rot == 90 || rot == 270){ bfo.inSampleSize = Math.min(bfo.outWidth/140, bfo.outHeight/120); } else { bfo.inSampleSize = Math.max(bfo.outWidth/140, bfo.outHeight/120); } bfo.inJustDecodeBounds = false; Bitmap src = BitmapFactory.decodeFile(mi.path, bfo); if(rot != 0){ Matrix m = new Matrix(); m.postRotate(rot); src = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), m, true); } float aspect = (float)src.getWidth()/src.getHeight(); int image_w = 160; int image_h = 140; if(aspect > 1){ image_h = (int)((image_w-20)/aspect+20); } else { image_w = (int)((image_h-20)*aspect+20); } Bitmap result = Bitmap.createBitmap(image_w, image_h, Config.ARGB_8888); Canvas c = new Canvas(result); frame.setBounds(0, 0, image_w, image_h); frame.draw(c); c.drawBitmap(src, null, new RectF(10, 10, image_w-10, image_h-10), p); return result; } else if(mi.type == 2){ Bitmap src = ThumbnailUtils.createVideoThumbnail(mi.path, MediaStore.Video.Thumbnails.MINI_KIND); float aspect = (float)src.getWidth()/src.getHeight(); int image_w = 160; int image_h = (int)((image_w-20)/aspect+20); Bitmap result = Bitmap.createBitmap(image_w, image_h, Config.ARGB_8888); Canvas c = new Canvas(result); frame.setBounds(0, 0, image_w, image_h); frame.draw(c); c.drawBitmap(src, null, new RectF(10, 10, image_w-10, image_h-10), p); c.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.video), (image_w-64)/2, (image_h-64)/2, p); return result; } } catch(Exception e){ return null; } catch(OutOfMemoryError e){ return null; } return null; } Bitmap getBmp(int res){ Bitmap bmp = Bitmap.createBitmap(160, 140, Config.ARGB_8888); Canvas c = new Canvas(bmp); Paint p = new Paint(); p.setDither(true); p.setFilterBitmap(true); frame.setBounds(0, 0, 160, 140); frame.draw(c); c.drawBitmap(BitmapFactory.decodeResource(getResources(), res), 48, 38, p); return bmp; } public class MediaItem { String path; String date; int type; public MediaItem(String str1, String str2, int i){ path = str1; date = str2; type = i; } } public class Item { String path; Bitmap thumbnail; int type; public Item(String str, Bitmap bmp, int i){ path = str; thumbnail = bmp; type = i; } } public class Transformation { Matrix matrix3d; int alpha; public Transformation(int pos, float percent, int imageWidth, int imageHeight){ float centerX = (float)width/2; float centerY = (float)height/2; float f = -pos + percent; centerY -= (float)Math.sin(f*Math.PI/2)*40; float f1 = Math.abs(f) - 0.5f; if(f1 < 0) f1 = 0; float f2 = Math.abs(f) / 1.5f; alpha = 255 - (int)(255*f1); float scale = (float) (1 - 0.4*f2); Camera c = new Camera(); matrix3d = new Matrix(); c.save(); float rotate = percent*100; if(pos == 0) rotate *= -1; c.rotateX(rotate); c.getMatrix(matrix3d); matrix3d.preTranslate(-imageWidth/2, -imageHeight/2); matrix3d.postScale(scale, scale); matrix3d.postTranslate(centerX, centerY); c.restore(); } } private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onFling(MotionEvent me1, MotionEvent me2, float vX, float vY){ scroller.fling(0, getScrollY, 0, -(int)vY, 0, 0, -50, Math.max(items.length-1, 0)*140+50); invalidate(); return true; } } } 


A bit of explanation:

ContentObserver logs all changes in external memory. For example, when adding a photo, the onChange method (boolean selfChange) is called several times at intervals of a couple of milliseconds. The loadThumbnails () method loads thumbnails of all photos in a separate stream. Each time the onChange ContentObserver method is triggered, a new stream is created to create thumbnails. There is a situation when adding one photo immediately creates several threads at the same time, doing the same actions. In order to avoid this situation, the loaderId variable stores the id of the last created stream, and all the streams have a comparison of the saved id and id of this stream. If the numbers are not equal, the stream is destroyed.

Paging and throwing gestures do not cause movement of the View canvas, as is customary. Instead, the variable getScrollY is changed. Integer getSelected () returns the current position in the thumbnail array, float getSelectedPercent () returns the angle of the thumbnails relative to the viewer. A value of 0.5 corresponds to 45 degrees. Perhaps the following picture will clarify the situation:



In the onTouchEvent (MotionEvent me) method, it is important not to forget to call getParent (). RequestDisallowInterceptTouchEvent (true) so that during the thumbnails browsing in the widget, the tables in the launcher do not scroll.

Well, at the end a few words about the Transformation class. It returns a transformation matrix for thumbnails, depending on what the above described getSelected () and getSelectedPercent () return.

It should also be noted that the thumbnails of all images and videos are stored in the Item [] array. This is bad. Thumbnail caching is a good option.

Conclusion


So I briefly explained how you can make a cute photo thumbnail viewer. I am very surprised that such beautiful three-dimensional effects of paging images do not slow down on my ancient Samsung Galaxy Gio phone. You can see this in the video:

Source: https://habr.com/ru/post/232477/


All Articles