/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.constraintlayout.utils.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Path; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; import android.widget.ImageView; import androidx.annotation.RequiresApi; import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.R; /** * An ImageView that can display, combine and filter images. Added in 2.0 *

* Subclass of ImageView to handle various common filtering operations *

* *

ImageFilterView attributes

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
altSrcProvide and alternative image to the src image to allow cross fading
saturationSets the saturation of the image.
0 = grayscale, 1 = original, 2 = hyper saturated
brightnessSets the brightness of the image.
0 = black, 1 = original, 2 = twice as bright *
warmthThis adjust the apparent color temperature of the image.
1=neutral, 2=warm, .5=cold
contrastThis sets the contrast. 1 = unchanged, 0 = gray, 2 = high contrast
crossfadeSet the current mix between the two images.
0=src 1= altSrc image
round(id) call the TransitionListener with this trigger id
roundPercentSet the corner radius of curvature as a fraction of the smaller side. * For squares 1 will result in a circle
overlayDefines whether the alt image will be faded in on top of the original image or if it will be * crossfaded with it. Default is true. Set to false for semitransparent objects
*/ public class ImageFilterView extends androidx.appcompat.widget.AppCompatImageView { static class ImageMatrix { float[] mMatrix = new float[4 * 5]; ColorMatrix mColorMatrix = new ColorMatrix(); ColorMatrix mTmpColorMatrix = new ColorMatrix(); float mBrightness = 1; float mSaturation = 1; float mContrast = 1; float mWarmth = 1; private void saturation(float saturationStrength) { float Rf = 0.2999f; float Gf = 0.587f; float Bf = 0.114f; float s = saturationStrength; float ms = 1.0f - s; float Rt = Rf * ms; float Gt = Gf * ms; float Bt = Bf * ms; mMatrix[0] = (Rt + s); mMatrix[1] = Gt; mMatrix[2] = Bt; mMatrix[3] = 0; mMatrix[4] = 0; mMatrix[5] = Rt; mMatrix[6] = (Gt + s); mMatrix[7] = Bt; mMatrix[8] = 0; mMatrix[9] = 0; mMatrix[10] = Rt; mMatrix[11] = Gt; mMatrix[12] = (Bt + s); mMatrix[13] = 0; mMatrix[14] = 0; mMatrix[15] = 0; mMatrix[16] = 0; mMatrix[17] = 0; mMatrix[18] = 1; mMatrix[19] = 0; } private void warmth(float warmth) { float baseTemperature = 5000; if (warmth <= 0) warmth = .01f; float tmpColor_r; float tmpColor_g; float tmpColor_b; float kelvin = baseTemperature / warmth; { // simulate a black body radiation float centiKelvin = kelvin / 100; float colorR, colorG, colorB; if (centiKelvin > 66) { float tmp = centiKelvin - 60.f; // colorR = (329.698727446f * (float) Math.pow(tmp, -0.1332047592f)) // colorG = (288.1221695283f * (float) Math.pow(tmp, 0.0755148492f)) colorR = (329.69873f * (float) Math.pow(tmp, -0.1332047592f)); colorG = (288.1221695283f * (float) Math.pow(tmp, 0.0755148492f)); } else { colorG = (99.4708025861f * (float) Math.log(centiKelvin) - 161.1195681661f); colorR = 255; } if (centiKelvin < 66) { if (centiKelvin > 19) { // 138.5177312231f * (float) Math.log(centiKelvin - 10) - 305.0447927307f); colorB = (138.51773f * (float) Math.log(centiKelvin - 10) - 305.0448f); } else { colorB = 0; } } else { colorB = 255; } tmpColor_r = Math.min(255, Math.max(colorR, 0)); tmpColor_g = Math.min(255, Math.max(colorG, 0)); tmpColor_b = Math.min(255, Math.max(colorB, 0)); } float color_r = tmpColor_r; float color_g = tmpColor_g; float color_b = tmpColor_b; kelvin = baseTemperature; { // simulate a black body radiation float centiKelvin = kelvin / 100; float colorR, colorG, colorB; if (centiKelvin > 66) { float tmp = centiKelvin - 60.f; // colorR = (329.698727446f * (float) Math.pow(tmp, -0.1332047592f)); // colorG = (288.1221695283f * (float) Math.pow(tmp, 0.0755148492f)); colorR = (329.69873f * (float) Math.pow(tmp, -0.13320476f)); colorG = (288.12216f * (float) Math.pow(tmp, 0.075514849f)); } else { //float of (99.4708025861f * (float) Math.log(centiKelvin) - 161.1195681661f); colorG = (99.4708f * (float) Math.log(centiKelvin) - 161.11957f); colorR = 255; } if (centiKelvin < 66) { if (centiKelvin > 19) { //float of (138.5177312231 * Math.log(centiKelvin - 10) - 305.0447927307); colorB = (138.51773f * (float) Math.log(centiKelvin - 10) - 305.0448f); } else { colorB = 0; } } else { colorB = 255; } tmpColor_r = Math.min(255, Math.max(colorR, 0)); tmpColor_g = Math.min(255, Math.max(colorG, 0)); tmpColor_b = Math.min(255, Math.max(colorB, 0)); } color_r /= tmpColor_r; color_g /= tmpColor_g; color_b /= tmpColor_b; mMatrix[0] = color_r; mMatrix[1] = 0; mMatrix[2] = 0; mMatrix[3] = 0; mMatrix[4] = 0; mMatrix[5] = 0; mMatrix[6] = color_g; mMatrix[7] = 0; mMatrix[8] = 0; mMatrix[9] = 0; mMatrix[10] = 0; mMatrix[11] = 0; mMatrix[12] = color_b; mMatrix[13] = 0; mMatrix[14] = 0; mMatrix[15] = 0; mMatrix[16] = 0; mMatrix[17] = 0; mMatrix[18] = 1; mMatrix[19] = 0; } private void brightness(float brightness) { mMatrix[0] = brightness; mMatrix[1] = 0; mMatrix[2] = 0; mMatrix[3] = 0; mMatrix[4] = 0; mMatrix[5] = 0; mMatrix[6] = brightness; mMatrix[7] = 0; mMatrix[8] = 0; mMatrix[9] = 0; mMatrix[10] = 0; mMatrix[11] = 0; mMatrix[12] = brightness; mMatrix[13] = 0; mMatrix[14] = 0; mMatrix[15] = 0; mMatrix[16] = 0; mMatrix[17] = 0; mMatrix[18] = 1; mMatrix[19] = 0; } void updateMatrix(ImageView view) { mColorMatrix.reset(); boolean filter = false; if (mSaturation != 1.0f) { saturation(mSaturation); mColorMatrix.set(mMatrix); filter = true; } if (mContrast != 1.0f) { mTmpColorMatrix.setScale(mContrast, mContrast, mContrast, 1); mColorMatrix.postConcat(mTmpColorMatrix); filter = true; } if (mWarmth != 1.0f) { warmth(mWarmth); mTmpColorMatrix.set(mMatrix); mColorMatrix.postConcat(mTmpColorMatrix); filter = true; } if (mBrightness != 1.0f) { brightness(mBrightness); mTmpColorMatrix.set(mMatrix); mColorMatrix.postConcat(mTmpColorMatrix); filter = true; } if (filter) { view.setColorFilter(new ColorMatrixColorFilter(mColorMatrix)); } else { view.clearColorFilter(); } } } private ImageMatrix mImageMatrix = new ImageMatrix(); private boolean mOverlay = true; private Drawable mAltDrawable = null; private Drawable mDrawable = null; private float mCrossfade = 0; private float mRoundPercent = 0; // rounds the corners as a percent private float mRound = Float.NaN; // rounds the corners in dp if NaN RoundPercent is in effect private Path mPath; ViewOutlineProvider mViewOutlineProvider; RectF mRect; Drawable[] mLayers = new Drawable[2]; LayerDrawable mLayer; // ======================== support for pan/zoom/rotate ================= // defined as 0 = center of screen // if with < scree with, 1 is the right edge lines up with screen // if width > screen width, 1 is thee left edge lines up // -1 works similarly // zoom 1 = the image fits such that the view is filed float mPanX = Float.NaN; float mPanY = Float.NaN; float mZoom = Float.NaN; float mRotate = Float.NaN; /** * Gets the pan from the center * pan of 1 the image is "all the way to the right" * if the images width is greater than the screen width, * pan = 1 results in the left edge lining up * if the images width is less than the screen width, * pan = 1 results in the right edges lining up * if image width == screen width it does nothing * * @return the pan in X. Where 0 is centered = Float. NaN if not set */ public float getImagePanX() { return mPanX; } /** * gets the pan from the center * pan of 1 the image is "all the way to the bottom" * if the images width is greater than the screen height, * pan = 1 results in the bottom edge lining up * if the images width is less than the screen height, * pan = 1 results in the top edges lining up * if image height == screen height it does nothing * * @return pan in y. Where 0 is centered NaN if not set */ public float getImagePanY() { return mPanY; } /** * gets the zoom where 1 scales the image just enough to fill the view * * @return the zoom factor */ public float getImageZoom() { return mZoom; } /** * gets the rotation * * @return the rotation in degrees */ public float getImageRotate() { return mRotate; } /** * sets the pan from the center * pan of 1 the image is "all the way to the right" * if the images width is greater than the screen width, * pan = 1 results in the left edge lining up * if the images width is less than the screen width, * pan = 1 results in the right edges lining up * if image width == screen width it does nothing * * @param pan sets the pan in X. Where 0 is centered */ public void setImagePanX(float pan) { mPanX = pan; updateViewMatrix(); } /** * sets the pan from the center * pan of 1 the image is "all the way to the bottom" * if the images width is greater than the screen height, * pan = 1 results in the bottom edge lining up * if the images width is less than the screen height, * pan = 1 results in the top edges lining up * if image height == screen height it does nothing * * @param pan sets the pan in X. Where 0 is centered */ public void setImagePanY(float pan) { mPanY = pan; updateViewMatrix(); } /** * sets the zoom where 1 scales the image just enough to fill the view * * @param zoom the zoom factor */ public void setImageZoom(float zoom) { mZoom = zoom; updateViewMatrix(); } /** * sets the rotation angle of the image in degrees * * @param rotation the rotation in degrees */ public void setImageRotate(float rotation) { mRotate = rotation; updateViewMatrix(); } @Override public void setImageDrawable(Drawable drawable) { if (mAltDrawable != null && drawable != null) { mDrawable = drawable.mutate(); mLayers[0] = mDrawable; mLayers[1] = mAltDrawable; mLayer = new LayerDrawable(mLayers); super.setImageDrawable(mLayer); setCrossfade(mCrossfade); } else { super.setImageDrawable(drawable); } } @Override public void setImageResource(int resId) { if (mAltDrawable != null) { mDrawable = AppCompatResources.getDrawable(getContext(), resId).mutate(); mLayers[0] = mDrawable; mLayers[1] = mAltDrawable; mLayer = new LayerDrawable(mLayers); super.setImageDrawable(mLayer); setCrossfade(mCrossfade); } else { super.setImageResource(resId); } } /** * Set the alternative Image resource used in cross fading * @param resId id of drawable */ public void setAltImageResource(int resId) { mAltDrawable = AppCompatResources.getDrawable(getContext(), resId); setAltImageDrawable(mAltDrawable); } /** * Set the alternative Image Drawable used in cross fading. * @param altDrawable of drawable */ public void setAltImageDrawable(Drawable altDrawable) { mAltDrawable = altDrawable.mutate(); mLayers[0] = mDrawable; mLayers[1] = mAltDrawable; mLayer = new LayerDrawable(mLayers); super.setImageDrawable(mLayer); setCrossfade(mCrossfade); } private void updateViewMatrix() { if (Float.isNaN(mPanX) && Float.isNaN(mPanY) && Float.isNaN(mZoom) && Float.isNaN(mRotate) ) { setScaleType(ScaleType.FIT_CENTER); return; } setMatrix(); } private void setMatrix() { if (Float.isNaN(mPanX) && Float.isNaN(mPanY) && Float.isNaN(mZoom) && Float.isNaN(mRotate) ) { return; } float panX = Float.isNaN(mPanX) ? 0 : mPanX; float panY = Float.isNaN(mPanY) ? 0 : mPanY; float zoom = Float.isNaN(mZoom) ? 1 : mZoom; float rota = Float.isNaN(mRotate) ? 0 : mRotate; Matrix imageMatrix = new Matrix(); imageMatrix.reset(); float iw = getDrawable().getIntrinsicWidth(); float ih = getDrawable().getIntrinsicHeight(); float sw = getWidth(); float sh = getHeight(); float scale = zoom * ((iw * sh < ih * sw) ? sw / iw : sh / ih); imageMatrix.postScale(scale, scale); float tx = 0.5f * (panX * (sw - scale * iw) + sw - (scale * iw)); float ty = 0.5f * (panY * (sh - scale * ih) + sh - (scale * ih)); imageMatrix.postTranslate(tx, ty); imageMatrix.postRotate(rota, sw / 2, sh / 2); setImageMatrix(imageMatrix); setScaleType(ScaleType.MATRIX); } public ImageFilterView(Context context) { super(context); init(context, null); } public ImageFilterView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public ImageFilterView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { if (attrs != null) { TypedArray a = getContext() .obtainStyledAttributes(attrs, R.styleable.ImageFilterView); final int count = a.getIndexCount(); mAltDrawable = a.getDrawable(R.styleable.ImageFilterView_altSrc); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.ImageFilterView_crossfade) { mCrossfade = a.getFloat(attr, 0); } else if (attr == R.styleable.ImageFilterView_warmth) { setWarmth(a.getFloat(attr, 0)); } else if (attr == R.styleable.ImageFilterView_saturation) { setSaturation(a.getFloat(attr, 0)); } else if (attr == R.styleable.ImageFilterView_contrast) { setContrast(a.getFloat(attr, 0)); } else if (attr == R.styleable.ImageFilterView_brightness) { setBrightness(a.getFloat(attr, 0)); } else if (attr == R.styleable.ImageFilterView_round) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setRound(a.getDimension(attr, 0)); } } else if (attr == R.styleable.ImageFilterView_roundPercent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setRoundPercent(a.getFloat(attr, 0)); } } else if (attr == R.styleable.ImageFilterView_overlay) { setOverlay(a.getBoolean(attr, mOverlay)); } else if (attr == R.styleable.ImageFilterView_imagePanX) { setImagePanX(a.getFloat(attr, mPanX)); } else if (attr == R.styleable.ImageFilterView_imagePanY) { setImagePanY(a.getFloat(attr, mPanY)); } else if (attr == R.styleable.ImageFilterView_imageRotate) { setImageRotate(a.getFloat(attr, mRotate)); } else if (attr == R.styleable.ImageFilterView_imageZoom) { setImageZoom(a.getFloat(attr, mZoom)); } } a.recycle(); mDrawable = getDrawable(); if (mAltDrawable != null && mDrawable != null) { mLayers[0] = mDrawable = getDrawable().mutate(); mLayers[1] = mAltDrawable.mutate(); mLayer = new LayerDrawable(mLayers); mLayer.getDrawable(1).setAlpha((int) (255 * (mCrossfade))); if (!mOverlay) { mLayer.getDrawable(0).setAlpha((int) (255 * (1 - mCrossfade))); } super.setImageDrawable(mLayer); } else { mDrawable = getDrawable(); if (mDrawable != null) { mLayers[0] = mDrawable = mDrawable.mutate(); } } } } /** * Defines whether the alt image will be faded in on top * of the original image or if it will be crossfaded with it. * Default is true; * * @param overlay */ private void setOverlay(boolean overlay) { mOverlay = overlay; } /** * sets the saturation of the image; * 0 = grayscale, 1 = original, 2 = hyper saturated * * @param saturation */ public void setSaturation(float saturation) { mImageMatrix.mSaturation = saturation; mImageMatrix.updateMatrix(this); } /** * Returns the currently applied saturation * * @return 0 = grayscale, 1 = original, 2 = hyper saturated */ public float getSaturation() { return mImageMatrix.mSaturation; } /** * This sets the contrast. 1 = unchanged, 0 = gray, 2 = high contrast * * @param contrast */ public void setContrast(float contrast) { mImageMatrix.mContrast = contrast; mImageMatrix.updateMatrix(this); } /** * Returns the currently applied contrast * * @return 1 = unchanged, 0 = gray, 2 = high contrast */ public float getContrast() { return mImageMatrix.mContrast; } /** * This makes the apparent color temperature of the image warmer or colder. * * @param warmth 1 is neutral, 2 is warm, .5 is cold */ public void setWarmth(float warmth) { mImageMatrix.mWarmth = warmth; mImageMatrix.updateMatrix(this); } /** * Returns the currently applied warmth * * @return warmth 1 is neutral, 2 is warm, .5 is cold */ public float getWarmth() { return mImageMatrix.mWarmth; } /** * Set the current mix between the two images that can be set on this view. * * @param crossfade a number from 0 to 1 */ public void setCrossfade(float crossfade) { mCrossfade = crossfade; if (mLayers != null) { if (!mOverlay) { mLayer.getDrawable(0).setAlpha((int) (255 * (1 - mCrossfade))); } mLayer.getDrawable(1).setAlpha((int) (255 * mCrossfade)); super.setImageDrawable(mLayer); } } /** * Returns the currently applied crossfade. * * @return a number from 0 to 1 */ public float getCrossfade() { return mCrossfade; } /** * sets the brightness of the image; * 0 = black, 1 = original, 2 = twice as bright * * @param brightness */ public void setBrightness(float brightness) { mImageMatrix.mBrightness = brightness; mImageMatrix.updateMatrix(this); } /** * Returns the currently applied brightness * * @return brightness 0 = black, 1 = original, 2 = twice as bright */ public float getBrightness() { return mImageMatrix.mBrightness; } /** * Set the corner radius of curvature as a fraction of the smaller side. * For squares 1 will result in a circle * * @param round the radius of curvature as a fraction of the smaller width */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float round) { boolean change = (mRoundPercent != round); mRoundPercent = round; if (mRoundPercent != 0.0f) { if (mPath == null) { mPath = new Path(); } if (mRect == null) { mRect = new RectF(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (mViewOutlineProvider == null) { mViewOutlineProvider = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { int w = getWidth(); int h = getHeight(); float r = Math.min(w, h) * mRoundPercent / 2; outline.setRoundRect(0, 0, w, h, r); } }; setOutlineProvider(mViewOutlineProvider); } setClipToOutline(true); } int w = getWidth(); int h = getHeight(); float r = Math.min(w, h) * mRoundPercent / 2; mRect.set(0, 0, w, h); mPath.reset(); mPath.addRoundRect(mRect, r, r, Path.Direction.CW); } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setClipToOutline(false); } } if (change) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { invalidateOutline(); } } } /** * Set the corner radius of curvature * * @param round the radius of curvature NaN = default meaning roundPercent in effect */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public void setRound(float round) { if (Float.isNaN(round)) { mRound = round; float tmp = mRoundPercent; mRoundPercent = -1; setRoundPercent(tmp); // force eval of roundPercent return; } boolean change = (mRound != round); mRound = round; if (mRound != 0.0f) { if (mPath == null) { mPath = new Path(); } if (mRect == null) { mRect = new RectF(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (mViewOutlineProvider == null) { mViewOutlineProvider = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { int w = getWidth(); int h = getHeight(); outline.setRoundRect(0, 0, w, h, mRound); } }; setOutlineProvider(mViewOutlineProvider); } setClipToOutline(true); } int w = getWidth(); int h = getHeight(); mRect.set(0, 0, w, h); mPath.reset(); mPath.addRoundRect(mRect, mRound, mRound, Path.Direction.CW); } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setClipToOutline(false); } } if (change) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { invalidateOutline(); } } } /** * Get the fractional corner radius of curvature. * * @return Fractional radius of curvature with respect to smallest size */ public float getRoundPercent() { return mRoundPercent; } /** * Get the corner radius of curvature NaN = RoundPercent in effect. * * @return Radius of curvature */ public float getRound() { return mRound; } @Override public void draw(Canvas canvas) { boolean clip = false; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (mRoundPercent != 0.0f && mPath != null) { clip = true; canvas.save(); canvas.clipPath(mPath); } } super.draw(canvas); if (clip) { canvas.restore(); } } @Override public void layout(int l, int t, int r, int b) { super.layout(l, t, r, b); setMatrix(); } }